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 @@
-
+
@@ -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 = ` _____ _
|_ _| (_)