diff --git a/HISTORY.md b/HISTORY.md
index 37dab6ee..02af4036 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -609,6 +609,7 @@ New Package-level Variables:
New Context Methods:
+- `Context.IsRecovered()` reports whether the current request was recovered from the [recover middleware](https://github.com/kataras/iris/tree/master/middleware/recover). Also the `iris.IsErrPrivate` function and `iris.ErrPrivate` interface have been introduced.
- `Context.RecordBody()` same as the Application's `DisableBodyConsumptionOnUnmarshal` configuration field but registers per chain of handlers. It makes the request body readable more than once.
- `Context.IsRecordingBody() bool` reports whether the request body can be readen multiple times.
- `Context.ReadHeaders(ptr interface{}) error` binds request headers to "ptr". [Example](https://github.com/kataras/iris/blob/master/_examples/request-body/read-headers/main.go).
diff --git a/_examples/logging/request-logger/accesslog-broker/main.go b/_examples/logging/request-logger/accesslog-broker/main.go
index a2f0b267..3b2386bf 100644
--- a/_examples/logging/request-logger/accesslog-broker/main.go
+++ b/_examples/logging/request-logger/accesslog-broker/main.go
@@ -3,6 +3,7 @@ package main
import (
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/accesslog"
+ "github.com/kataras/iris/v12/middleware/recover"
)
func main() {
@@ -16,13 +17,18 @@ func main() {
ac := accesslog.File("./access.log")
ac.TimeFormat = "2006-01-02 15:04:05"
+ // ac.KeepMultiLineError = false // set to false to print errors as one line.
// Optionally run logging after response has sent:
// ac.Async = true
broker := ac.Broker() // <- IMPORTANT
app := iris.New()
app.UseRouter(ac.Handler)
+ app.UseRouter(recover.New())
+ app.OnErrorCode(iris.StatusNotFound, notFoundHandler)
+
+ app.Get("/panic", testPanic)
app.Get("/", indexHandler)
app.Get("/profile/{username}", profileHandler)
app.Post("/read_body", readBodyHandler)
@@ -36,6 +42,22 @@ func main() {
app.Listen(":8080")
}
+func notFoundHandler(ctx iris.Context) {
+ ctx.Application().Logger().Infof("Not Found Handler for: %s", ctx.Path())
+
+ suggestPaths := ctx.FindClosest(3)
+ if len(suggestPaths) == 0 {
+ ctx.WriteString("The page you're looking does not exist.")
+ return
+ }
+
+ ctx.HTML("Did you mean?
")
+ for _, s := range suggestPaths {
+ ctx.HTML(`- %s
`, s, s)
+ }
+ ctx.HTML("
")
+}
+
func indexHandler(ctx iris.Context) {
ctx.HTML("Index
")
}
@@ -55,6 +77,10 @@ func readBodyHandler(ctx iris.Context) {
ctx.JSON(iris.Map{"message": "OK", "data": request})
}
+func testPanic(ctx iris.Context) {
+ panic("PANIC HERE")
+}
+
func logsHandler(b *accesslog.Broker) iris.Handler {
return func(ctx iris.Context) {
// accesslog.Skip(ctx) // or inline skip.
diff --git a/aliases.go b/aliases.go
index e021e021..e9d50a22 100644
--- a/aliases.go
+++ b/aliases.go
@@ -162,6 +162,10 @@ type (
// Locale describes the i18n locale.
// An alias for the `context.Locale`.
Locale = context.Locale
+ // ErrPrivate if provided then the error saved in context
+ // should NOT be visible to the client no matter what.
+ // An alias for the `context.ErrPrivate`.
+ ErrPrivate = context.ErrPrivate
)
// Constants for input argument at `router.RouteRegisterRule`.
@@ -458,6 +462,8 @@ var (
// on post data, versioning feature and others.
// An alias of `context.ErrNotFound`.
ErrNotFound = context.ErrNotFound
+ // IsErrPrivate reports whether the given "err" is a private one.
+ IsErrPrivate = context.IsErrPrivate
// NewProblem returns a new Problem.
// Head over to the `Problem` type godoc for more.
//
diff --git a/context/context.go b/context/context.go
index 7e66c9c6..8d5b65c4 100644
--- a/context/context.go
+++ b/context/context.go
@@ -706,12 +706,22 @@ func (ctx *Context) StopWithText(statusCode int, format string, args ...interfac
//
// If the status code is a failure one then
// it will also fire the specified error code handler.
+//
+// If the given "err" is private then the
+// status code's text is rendered instead (unless a registered error handler overrides it).
func (ctx *Context) StopWithError(statusCode int, err error) {
if err == nil {
return
}
ctx.SetErr(err)
+ if IsErrPrivate(err) {
+ // error is private, we can't render it, instead .
+ // let the error handler render the code text.
+ ctx.StopWithStatus(statusCode)
+ return
+ }
+
ctx.StopWithText(statusCode, err.Error())
}
@@ -4782,7 +4792,32 @@ func (ctx *Context) IsRecording() (*ResponseRecorder, bool) {
// ErrPanicRecovery may be returned from `Context` actions of a `Handler`
// which recovers from a manual panic.
-// var ErrPanicRecovery = errors.New("recovery from panic")
+type ErrPanicRecovery struct {
+ ErrPrivate
+ Cause interface{}
+ Stacktrace string
+}
+
+// Error implements the Go standard error type.
+func (e ErrPanicRecovery) Error() string {
+ return fmt.Sprintf("%v\n%s", e.Cause, e.Stacktrace)
+}
+
+// ErrPrivate if provided then the error saved in context
+// should NOT be visible to the client no matter what.
+type ErrPrivate interface {
+ IrisPrivateError()
+}
+
+// IsErrPrivate reports whether the given "err" is a private one.
+func IsErrPrivate(err error) bool {
+ if err == nil {
+ return false
+ }
+
+ _, ok := err.(ErrPrivate)
+ return ok
+}
// ErrTransactionInterrupt can be used to manually force-complete a Context's transaction
// and log(warn) the wrapped error's message.
@@ -5052,6 +5087,21 @@ func (ctx *Context) GetErr() error {
return nil
}
+// IsRecovered reports whether this handler has been recovered
+// by the Iris recover middleware.
+func (ctx *Context) IsRecovered() (ErrPanicRecovery, bool) {
+ if ctx.GetStatusCode() == 500 {
+ // Panic error from recovery middleware is private.
+ if err := ctx.GetErr(); err != nil {
+ if panicErr, ok := err.(ErrPanicRecovery); ok {
+ return panicErr, true
+ }
+ }
+ }
+
+ return ErrPanicRecovery{}, false
+}
+
const idContextKey = "iris.context.id"
// SetID sets an ID, any value, to the Request Context.
diff --git a/core/router/handler.go b/core/router/handler.go
index bad9b29e..08195abf 100644
--- a/core/router/handler.go
+++ b/core/router/handler.go
@@ -130,10 +130,14 @@ type RoutesProvider interface { // api builder
func defaultErrorHandler(ctx *context.Context) {
if err := ctx.GetErr(); err != nil {
- ctx.WriteString(err.Error())
- } else {
- ctx.WriteString(context.StatusText(ctx.GetStatusCode()))
+ if !context.IsErrPrivate(err) {
+ ctx.WriteString(err.Error())
+ return
+ }
}
+
+ ctx.WriteString(context.StatusText(ctx.GetStatusCode()))
+
}
func (h *routerHandler) Build(provider RoutesProvider) error {
diff --git a/middleware/accesslog/accesslog.go b/middleware/accesslog/accesslog.go
index 47a90370..026648ad 100644
--- a/middleware/accesslog/accesslog.go
+++ b/middleware/accesslog/accesslog.go
@@ -6,6 +6,7 @@ import (
"io"
"net/http/httputil"
"os"
+ "strings"
"sync"
"time"
@@ -150,6 +151,10 @@ type AccessLog struct {
// Note that, if this is true then it uses a response recorder.
ResponseBody bool
+ // KeepMultiLineError displays the Context's error as it's.
+ // If set to false then it replaces all line characters with spaces.
+ KeepMultiLineError bool
+
// Map log fields with custom request values.
// See `AddFields` method.
FieldSetters []FieldSetter
@@ -168,11 +173,12 @@ type AccessLog struct {
// Example: https://github.com/kataras/iris/tree/master/_examples/logging/request-logger/accesslog
func New(w io.Writer) *AccessLog {
ac := &AccessLog{
- BytesReceived: true,
- BytesSent: true,
- BodyMinify: true,
- RequestBody: true,
- ResponseBody: true,
+ BytesReceived: true,
+ BytesSent: true,
+ BodyMinify: true,
+ RequestBody: true,
+ ResponseBody: true,
+ KeepMultiLineError: true,
}
if w == nil {
@@ -367,7 +373,28 @@ func (ac *AccessLog) Handler(ctx *context.Context) {
// So we initialize them whenever, and if, asked.
// Proceed to the handlers chain.
+ currentIndex := ctx.HandlerIndex(-1)
ctx.Next()
+ if context.StatusCodeNotSuccessful(ctx.GetStatusCode()) {
+ _, wasRecovered := ctx.IsRecovered()
+ // The ctx.HandlerName is still accesslog because
+ // on end of router filters the router resets
+ // the handler index, same for errors.
+ // So, as a special case, if it's a failure status code
+ // call FireErorrCode manually instead of wait
+ // to be called on EndRequest (which is, correctly, called on end of everything
+ // so we don't have chance to record its body by default).
+ //
+ // Note: this however will call the error handler twice
+ // if the end-developer registered that using `UseError` instead of `UseRouter`,
+ // there is a way to fix that too: by checking the handler index diff:
+ if currentIndex == ctx.HandlerIndex(-1) || wasRecovered {
+ // if handler index before and after ctx.Next
+ // is the same, then it means we are in `UseRouter`
+ // and on error handler.
+ ctx.Application().FireErrorCode(ctx)
+ }
+ }
if shouldSkip(ctx) { // normal flow, we can get the context by executing the handler first.
return
@@ -398,12 +425,12 @@ func (ac *AccessLog) after(ctx *context.Context, lat time.Duration, method, path
// If there is an error here
// we may need to NOT read the body for security reasons, e.g.
// unauthorized user tries to send a malicious body.
- requestBody = fmt.Sprintf("error(%s)", ctxErr.Error())
+ requestBody = ac.getErrorText(ctxErr)
} else {
requestData, err := ctx.GetBody()
requestBodyLength := len(requestData)
if err != nil && ac.RequestBody {
- requestBody = fmt.Sprintf("error(%s)", err)
+ requestBody = ac.getErrorText(err)
} else if requestBodyLength > 0 {
if ac.RequestBody {
if ac.BodyMinify {
@@ -554,3 +581,15 @@ func (ac *AccessLog) Print(ctx *context.Context, latency time.Duration, timeForm
return
}
+
+var lineBreaksReplacer = strings.NewReplacer("\n\r", " ", "\n", " ")
+
+func (ac *AccessLog) getErrorText(err error) string { // caller checks for nil.
+ text := fmt.Sprintf("error(%s)", err.Error())
+
+ if !ac.KeepMultiLineError {
+ return lineBreaksReplacer.Replace(text)
+ }
+
+ return text
+}
diff --git a/middleware/recover/recover.go b/middleware/recover/recover.go
index 0f0a614c..1166e409 100644
--- a/middleware/recover/recover.go
+++ b/middleware/recover/recover.go
@@ -41,12 +41,13 @@ func New() context.Handler {
// when stack finishes
logMessage := fmt.Sprintf("Recovered from a route's Handler('%s')\n", ctx.HandlerName())
- logMessage += fmt.Sprintf("At Request: %s\n", getRequestLogs(ctx))
- logMessage += fmt.Sprintf("Trace: %s\n", err)
- logMessage += fmt.Sprintf("\n%s", stacktrace)
+ logMessage += fmt.Sprint(getRequestLogs(ctx))
+ logMessage += fmt.Sprintf("%s\n", err)
+ logMessage += fmt.Sprintf("%s\n", stacktrace)
ctx.Application().Logger().Warn(logMessage)
- ctx.StopWithStatus(500)
+ // see accesslog.isPanic too.
+ ctx.StopWithPlainError(500, context.ErrPanicRecovery{Cause: err, Stacktrace: stacktrace})
}
}()