1
0
mirror of https://github.com/kataras/iris.git synced 2025-12-18 02:17:05 +00:00

context transactions removed and make Context.Domain customizable as requested

This commit is contained in:
Gerasimos (Makis) Maropoulos
2022-06-05 06:15:10 +03:00
parent c6911851f1
commit d8af2a1e14
8 changed files with 28 additions and 313 deletions

View File

@@ -1001,7 +1001,8 @@ func (ctx *Context) Host() string {
}
// GetDomain resolves and returns the server's domain.
func GetDomain(hostport string) string {
// To customize its behavior, developers can modify this package-level function at initialization.
var GetDomain = func(hostport string) string {
host := hostport
if tmp, _, err := net.SplitHostPort(hostport); err == nil {
host = tmp
@@ -1459,7 +1460,7 @@ func (ctx *Context) GetContentLength() int64 {
// StatusCode sets the status code header to the response.
// Look .GetStatusCode & .FireStatusCode too.
//
// Remember, the last one before .Write matters except recorder and transactions.
// Note that you must set status code before write response body (except when recorder is used).
func (ctx *Context) StatusCode(statusCode int) {
ctx.writer.WriteHeader(statusCode)
}
@@ -5656,7 +5657,7 @@ func (ctx *Context) MaxAge() int64 {
}
// +------------------------------------------------------------+
// | Advanced: Response Recorder and Transactions |
// | Advanced: Response Recorder |
// +------------------------------------------------------------+
// Record transforms the context's basic and direct responseWriter to a *ResponseRecorder
@@ -5691,72 +5692,6 @@ func (ctx *Context) IsRecording() (*ResponseRecorder, bool) {
return rr, ok
}
// ErrTransactionInterrupt can be used to manually force-complete a Context's transaction
// and log(warn) the wrapped error's message.
// Usage: `... return fmt.Errorf("my custom error message: %w", context.ErrTransactionInterrupt)`.
var ErrTransactionInterrupt = errors.New("transaction interrupted")
// BeginTransaction starts a scoped transaction.
//
// Can't say a lot here because it will take more than 200 lines to write about.
// You can search third-party articles or books on how Business Transaction works (it's quite simple, especially here).
//
// Note that this is unique and new
// (=I haver never seen any other examples or code in Golang on this subject, so far, as with the most of iris features...)
// it's not covers all paths,
// such as databases, this should be managed by the libraries you use to make your database connection,
// this transaction scope is only for context's response.
// Transactions have their own middleware ecosystem also.
//
// See https://github.com/kataras/iris/tree/master/_examples/ for more
func (ctx *Context) BeginTransaction(pipe func(t *Transaction)) {
// do NOT begin a transaction when the previous transaction has been failed
// and it was requested scoped or SkipTransactions called manually.
if ctx.TransactionsSkipped() {
return
}
// start recording in order to be able to control the full response writer
ctx.Record()
t := newTransaction(ctx) // it calls this *context, so the overriding with a new pool's New of context.Context wil not work here.
defer func() {
if err := recover(); err != nil {
ctx.app.Logger().Warn(fmt.Errorf("recovery from panic: %w", ErrTransactionInterrupt))
// complete (again or not , doesn't matters) the scope without loud
t.Complete(nil)
// we continue as normal, no need to return here*
}
// write the temp contents to the original writer
t.Context().ResponseWriter().CopyTo(ctx.writer)
// give back to the transaction the original writer (SetBeforeFlush works this way and only this way)
// this is tricky but nessecery if we want ctx.FireStatusCode to work inside transactions
t.Context().ResetResponseWriter(ctx.writer)
}()
// run the worker with its context clone inside.
pipe(t)
}
// skipTransactionsContextKey set this to any value to stop executing next transactions
// it's a context-key in order to be used from anywhere, set it by calling the SkipTransactions()
const skipTransactionsContextKey = "iris.transactions.skip"
// SkipTransactions if called then skip the rest of the transactions
// or all of them if called before the first transaction
func (ctx *Context) SkipTransactions() {
ctx.values.Set(skipTransactionsContextKey, 1)
}
// TransactionsSkipped returns true if the transactions skipped or canceled at all.
func (ctx *Context) TransactionsSkipped() bool {
if n, err := ctx.values.GetInt(skipTransactionsContextKey); err == nil && n == 1 {
return true
}
return false
}
// Exec calls the framewrok's ServeHTTPC
// based on this context but with a changed method and path
// like it was requested by the user, but it is not.

View File

@@ -29,17 +29,14 @@ func releaseResponseRecorder(w *ResponseRecorder) {
rrpool.Put(w)
}
// A ResponseRecorder is used mostly by context's transactions
// in order to record and change if needed the body, status code and headers.
// A ResponseRecorder is used mostly for testing
// in order to record and modify, if necessary, the body and status code and headers.
//
// Developers are not limited to manually ask to record a response.
// To turn on the recorder from a Handler,
// rec := context.Recorder()
// See `context.Recorder`` method too.
type ResponseRecorder struct {
ResponseWriter
// keep track of the body in order to be
// resetable and useful inside custom transactions
// keep track of the body written.
chunks []byte
// the saved headers
headers http.Header

View File

@@ -1,179 +0,0 @@
package context
// TransactionErrResult could be named also something like 'MaybeError',
// it is useful to send it on transaction.Complete in order to execute a custom error mesasge to the user.
//
// in simple words it's just a 'traveler message' between the transaction and its scope.
// it is totally optional
type TransactionErrResult struct {
StatusCode int
// if reason is empty then the already relative registered (custom or not)
// error will be executed if the scope allows that.
Reason string
ContentType string
}
// Error returns the reason given by the user or an empty string
func (err TransactionErrResult) Error() string {
return err.Reason
}
// IsFailure returns true if this is an actual error
func (err TransactionErrResult) IsFailure() bool {
return StatusCodeNotSuccessful(err.StatusCode)
}
// NewTransactionErrResult returns a new transaction result with the given error message,
// it can be empty too, but if not then the transaction's scope is decided what to do with that
func NewTransactionErrResult() TransactionErrResult {
return TransactionErrResult{}
}
// TransactionScope is the manager of the transaction's response, can be resseted and skipped
// from its parent context or execute an error or skip other transactions
type TransactionScope interface {
// EndTransaction returns if can continue to the next transactions or not (false)
// called after Complete, empty or not empty error
EndTransaction(maybeErr TransactionErrResult, ctx *Context) bool
}
// TransactionScopeFunc the transaction's scope signature
type TransactionScopeFunc func(maybeErr TransactionErrResult, ctx *Context) bool
// EndTransaction ends the transaction with a callback to itself, implements the TransactionScope interface
func (tsf TransactionScopeFunc) EndTransaction(maybeErr TransactionErrResult, ctx *Context) bool {
return tsf(maybeErr, ctx)
}
// +------------------------------------------------------------+
// | Transaction Implementation |
// +------------------------------------------------------------+
// Transaction gives the users the opportunity to code their route handlers cleaner and safier
// it receives a scope which is decided when to send an error to the user, recover from panics
// stop the execution of the next transactions and so on...
//
// it's default scope is the TransientTransactionScope which is silently
// skips the current transaction's response if transaction.Complete accepts a non-empty error.
//
// Create and set custom transactions scopes with transaction.SetScope.
//
// For more information please visit the tests.
type Transaction struct {
context *Context
parent *Context
hasError bool
scope TransactionScope
}
func newTransaction(from *Context) *Transaction {
tempCtx := *from
writer := tempCtx.ResponseWriter().Clone()
tempCtx.ResetResponseWriter(writer)
t := &Transaction{
parent: from,
context: &tempCtx,
scope: TransientTransactionScope,
}
return t
}
// Context returns the current context of the transaction.
func (t *Transaction) Context() *Context {
return t.context
}
// SetScope sets the current transaction's scope
// iris.RequestTransactionScope || iris.TransientTransactionScope (default).
func (t *Transaction) SetScope(scope TransactionScope) {
t.scope = scope
}
// Complete completes the transaction
// rollback and send an error when the error is not empty.
// The next steps depends on its Scope.
//
// The error can be a type of context.NewTransactionErrResult().
func (t *Transaction) Complete(err error) {
maybeErr := TransactionErrResult{}
if err != nil {
t.hasError = true
statusCode := 400 // bad request
reason := err.Error()
cType := "text/plain; charset=" + t.context.Application().ConfigurationReadOnly().GetCharset()
if errWstatus, ok := err.(TransactionErrResult); ok {
if errWstatus.StatusCode > 0 {
statusCode = errWstatus.StatusCode
}
if errWstatus.Reason != "" {
reason = errWstatus.Reason
}
// get the content type used on this transaction
if cTypeH := t.context.GetContentType(); cTypeH != "" {
cType = cTypeH
}
}
maybeErr.StatusCode = statusCode
maybeErr.Reason = reason
maybeErr.ContentType = cType
}
// the transaction ends with error or not error, it decides what to do next with its Response
// the Response is appended to the parent context an all cases but it checks for empty body,headers and all that,
// if they are empty (silent error or not error at all)
// then all transaction's actions are skipped as expected
canContinue := t.scope.EndTransaction(maybeErr, t.context)
if !canContinue {
t.parent.SkipTransactions()
}
}
// TransientTransactionScope explanation:
//
// independent 'silent' scope, if transaction fails (if transaction.IsFailure() == true)
// then its response is not written to the real context no error is provided to the user.
// useful for the most cases.
var TransientTransactionScope = TransactionScopeFunc(func(maybeErr TransactionErrResult, ctx *Context) bool {
if maybeErr.IsFailure() {
ctx.Recorder().Reset() // this response is skipped because it's empty.
}
return true
})
// RequestTransactionScope explanation:
//
// if scope fails (if transaction.IsFailure() == true)
// then the rest of the context's response (transaction or normal flow)
// is not written to the client, and an error status code is written instead.
var RequestTransactionScope = TransactionScopeFunc(func(maybeErr TransactionErrResult, ctx *Context) bool {
if maybeErr.IsFailure() {
// we need to register a beforeResponseFlush event here in order
// to execute last the FireStatusCode
// (which will reset the whole response's body, status code and headers set from normal flow or other transactions too)
ctx.ResponseWriter().SetBeforeFlush(func() {
// we need to re-take the context's response writer
// because inside here the response writer is changed to the original's
// look ~context:1306
w := ctx.ResponseWriter().(*ResponseRecorder)
if maybeErr.Reason != "" {
// send the error with the info user provided
w.SetBodyString(maybeErr.Reason)
w.WriteHeader(maybeErr.StatusCode)
ctx.ContentType(maybeErr.ContentType)
} else {
// else execute the registered user error and skip the next transactions and all normal flow,
ctx.StopWithStatus(maybeErr.StatusCode)
}
})
return false
}
return true
})