mirror of
https://github.com/kataras/iris.git
synced 2025-02-02 15:30:36 +01:00
context transactions removed and make Context.Domain customizable as requested
This commit is contained in:
parent
c6911851f1
commit
d8af2a1e14
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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("<h1>This will sent at all cases because it lives on different transaction and it doesn't fails</h1>")
|
||||
// * 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("<h1>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!</h1>")
|
||||
})
|
||||
|
||||
app.Listen(":8080")
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
})
|
|
@ -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
|
||||
|
|
2
iris.go
2
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)")
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user