From 88c98bb1e14b07c5dcac0a76a5977be51b6ff436 Mon Sep 17 00:00:00 2001 From: "Gerasimos (Makis) Maropoulos" Date: Sun, 18 Dec 2016 14:08:28 +0200 Subject: [PATCH] More on Transactions: Fallback on, unexpected, panics and able to send 'silent' error which stills reverts the changes but no output --- HISTORY.md | 3 ++ README.md | 4 +-- context.go | 75 ++++++++++++++++++++++++++++++++++++++----------- context_test.go | 64 +++++++++++++++++++++++++++++++++++++++++ iris.go | 2 +- 5 files changed, 129 insertions(+), 19 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index b9582fcc..3d2de1ef 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -2,6 +2,9 @@ **How to upgrade**: remove your `$GOPATH/src/github.com/kataras` folder, open your command-line and execute this command: `go get -u github.com/kataras/iris/iris`. +## 5.1.1 -> 5.1.3 +- **More on Transactions vol 3**: Recovery from any (unexpected error) panics inside `context.BeginTransaction` without loud, continue the execution as expected. Next version will have a little cleanup if I see that the transactions code is going very large or hard to understand the flow* + ## 5.1.1 -> 5.1.2 - **More on Transactions vol 2**: Added **iris.UseTransaction** and **iris.DoneTransaction** to register transactions as you register middleware(handlers). new named type **iris.TransactionFunc**, shortcut of `func(scope *iris.TransactionScope)`, that gives you a function which you can convert a transaction to a normal handler/middleware using its `.ToMiddleware()`, for more see the `test code inside context_test.go:TestTransactionsMiddleware`. diff --git a/README.md b/README.md index f1cd76d2..bdceb42e 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@
-Releases +Releases Examples @@ -920,7 +920,7 @@ I recommend writing your API tests using this new library, [httpexpect](https:// Versioning ------------ -Current: **v5.1.2** +Current: **v5.1.3** Stable: **[v4 LTS](https://github.com/kataras/iris/tree/4.0.0#versioning)** diff --git a/context.go b/context.go index 6d0f0ef2..90f7d59b 100644 --- a/context.go +++ b/context.go @@ -1122,6 +1122,23 @@ func (ctx *Context) MaxAge() int64 { return -1 } +// ErrFallback is just an empty error but it is recognised from the TransactionScope.Complete, +// it reverts its changes and continue as normal, no error will be shown to the user. +// +// Usually it is used on recovery from panics (inside .BeginTransaction) +// but users can use that also to by-pass the error's response of your custom transaction pipe. +type ErrFallback struct{} + +func (ne *ErrFallback) Error() string { + return "" +} + +// NewErrFallback returns a new error wihch contains an empty error, +// look .BeginTransaction and context_test.go:TestTransactionRecoveryFromPanic +func NewErrFallback() *ErrFallback { + return &ErrFallback{} +} + // ErrWithStatus custom error type which is useful // to send an error containing the http status code and a reason type ErrWithStatus struct { @@ -1131,6 +1148,11 @@ type ErrWithStatus struct { message string // if it's empty then the already registered custom(or default) http error will be fired. } +// Silent in case the user changed his/her mind and wants to silence this error +func (err *ErrWithStatus) Silent() error { + return NewErrFallback() +} + // Status sets the http status code of this error // if only status exists but no reason then // custom http error of this staus (if any) will be fired (context.EmitError) @@ -1215,7 +1237,12 @@ func (r *TransactionScope) Complete(err error) { ctx := r.Context statusCode := StatusInternalServerError // default http status code if not provided reason := err.Error() - + if _, ok := err.(*ErrFallback); ok { + // revert without any log or response. + r.isFailure = true + ctx.Response.Reset() + return + } if errWstatus, ok := err.(*ErrWithStatus); ok { if errWstatus.statusCode > 0 { // get the status code from the custom error type @@ -1303,11 +1330,17 @@ func (pipe TransactionFunc) ToMiddleware() HandlerFunc { } } +// non-detailed error log for transacton unexpected panic +var errTransactionInterrupted = errors.New("Transaction Interrupted, recovery from panic:\n%s") + // BeginTransaction starts a request scoped transaction. // Transactions have their own middleware ecosystem also, look iris.go:UseTransaction. // // See https://github.com/iris-contrib/examples/tree/master/transactions for more -func (ctx *Context) BeginTransaction(pipe TransactionFunc) { +func (ctx *Context) BeginTransaction(pipe func(scope *TransactionScope)) { + // SILLY NOTE: use of manual pipe type in order of TransactionFunc + // in order to help editors complete the sentence here... + // do NOT begin a transaction when the previous transaction has been failed // and it was requested scoped or SkipTransactions called manually. if ctx.TransactionsSkipped() { @@ -1318,24 +1351,34 @@ func (ctx *Context) BeginTransaction(pipe TransactionFunc) { tempCtx := *ctx // get a transaction scope from the pool by passing the temp context/ scope := acquireTransactionScope(&tempCtx) + defer func() { + if err := recover(); err != nil { + if ctx.framework.Config.IsDevelopment { + ctx.Log(errTransactionInterrupted.Format(err).Error()) + } + // complete (again or not , doesn't matters) the scope without loud + scope.Complete(NewErrFallback()) + // we continue as normal, no need to return here* + } + // if the transaction completed with an error then the transaction itself reverts the changes + // and replaces the context's response with an error. + // if the transaction completed successfully then we need to pass the temp's context's response to this context. + // so we must copy back its context at all cases, no matter the result of the transaction. + *ctx = *scope.Context + + // if the scope had lifetime of the whole request and it completed with an error(failure) + // then we do not continue to the next transactions. + if scope.isRequestScoped && scope.isFailure { + ctx.SkipTransactions() + } + + // finally, release and put the transaction scope back to the pool. + releaseTransactionScope(scope) + }() // run the worker with its context inside this scope. pipe(scope) - // if the transaction completed with an error then the transaction itself reverts the changes - // and replaces the context's response with an error. - // if the transaction completed successfully then we need to pass the temp's context's response to this context. - // so we must copy back its context at all cases, no matter the result of the transaction. - *ctx = *scope.Context - - // if the scope had lifetime of the whole request and it completed with an error(failure) - // then we do not continue to the next transactions. - if scope.isRequestScoped && scope.isFailure { - ctx.SkipTransactions() - } - - // finally, release and put the transaction scope back to the pool. - releaseTransactionScope(scope) } // Log logs to the iris defined logger diff --git a/context_test.go b/context_test.go index 59b33de5..4d642901 100644 --- a/context_test.go +++ b/context_test.go @@ -972,5 +972,69 @@ func TestTransactionsMiddleware(t *testing.T) { ContentType("text/html", api.Config.Charset). Body(). Equal(expectedResponse) +} + +func TestTransactionFailureCompletionButSilently(t *testing.T) { + iris.ResetDefault() + expectedBody := "I don't care for any unexpected panics, this response should be sent." + + iris.Get("/panic_silent", func(ctx *iris.Context) { + ctx.BeginTransaction(func(scope *iris.TransactionScope) { + scope.Context.Write("blablabla this should not be shown because of 'unexpected' panic.") + panic("OMG, UNEXPECTED ERROR BECAUSE YOU ARE NOT A DISCIPLINED PROGRAMMER, BUT IRIS HAS YOU COVERED!") + }) + + ctx.WriteString(expectedBody) + }) + + iris.Get("/expected_error_but_silent_instead_of_send_the_reason", func(ctx *iris.Context) { + ctx.BeginTransaction(func(scope *iris.TransactionScope) { + scope.Context.Write("this will not be sent.") + // complete with a failure ( so revert the changes) but do it silently. + scope.Complete(iris.NewErrFallback()) + }) + + ctx.WriteString(expectedBody) + }) + + iris.Get("/silly_way_expected_error_but_silent_instead_of_send_the_reason", func(ctx *iris.Context) { + ctx.BeginTransaction(func(scope *iris.TransactionScope) { + scope.Context.Write("this will not be sent.") + + // or if you know the error will be silent from the beggining: err := &iris.ErrFallback{} + err := iris.NewErrWithStatus() + + fail := true + + if fail { + err.Status(iris.StatusBadRequest).Reason("we dont know but it was expected error") + } + + // we change our mind we don't want to send the error to the user, so err.Silent to the .Complete + // complete with a failure ( so revert the changes) but do it silently. + scope.Complete(err.Silent()) + }) + + ctx.WriteString(expectedBody) + }) + + e := httptest.New(iris.Default, t) + + e.GET("/panic_silent").Expect(). + Status(iris.StatusOK). + Body(). + Equal(expectedBody) + + e.GET("/expected_error_but_silent_instead_of_send_the_reason"). + Expect(). + Status(iris.StatusOK). + Body(). + Equal(expectedBody) + + e.GET("/silly_way_expected_error_but_silent_instead_of_send_the_reason"). + Expect(). + Status(iris.StatusOK). + Body(). + Equal(expectedBody) } diff --git a/iris.go b/iris.go index 1b81ae3e..21ebbc01 100644 --- a/iris.go +++ b/iris.go @@ -80,7 +80,7 @@ const ( // IsLongTermSupport flag is true when the below version number is a long-term-support version IsLongTermSupport = false // Version is the current version number of the Iris web framework - Version = "5.1.2" + Version = "5.1.3" banner = ` _____ _ |_ _| (_)