aah Error Handling
aah is versatile and seamless about handling errors. It is made for modern Web and API applications. It propagates all types of errors- Validation, Authentication, Authorization, Route Not Found and Content-Negotiation.
The propagation order is listed below -
- Controller level error handler Since v0.10.0
- Centralized error handler Since v0.8.0
- Default error handler
Benefits of aah error handling:
- Handle errors in each controller locally
- Achieve application level error handling
- Send custom HTTP code with various response types after handling errors
- Filter appropriate errors and send notification emails, slacks, etc in response.
- Log error informations and send messages to external systems such as Kibana, Prometheus, Splunk, Sentry, Airbrake, etc.
Tip: Easy to handle errors at each controller level. If needed, it is also feasiable to propagate errors further after handling.
Table of Contents
- aah Error Struct and aah Errors
- Defining Controller Level Error Handler
- Registering Centralized Error Handler
- Default Error Handler
- Reply().Error(err)
- Sample: Easy to read error stacktrace
aah Error Struct and aah Errors
Let us take a look at aah error object structure and aah errors.
aah.Error
Struct
// Error structure is used to represent the error information in aah framework.
type Error struct {
Reason error `json:"-" xml:"-"`
Code int `json:"code,omitempty" xml:"code,omitempty"`
Message string `json:"message,omitempty" xml:"message,omitempty"`
Data interface{} `json:"data,omitempty" xml:"data,omitempty"`
}
aah errors
For framework generated errors, field Reason
holds an appropriate text from any one of these common errors.
var (
ErrPanicRecovery = errors.New("aah: panic recovery")
ErrDomainNotFound = errors.New("aah: domain not found")
ErrRouteNotFound = errors.New("aah: route not found")
ErrStaticFileNotFound = errors.New("aah: static file not found")
ErrControllerOrActionNotFound = errors.New("aah: controller or action not found")
ErrInvalidRequestParameter = errors.New("aah: invalid request parameter")
ErrContentTypeNotAccepted = errors.New("aah: content type not accepted")
ErrContentTypeNotOffered = errors.New("aah: content type not offered")
ErrHTTPMethodNotAllowed = errors.New("aah: http method not allowed")
ErrNotAuthenticated = errors.New("aah: not authenticated")
ErrAccessDenied = errors.New("aah: access denied")
ErrAuthenticationFailed = errors.New("aah: authentication failed")
ErrAuthorizationFailed = errors.New("aah: authorization failed")
ErrSessionAuthenticationInfo = errors.New("aah: session authentication info")
ErrUnableToGetPrincipal = errors.New("aah: unable to get principal")
ErrGeneric = errors.New("aah: generic error")
ErrValidation = errors.New("aah: validation error")
ErrRenderResponse = errors.New("aah: render response error")
ErrWriteResponse = errors.New("aah: write response error")
)
Defining Controller Level Error Handler
Controller level error handler can be implemented using interface aah.ErrorHandler
on controller. aah propagates all controller specific errors to this handler.
// ErrorHandler is an interface to implement controller level error handling
type ErrorHandler interface {
// HandleError method is to handle controller specific errors
//
// - Returns `true` if one or more errors are handled. aah just writes the reply on the wire.
//
// - Return `false` if one or more errors could not be handled. aah propagates the error(s)
// further onto centralized error handler. If not handled, then finally default
// error handler takes control.
HandleError(err *Error) bool
}
Registering Centralized Error Handler
Implement error func aah.ErrorHandlerFunc
in an appropriate package and register via init.go
file.
// ErrorHandlerFunc is a function type. It is used to define a centralized error handler
// for an application.
//
// - Returns `true` when one or more errors are handled. aah just writes the reply on the wire.
//
// - Returns `false' when one or more errors could not be handled. aah propagates the error(s)
// to default error handler.
type ErrorHandlerFunc func(ctx *Context, err *Error) bool
All application errors can be handled via this function- handle by Request Host
, Error Code
, etc; then reply appropriate error(s).
Note: If it is application panic
, then field aah.Error.Data
holds the recovered value from panic.
For Illustration Purposes
package util
// Implement error handler with `aah.ErrorHandlerFunc` func type.
// This is just an idea. Creativity can very well play here :)
func AppErrorHandler(ctx *aah.Context, err *aah.Error) bool {
switch ctx.Req.Host {
case "mydomain.com":
switch err.Code {
case 400:
// handle bad request
case 401:
// handle unauthenticated request
case 403:
// handle permission issues
case 500:
// handle internal server issues
//...
}
case "subdomain.mydomain.com":
switch err.Code {
case 400:
// handle bad request
case 401:
// handle unauthenticated request
case 403:
// handle permission issues
case 500:
// handle internal server issues
//...
}
}
return true
}
// Register App Error Handler at `init.go` file
func init() {
aah.App().SetErrorHandler(util.AppErrorHandler)
}
Default Error Handler
Default error handler is to handle all errors from the application and writes the reply on the wire based on Content-Type
. If reply Content-Type is not set, then Content-Type is derived from header Accept
; otherwise, it falls back to config value render.default
from aah.conf. The default error handler is capable of writing response in JSON
, XML
, HTML
and Plain Text
.
Web application has special flow for HTML error page rendering. It generally works by naming the error view file, which is the same as HTTP status code.
The default error handler looks for the error view file views/errors/<http-status-code>.<extension>
.
- If found, then it renders that file.
- See
views/errors/500.html
andviews/errors/404.html
for examples
- See
- If not found, then it uses framework default HTML error template.
Since v0.8.0 aah new
command generates the web application with two error files (500.html, 404.html) under views/errors
.
Reply().Error(err)
Reply().Error()
reply gets processed by Controller level, Centralized Error Handler then the reply is written on the wire.
Tip: It is highly recommended to use method Reply().Error()
for all application error responses. So that aah user could have extensible workflow of dealing application errors.
Sample: Easy to read error stacktrace
2018-11-23 23:41:55.255 ERROR THUMBAI admin STACKTRACE:
This is test panic message
goroutine 58 [running]:
FILE FUNCTION LINE NO
-------------------------------------------------------------------------------------------------------------------------------
/usr/local/Cellar/go/1.11.1/libexec/src/runtime/panic.go panic(...) #513
/Users/jeeva/scm/thumbai/app/controllers/admin/proxy_controller.go admin.(*ProxyController).List() #39
/usr/local/Cellar/go/1.11.1/libexec/src/reflect/value.go reflect.Value.call(...) #447
/usr/local/Cellar/go/1.11.1/libexec/src/reflect/value.go reflect.Value.Call(...) #308
/Users/jeeva/go/pkg/mod/aahframe.work@v0.12.0/middleware.go aahframe.work.ActionMiddleware(...) #194
/Users/jeeva/go/pkg/mod/aahframe.work@v0.12.0/middleware.go aahframe.work.(*Middleware).Next(...) #78
/Users/jeeva/go/pkg/mod/aahframe.work@v0.12.0/security.go aahframe.work.AuthcAuthzMiddleware(...) #72
/Users/jeeva/go/pkg/mod/aahframe.work@v0.12.0/middleware.go aahframe.work.(*Middleware).Next(...) #78
/Users/jeeva/go/pkg/mod/aahframe.work@v0.12.0/security.go aahframe.work.AntiCSRFMiddleware(...) #318
/Users/jeeva/go/pkg/mod/aahframe.work@v0.12.0/middleware.go aahframe.work.(*Middleware).Next(...) #78
/Users/jeeva/go/pkg/mod/aahframe.work@v0.12.0/bind.go aahframe.work.BindMiddleware(...) #95
/Users/jeeva/go/pkg/mod/aahframe.work@v0.12.0/middleware.go aahframe.work.(*Middleware).Next(...) #78
/Users/jeeva/go/pkg/mod/aahframe.work@v0.12.0/router.go aahframe.work.RouteMiddleware(...) #29
/Users/jeeva/go/pkg/mod/aahframe.work@v0.12.0/middleware.go aahframe.work.(*Middleware).Next(...) #78
/Users/jeeva/go/pkg/mod/aahframe.work@v0.12.0/http_engine.go aahframe.work.(*HTTPEngine).Handle(...) #113
/Users/jeeva/go/pkg/mod/aahframe.work@v0.12.0/aah.go aahframe.work.(*Application).ServeHTTP(...) #769
/usr/local/Cellar/go/1.11.1/libexec/src/net/http/server.go http.serverHandler.ServeHTTP(...) #2741
/usr/local/Cellar/go/1.11.1/libexec/src/net/http/server.go http.(*conn).serve(...) #1847
/usr/local/Cellar/go/1.11.1/libexec/src/net/http/server.go http.(*Server).Serve #2851
goroutine 5 [syscall]:
FILE FUNCTION LINE NO
--------------------------------------------------------------------------------------------------
/usr/local/Cellar/go/1.11.1/libexec/src/runtime/sigqueue.go signal.signal_recv() #139
/usr/local/Cellar/go/1.11.1/libexec/src/os/signal/signal_unix.go signal.loop() #23
/usr/local/Cellar/go/1.11.1/libexec/src/os/signal/signal_unix.go signal.init.0 #29
goroutine 135 [IO wait]:
FILE FUNCTION LINE NO
---------------------------------------------------------------------------------------------------------------------------------------------
/usr/local/Cellar/go/1.11.1/libexec/src/runtime/netpoll.go poll.runtime_pollWait(...) #173
/usr/local/Cellar/go/1.11.1/libexec/src/internal/poll/fd_poll_runtime.go poll.(*pollDesc).wait(...) #85
/usr/local/Cellar/go/1.11.1/libexec/src/internal/poll/fd_poll_runtime.go poll.(*pollDesc).waitRead(...) #90
/usr/local/Cellar/go/1.11.1/libexec/src/internal/poll/fd_unix.go poll.(*FD).Accept(...) #384
/usr/local/Cellar/go/1.11.1/libexec/src/net/fd_unix.go net.(*netFD).accept(...) #238
/usr/local/Cellar/go/1.11.1/libexec/src/net/tcpsock_posix.go net.(*TCPListener).accept(...) #139
/usr/local/Cellar/go/1.11.1/libexec/src/net/tcpsock.go net.(*TCPListener).AcceptTCP(...) #247
/usr/local/Cellar/go/1.11.1/libexec/src/net/http/server.go http.tcpKeepAliveListener.Accept(...) #3232
/usr/local/Cellar/go/1.11.1/libexec/src/crypto/tls/tls.go tls.(*listener).Accept(...) #52
/usr/local/Cellar/go/1.11.1/libexec/src/net/http/server.go http.(*Server).Serve(...) #2826
/usr/local/Cellar/go/1.11.1/libexec/src/net/http/server.go http.(*Server).ServeTLS(...) #2891
/usr/local/Cellar/go/1.11.1/libexec/src/net/http/server.go http.(*Server).ListenAndServeTLS(...) #3048
/Users/jeeva/go/pkg/mod/aahframe.work@v0.12.0/server.go aahframe.work.(*Application).startHTTPS() #236
/Users/jeeva/go/pkg/mod/aahframe.work@v0.12.0/server.go aahframe.work.(*Application).Start() #126
/Users/jeeva/go/pkg/mod/aahframe.work@v0.12.0/commands.go created by aahframe.work.(*Application).cliCmdRun.func1 #144
And runtime.debug.strip_src_base
set to true
2018-11-23 23:53:14.160 ERROR THUMBAI admin STACKTRACE:
This is test panic message
goroutine 200 [running]:
FILE FUNCTION LINE NO
----------------------------------------------------------------------------------------------------------
.../runtime/panic.go panic(...) #513
.../app/controllers/admin/proxy_controller.go admin.(*ProxyController).List() #39
.../reflect/value.go reflect.Value.call(...) #447
.../reflect/value.go reflect.Value.Call(...) #308
.../aahframe.work@v0.12.0/middleware.go aahframe.work.ActionMiddleware(...) #194
.../aahframe.work@v0.12.0/middleware.go aahframe.work.(*Middleware).Next(...) #78
.../aahframe.work@v0.12.0/security.go aahframe.work.AuthcAuthzMiddleware(...) #72
.../aahframe.work@v0.12.0/middleware.go aahframe.work.(*Middleware).Next(...) #78
.../aahframe.work@v0.12.0/security.go aahframe.work.AntiCSRFMiddleware(...) #318
.../aahframe.work@v0.12.0/middleware.go aahframe.work.(*Middleware).Next(...) #78
.../aahframe.work@v0.12.0/bind.go aahframe.work.BindMiddleware(...) #95
.../aahframe.work@v0.12.0/middleware.go aahframe.work.(*Middleware).Next(...) #78
.../aahframe.work@v0.12.0/router.go aahframe.work.RouteMiddleware(...) #29
.../aahframe.work@v0.12.0/middleware.go aahframe.work.(*Middleware).Next(...) #78
.../aahframe.work@v0.12.0/http_engine.go aahframe.work.(*HTTPEngine).Handle(...) #113
.../aahframe.work@v0.12.0/aah.go aahframe.work.(*Application).ServeHTTP(...) #788
.../net/http/server.go http.serverHandler.ServeHTTP(...) #2741
.../net/http/server.go http.(*conn).serve(...) #1847
.../net/http/server.go http.(*Server).Serve #2851
goroutine 5 [syscall]:
FILE FUNCTION LINE NO
--------------------------------------------------------------
.../runtime/sigqueue.go signal.signal_recv() #139
.../os/signal/signal_unix.go signal.loop() #23
.../os/signal/signal_unix.go signal.init.0 #29
goroutine 119 [IO wait]:
FILE FUNCTION LINE NO
----------------------------------------------------------------------------------------------------------
.../runtime/netpoll.go poll.runtime_pollWait(...) #173
.../internal/poll/fd_poll_runtime.go poll.(*pollDesc).wait(...) #85
.../internal/poll/fd_poll_runtime.go poll.(*pollDesc).waitRead(...) #90
.../internal/poll/fd_unix.go poll.(*FD).Accept(...) #384
.../net/fd_unix.go net.(*netFD).accept(...) #238
.../net/tcpsock_posix.go net.(*TCPListener).accept(...) #139
.../net/tcpsock.go net.(*TCPListener).AcceptTCP(...) #247
.../net/http/server.go http.tcpKeepAliveListener.Accept(...) #3232
.../crypto/tls/tls.go tls.(*listener).Accept(...) #52
.../net/http/server.go http.(*Server).Serve(...) #2826
.../net/http/server.go http.(*Server).ServeTLS(...) #2891
.../net/http/server.go http.(*Server).ListenAndServeTLS(...) #3048
.../aahframe.work@v0.12.0/server.go aahframe.work.(*Application).startHTTPS() #236
.../aahframe.work@v0.12.0/server.go aahframe.work.(*Application).Start() #126
.../aahframe.work@v0.12.0/commands.go created by aahframe.work.(*Application).cliCmdRun.func1 #144