diff --git a/HISTORY.md b/HISTORY.md index 92602986..72496cec 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -28,6 +28,8 @@ The codebase for Dependency Injection, Internationalization and localization and ## Fixes and Improvements +- Make `Context.Domain()` customizable by letting developers to set the custom `Context.GetDomain` package-level function. +- Remove Request Context-based Transaction feature as its usage can be replaced with just the Iris Context (as of go1.7+) and better [project](_examples/project) structure. - Fix [#1882](https://github.com/kataras/iris/issues/1882) - Fix [#1877](https://github.com/kataras/iris/issues/1877) - Fix [#1876](https://github.com/kataras/iris/issues/1876) diff --git a/_examples/README.md b/_examples/README.md index 64eb7c9a..28821cc0 100644 --- a/_examples/README.md +++ b/_examples/README.md @@ -200,7 +200,6 @@ * [Protocol Buffers](response-writer/protobuf/main.go) * [HTTP/2 Server Push](response-writer/http2push/main.go) * [Stream Writer](response-writer/stream-writer/main.go) - * [Transactions](response-writer/transactions/main.go) * [Server-Sent Events](response-writer/sse/main.go) * [SSE 3rd-party (r3labs/sse)](response-writer/sse-third-party/main.go) * [SSE 3rd-party (alexandrevicenzi/go-sse)](response-writer/sse-third-party-2/main.go) diff --git a/_examples/response-writer/transactions/main.go b/_examples/response-writer/transactions/main.go deleted file mode 100644 index 7b18b64d..00000000 --- a/_examples/response-writer/transactions/main.go +++ /dev/null @@ -1,54 +0,0 @@ -package main - -import ( - "github.com/kataras/iris/v12" - "github.com/kataras/iris/v12/context" -) - -func main() { - app := iris.New() - - // subdomains works with all available routers, like other features too. - - app.Get("/", func(ctx iris.Context) { - ctx.BeginTransaction(func(t *context.Transaction) { - // OPTIONAl STEP: , if true then the next transictions will not be executed if this transiction fails - // t.SetScope(context.RequestTransactionScope) - - // OPTIONAL STEP: - // create a new custom type of error here to keep track of the status code and reason message - err := context.NewTransactionErrResult() - - // we should use t.Context if we want to rollback on any errors lives inside this function clojure. - t.Context().Text("Blablabla this should not be sent to the client because we will fill the err with a message and status") - - // virtualize a fake error here, for the sake of the example - fail := true - if fail { - err.StatusCode = iris.StatusInternalServerError - // NOTE: if empty reason then the default or the custom http error will be fired (like ctx.FireStatusCode) - err.Reason = "Error: Virtual failure!!" - } - - // OPTIONAl STEP: - // but useful if we want to post back an error message to the client if the transaction failed. - // if the reason is empty then the transaction completed successfully, - // otherwise we rollback the whole response writer's body, - // headers and cookies, status code and everything lives inside this transaction - t.Complete(err) - }) - - ctx.BeginTransaction(func(t *context.Transaction) { - t.Context().HTML("

This will sent at all cases because it lives on different transaction and it doesn't fails

") - // * if we don't have any 'throw error' logic then no need of scope.Complete() - }) - - // OPTIONALLY, depends on the usage: - // at any case, what ever happens inside the context's transactions send this to the client - ctx.HTML("

Let's add a second html message to the response, " + - "if the transaction was failed and it was request scoped then this message would " + - "not been shown. But it has a transient scope(default) so, it is visible as expected!

") - }) - - app.Listen(":8080") -} diff --git a/context/context.go b/context/context.go index 992477dc..8117afa8 100644 --- a/context/context.go +++ b/context/context.go @@ -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. diff --git a/context/response_recorder.go b/context/response_recorder.go index d2162d97..ab19c7aa 100644 --- a/context/response_recorder.go +++ b/context/response_recorder.go @@ -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 diff --git a/context/transaction.go b/context/transaction.go deleted file mode 100644 index 823b1502..00000000 --- a/context/transaction.go +++ /dev/null @@ -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 -}) diff --git a/core/host/interrupt.go b/core/host/interrupt.go index cdd74072..07e717fe 100644 --- a/core/host/interrupt.go +++ b/core/host/interrupt.go @@ -7,15 +7,30 @@ import ( "syscall" ) -// RegisterOnInterrupt registers a global function to call when CTRL+C/CMD+C pressed or a unix kill command received. +// RegisterOnInterrupt registers a global function to call when CTRL+C pressed or a unix kill command received. func RegisterOnInterrupt(cb func()) { + // var cb func() + // switch v := callbackFuncOrFuncReturnsError.(type) { + // case func(): + // cb = v + // case func() error: + // cb = func() { v() } + // default: + // panic(fmt.Errorf("unknown type of RegisterOnInterrupt callback: expected func() or func() error but got: %T", v)) + // } + Interrupt.Register(cb) } // Interrupt watches the os.Signals for interruption signals // and fires the callbacks when those happens. // A call of its `FireNow` manually will fire and reset the registered interrupt handlers. -var Interrupt = new(interruptListener) +var Interrupt TnterruptListener = new(interruptListener) + +type TnterruptListener interface { + Register(cb func()) + FireNow() +} type interruptListener struct { mu sync.Mutex diff --git a/iris.go b/iris.go index 697dc562..20318c3f 100644 --- a/iris.go +++ b/iris.go @@ -588,7 +588,7 @@ func (app *Application) NewHost(srv *http.Server) *host.Supervisor { if !app.config.DisableInterruptHandler { // when CTRL/CMD+C pressed. shutdownTimeout := 10 * time.Second - host.RegisterOnInterrupt(host.ShutdownOnInterrupt(su, shutdownTimeout)) + RegisterOnInterrupt(host.ShutdownOnInterrupt(su, shutdownTimeout)) // app.logger.Debugf("Host: register server shutdown on interrupt(CTRL+C/CMD+C)") }