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)")
}