-
Notifications
You must be signed in to change notification settings - Fork 1
/
handler.go
293 lines (259 loc) · 9.8 KB
/
handler.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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
package httphandler
import (
"context"
"fmt"
"mime"
"net/http"
"strings"
"github.com/pkg/errors"
)
// Handler provides a HandleFunc function that can be used to return errors based on the client "Accept" header value.
type Handler struct {
options *Options
}
// New constructs a new Handler with the specified Options.
// To construct with default options use New(nil) or use the DefaultHandler.
func New(options *Options) *Handler {
if options == nil {
return &Handler{options: defaultOptions()}
}
if options.LogFunc == nil {
options.LogFunc = defaultLogFunc()
}
if options.Encoders == nil {
options.Encoders = defaultEncoders()
} else {
_ = options.SetEncoders(options.Encoders)
}
if options.FallbackEncoderFunc == nil {
options.FallbackEncoderFunc = defaultFallbackEncoder()
}
if options.RequestUUIDFunc == nil {
options.RequestUUIDFunc = defaultRequestUUID()
}
if options.CustomPanicHandler == nil {
options.CustomPanicHandler = defaultCustomPanicHandler()
}
return &Handler{options: options}
}
// HandlerError represents the error that should be returned from the handler func in case of error.
type HandlerError struct {
// StatusCode is the http status code to send to the client.
// If not specified HandleFunc will use http.StatusInternalServerError.
StatusCode int
// PublicError is the error that will be visible to the client. Do not include sensitive information here.
// Remember that this type should implement the necessary marshal functions for the specified encoder,
// otherwise you might see unexpected results.
PublicError error
// InternalError is the error that will not be visible to the client. Remember that this type should
// implement the necessary marshal functions for the specified encoder, otherwise you might see unexpected results.
InternalError error
// ContentType specifies the Content-Type of this error. If not specified HandleFunc will use the clients Accept
// header. If specified the clients Accept header will be ignored.
ContentType string
}
// WireError represents the error that will be send "over the wire" to the client.
type WireError struct {
// StatusCode is the http status code that was sent to the client.
StatusCode int
// Error is the error message that should be send to the client.
Error error
// RequestUUID is the request uuid that should be send to the client.
RequestUUID string
}
// PanicHandler is the type for custom functions for handling panics.
type PanicHandler func(context.Context, *HandlerError)
// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(w http.ResponseWriter, r *http.Request) *HandlerError
// ServeHTTP mimics the http.Handler interface, with the addition of the *HandlerError.
type ServeHTTP interface {
ServeHTTP(http.ResponseWriter, *http.Request) *HandlerError
}
// HandleFunc wraps a handler with a HandlerError return value.
// In case the provided handler function returns an error, HandleFunc will construct a response based on the error and
// the Accept header of the client.
// If the HandlerError specifies a ContentType value the clients Accept header will be ignored.
// If the provided handler function returns no error no action will be taken, this means that the specified handler func
// is required to send the http headers, status code and body.
//
// Example:
// http.HandleFunc("/", HandleFunc(func(w http.ResponseWriter, r *http.Request) *HandlerError {
// return &HandlerError{
// StatusCode: http.StatusUnauthorized,
// PublicError: "you have no permission to view this site",
// InternalError: "client authentication failed",
// }
// })
func (h *Handler) HandleFunc(handler func(w http.ResponseWriter, r *http.Request) *HandlerError) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
h.callNextHandler(handler, w, r)
}
}
// Handle mimics a http.Handler with a HandlerError return value.
// See also HandleFunc.
func (h *Handler) Handle(handler ServeHTTP) http.Handler {
return &httpHandler{
handler: h,
serveHTTP: handler,
}
}
// SetLogFunc sets the log function that will be called in case of error.
func (h *Handler) SetLogFunc(logFunc LogFunc) error {
return h.options.SetLogFunc(logFunc)
}
// SetEncoders sets the Encoders to the specified map of content type and EncodeFunc.
// It will be used to lookup the encoder for the error content type.
func (h *Handler) SetEncoders(encoders map[string]EncodeFunc) error {
return h.options.SetEncoders(encoders)
}
// SetEncoder sets one specific encoder in the Encoders map.
func (h *Handler) SetEncoder(contentType string, encoder EncodeFunc) error {
return h.options.SetEncoder(contentType, encoder)
}
// SetFallbackEncoder sets the fallback encoder in case the error Content-Type does not exist in the
// Encoders map.
func (h *Handler) SetFallbackEncoder(contentType string, encoder EncodeFunc) error {
return h.options.SetFallbackEncoder(contentType, encoder)
}
// SetRequestUUIDFunc specifies the function that returns an request uuid. This request uuid will be send to the
// LogFunc in case of error.
// The request uuid is also available in the specified handler (in HandleFunc()) by using GetRequestUUID().
func (h *Handler) SetRequestUUIDFunc(requestUUIDFunc func() string) error {
return h.options.SetRequestUUIDFunc(requestUUIDFunc)
}
// SetCustomPanicHandler sets a custom function that is going to be called when a panic occurs.
func (h *Handler) SetCustomPanicHandler(f PanicHandler) {
h.options.SetCustomPanicHandler(f)
}
// callNextHandler calls the next specified handler func.
func (h *Handler) callNextHandler(handler HandlerFunc, w http.ResponseWriter, r *http.Request) {
safeWriter := newSafeResponseWriter(w)
requestUUID := h.options.RequestUUIDFunc()
requestWithContext := r.WithContext(context.WithValue(r.Context(), uuidKey, requestUUID))
err := safeHandlerCall(handler, safeWriter, requestWithContext, h.options.CustomPanicHandler)
if err == nil {
return
}
if err.StatusCode == 0 {
err.StatusCode = http.StatusInternalServerError
}
if err.PublicError == nil {
err.PublicError = errors.New("unknown error")
}
h.options.LogFunc(r,
errors.New("handler error"),
err.InternalError,
err.PublicError,
err.StatusCode,
requestUUID,
)
// we have written already
if safeWriter.Written() {
return
}
h.sendError(err, requestUUID, safeWriter, requestWithContext)
}
func safeHandlerCall(h HandlerFunc, w http.ResponseWriter, r *http.Request, ph PanicHandler) (err *HandlerError) {
defer func() {
e := recover()
if e == nil {
return
}
switch v := e.(type) {
case error:
err = &HandlerError{
InternalError: errors.Wrap(v, "panic"),
}
default:
err = &HandlerError{
InternalError: fmt.Errorf("panic: %v", v),
}
}
ph(r.Context(), err)
}()
err = h(w, r)
return err
}
func (h *Handler) sendError(err *HandlerError, requestUUID string, w http.ResponseWriter, r *http.Request) {
errorToSend := &WireError{
StatusCode: err.StatusCode,
Error: err.PublicError,
RequestUUID: requestUUID,
}
var f EncodeFunc
if err.ContentType == "" {
f, err.ContentType = getPreferredContentType(h.options, r)
} else {
err.ContentType = strings.ToLower(err.ContentType)
f = h.options.Encoders[err.ContentType]
}
if f == nil || err.ContentType == "" {
// use fallback
f, err.ContentType = h.options.FallbackEncoderFunc()
err.ContentType = strings.ToLower(err.ContentType)
}
w.Header().Set("Content-Type", err.ContentType)
w.WriteHeader(err.StatusCode)
if encodeErr := f(w, r, errorToSend); encodeErr != nil {
h.options.LogFunc(r,
errors.Wrapf(encodeErr, "unable to encode %q", err.ContentType),
err.InternalError,
err.PublicError,
err.StatusCode,
requestUUID,
)
}
}
type httpHandler struct {
handler *Handler
serveHTTP ServeHTTP
}
func (h *httpHandler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
h.handler.callNextHandler(h.serveHTTP.ServeHTTP, writer, request)
}
func getPreferredContentType(options *Options, r *http.Request) (enocder EncodeFunc, contentType string) {
// if the request has a Accept header use this header to determinate the output format
if accept := r.Header.Values("Accept"); len(accept) != 0 {
for _, s := range accept {
mediaType, _, err := mime.ParseMediaType(s)
if err != nil {
continue
}
ct := strings.ToLower(mediaType)
f, ok := options.Encoders[ct]
if ok {
return f, ct
}
}
}
return nil, ""
}
// DefaultHandler is the default instance that can be used out of the box.
// It uses the default settings.
var DefaultHandler = New(nil)
// HandleFunc wraps a handler with a HandlerError return value.
// In case the provided handler function returns an error, HandleFunc will construct a response based on the error and
// the Accept header of the client.
// If the HandlerError specifies a ContentType value the clients Accept header will be ignored.
// If the provided handler function returns no error no action will be taken, this means that the specified handler func
// is required to send the http headers, status code and body.
//
// Example:
// http.HandleFunc("/", HandleFunc(func(w http.ResponseWriter, r *http.Request) *HandlerError {
// return &HandlerError{
// StatusCode: http.StatusUnauthorized,
// PublicError: "you have no permission to view this site",
// InternalError: "client authentication failed",
// }
// })
func HandleFunc(handler func(w http.ResponseWriter, r *http.Request) *HandlerError) http.HandlerFunc {
return DefaultHandler.HandleFunc(handler)
}
// Handle mimics a http.Handler with a HandlerError return value.
// See also HandleFunc.
func Handle(handler ServeHTTP) http.Handler {
return DefaultHandler.Handle(handler)
}