-
Notifications
You must be signed in to change notification settings - Fork 15
/
Copy patherrors.go
170 lines (149 loc) · 4.92 KB
/
errors.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
package abcmiddleware
import (
"errors"
"net/http"
"reflect"
chimiddleware "github.com/go-chi/chi/middleware"
"github.com/volatiletech/abcweb/v5/abcrender"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// These are the errors that can be used in your controllers and matched
// against in the Errors middleware to perform special actions.
var (
ErrUnauthorized = errors.New("not authorized")
ErrForbidden = errors.New("access is forbidden")
)
// ErrorContainer holds all of the relevant variables for a users custom error
type ErrorContainer struct {
// The error that will be returned by the controller
Err error
// Optional override layout string
ErrLayout string
// The template file top render (e.g "errors/500")
Template string
// HTTP status code to respond with
Code int
// A custom handler to perform additional operations on this error
Handler ErrorHandler
}
// ErrorManager helps manage errors at the application level
type ErrorManager struct {
render abcrender.Renderer
errors []ErrorContainer
errLayout string
}
// NewErrorManager creates an error manager that can be used to
// create an error handler to wrap your controllers with
func NewErrorManager(render abcrender.Renderer, errorLayout string) *ErrorManager {
return &ErrorManager{
render: render,
errors: []ErrorContainer{},
errLayout: errorLayout,
}
}
// NewError creates a new ErrorContainer that can be added to an errorManager.
// If you provide a handler here (instead of nil) then the Errors middleware
// will use your handler opposed to taking the default route of logging
// and rendering. You must handle logging and rendering yourself.
func NewError(err error, code int, errorLayout, template string, handler ErrorHandler) ErrorContainer {
if err == nil {
panic("cannot supply nil error")
}
// template and code must be set if handler is nil
if handler == nil && (len(template) == 0 || code == 0) {
panic("template and code must be set if handler is nil")
}
return ErrorContainer{
Err: err,
Code: code,
ErrLayout: errorLayout,
Template: template,
Handler: handler,
}
}
// Remove a ErrorContainer from the error manager
func (m *ErrorManager) Remove(e ErrorContainer) {
for i, v := range m.errors {
if reflect.DeepEqual(v, e) {
m.errors = append(m.errors[:i], m.errors[i+1:]...)
}
}
}
// Add a new ErrorContainer to the error manager
func (m *ErrorManager) Add(e ErrorContainer) {
m.errors = append(m.errors, e)
}
// AppHandler is the function signature for controllers that return errors.
type AppHandler func(w http.ResponseWriter, r *http.Request) error
// ErrorHandler is the function signature for user supplied error handlers.
type ErrorHandler func(w http.ResponseWriter, r *http.Request, e ErrorContainer, render abcrender.Renderer) error
// Errors is a middleware to handle controller errors and error page rendering.
// The benefit of using this middleware opposed to logging and rendering
// errors directly in your controller is that it's all centralized to one
// location which simplifies adding notifiers (like slack and email).
// It also reduces a lot of controller boilerplate.
func (m *ErrorManager) Errors(ctrl AppHandler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
err := ctrl(w, r)
if err == nil {
return
}
var container ErrorContainer
var layout string
var template string
var code int
found := false
for _, e := range m.errors {
if errors.Is(err, e.Err) {
container = e
found = true
break
}
}
if !found { // no error containers/handlers found, default path
code = http.StatusInternalServerError
layout = m.errLayout
template = "errors/500"
} else if container.Handler != nil { // container and handler are set
err := container.Handler(w, r, container, m.render)
if err != nil {
panic(err)
}
// Users handlers should handle EVERYTHING, so return here
// once the handler has been called successfully above.
return
} else { // container is set and handler is nil
code = container.Code
layout = container.ErrLayout
template = container.Template
}
// Get the Request ID scoped logger
log := Logger(r)
fields := []zapcore.Field{
zap.String("method", r.Method),
zap.String("uri", r.RequestURI),
zap.Bool("tls", r.TLS != nil),
zap.String("protocol", r.Proto),
zap.String("host", r.Host),
zap.String("remote_addr", r.RemoteAddr),
zap.Error(err),
}
// log with the request_id scoped logger
switch code {
case http.StatusInternalServerError:
log.Error("request error", fields...)
requestID := chimiddleware.GetReqID(r.Context())
err = m.render.HTMLWithLayout(w, code, template, requestID, layout)
if err != nil {
panic(err)
}
default: // warn does not log stacktrace in prod, but error and above does
log.Warn("request failed", fields...)
err = m.render.HTMLWithLayout(w, code, template, nil, layout)
if err != nil {
panic(err)
}
}
}
}