Skip to content

Commit de4052b

Browse files
author
Raphaël Simon
committed
Merge pull request goadesign#372 from goadesign/error_handler_middleware
Introduce error handler middleware.
2 parents 136f2be + 8e7f0a1 commit de4052b

8 files changed

+184
-86
lines changed

error_test.go

-1
Original file line numberDiff line numberDiff line change
@@ -273,7 +273,6 @@ var _ = Describe("Merge", func() {
273273

274274
Context("with two nil errors", func() {
275275
It("returns a nil error", func() {
276-
Ω(mErr).Should(BeAssignableToTypeOf(&goa.Error{}))
277276
Ω(mErr).Should(BeNil())
278277
})
279278
})

goagen/gen_main/generator.go

+1
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ func main() {
239239
// Setup middleware
240240
service.Use(middleware.RequestID())
241241
service.Use(middleware.LogRequest(true))
242+
service.Use(middleware.ErrorHandler(false))
242243
service.Use(middleware.Recover())
243244
{{$api := .API}}
244245
{{range $name, $res := $api.Resources}}{{$name := goify $res.Name true}} // Mount "{{$res.Name}}" controller

middleware/error_handler.go

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package middleware
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/goadesign/goa"
7+
"golang.org/x/net/context"
8+
)
9+
10+
// ErrorHandler turns a Go error into an HTTP response. It should be placed in the middleware chain
11+
// below the logger middleware so the logger properly logs the HTTP response. ErrorHandler
12+
// understands instances of goa.Error and returns the status and response body embodied in them,
13+
// it turns other Go error types into a 500 internal error response.
14+
// If suppressInternal is true the details of internal errors is not included in HTTP responses.
15+
func ErrorHandler(suppressInternal bool) goa.Middleware {
16+
return func(h goa.Handler) goa.Handler {
17+
return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
18+
e := h(ctx, rw, req)
19+
if e == nil {
20+
return nil
21+
}
22+
23+
goa.LogInfo(ctx, "Default error handler", "err", e)
24+
status := 500
25+
var respBody interface{}
26+
if err, ok := e.(*goa.Error); ok {
27+
status = err.Status
28+
respBody = err
29+
rw.Header().Set("Content-Type", goa.ErrorMediaIdentifier)
30+
} else {
31+
respBody = e.Error()
32+
rw.Header().Set("Content-Type", "text/plain")
33+
}
34+
if status >= 500 && status < 600 {
35+
goa.LogError(ctx, e.Error())
36+
if suppressInternal {
37+
rw.Header().Set("Content-Type", goa.ErrorMediaIdentifier)
38+
respBody = goa.ErrInternal("internal error, detail suppressed")
39+
}
40+
}
41+
return goa.ContextResponse(ctx).Send(ctx, status, respBody)
42+
}
43+
}
44+
}

middleware/error_handler_test.go

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package middleware_test
2+
3+
import (
4+
"bytes"
5+
"errors"
6+
"fmt"
7+
"net/http"
8+
9+
"golang.org/x/net/context"
10+
11+
"github.com/goadesign/goa"
12+
"github.com/goadesign/goa/middleware"
13+
. "github.com/onsi/ginkgo"
14+
. "github.com/onsi/gomega"
15+
)
16+
17+
var _ = Describe("ErrorHandler", func() {
18+
var service *goa.Service
19+
var h goa.Handler
20+
var suppressInternal bool
21+
22+
var rw *testResponseWriter
23+
24+
BeforeEach(func() {
25+
service = nil
26+
h = nil
27+
suppressInternal = false
28+
rw = nil
29+
})
30+
31+
JustBeforeEach(func() {
32+
rw = newTestResponseWriter()
33+
eh := middleware.ErrorHandler(suppressInternal)(h)
34+
req, err := http.NewRequest("GET", "/foo", nil)
35+
Ω(err).ShouldNot(HaveOccurred())
36+
ctx := newContext(service, rw, req, nil)
37+
err = eh(ctx, rw, req)
38+
Ω(err).ShouldNot(HaveOccurred())
39+
})
40+
41+
Context("with a handler returning a Go error", func() {
42+
BeforeEach(func() {
43+
service = newService(nil)
44+
h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
45+
return errors.New("boom")
46+
}
47+
})
48+
49+
It("turns Go errors into HTTP 500 responses", func() {
50+
Ω(rw.Status).Should(Equal(500))
51+
Ω(rw.ParentHeader["Content-Type"]).Should(Equal([]string{"text/plain"}))
52+
Ω(string(rw.Body)).Should(Equal(`"boom"` + "\n"))
53+
})
54+
55+
Context("suppressing internal errors", func() {
56+
BeforeEach(func() {
57+
suppressInternal = true
58+
})
59+
60+
It("suppresses the error details", func() {
61+
var decoded goa.Error
62+
Ω(rw.Status).Should(Equal(500))
63+
Ω(rw.ParentHeader["Content-Type"]).Should(Equal([]string{goa.ErrorMediaIdentifier}))
64+
err := service.Decode(&decoded, bytes.NewBuffer(rw.Body), "application/json")
65+
Ω(err).ShouldNot(HaveOccurred())
66+
Ω(fmt.Sprintf("%v", decoded)).Should(Equal(fmt.Sprintf("%v", *goa.ErrInternal("internal error, detail suppressed"))))
67+
})
68+
69+
})
70+
})
71+
72+
Context("with a handler returning a goa error", func() {
73+
var gerr *goa.Error
74+
75+
BeforeEach(func() {
76+
service = newService(nil)
77+
gerr = &goa.Error{
78+
Status: 418,
79+
Detail: "teapot",
80+
Code: "code",
81+
MetaValues: map[string]interface{}{"foobar": 42},
82+
}
83+
h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
84+
return gerr
85+
}
86+
})
87+
88+
It("maps goa errors to HTTP responses", func() {
89+
var decoded goa.Error
90+
Ω(rw.Status).Should(Equal(gerr.Status))
91+
Ω(rw.ParentHeader["Content-Type"]).Should(Equal([]string{goa.ErrorMediaIdentifier}))
92+
err := service.Decode(&decoded, bytes.NewBuffer(rw.Body), "application/json")
93+
Ω(err).ShouldNot(HaveOccurred())
94+
Ω(fmt.Sprintf("%v", decoded)).Should(Equal(fmt.Sprintf("%v", *gerr)))
95+
})
96+
})
97+
})

middleware/gzip/middleware_test.go

+1-3
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,7 @@ var _ = Describe("Gzip", func() {
4545
req, err = http.NewRequest("POST", "/foo/bar", strings.NewReader(`{"payload":42}`))
4646
req.Header.Set("Accept-Encoding", "gzip")
4747
Ω(err).ShouldNot(HaveOccurred())
48-
rw = &TestResponseWriter{
49-
ParentHeader: http.Header{},
50-
}
48+
rw = &TestResponseWriter{ParentHeader: make(http.Header)}
5149

5250
ctx = goa.NewContext(nil, rw, req, nil)
5351
goa.ContextRequest(ctx).Payload = payload

middleware/middleware_test.go

+6
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
func newService(logger goa.Logger) *goa.Service {
1313
service := goa.New("test")
1414
service.Encoder(goa.NewJSONEncoder, "*/*")
15+
service.Decoder(goa.NewJSONDecoder, "*/*")
1516
service.UseLogger(logger)
1617
return service
1718
}
@@ -48,6 +49,11 @@ type testResponseWriter struct {
4849
Status int
4950
}
5051

52+
func newTestResponseWriter() *testResponseWriter {
53+
h := make(http.Header)
54+
return &testResponseWriter{ParentHeader: h}
55+
}
56+
5157
func (t *testResponseWriter) Header() http.Header {
5258
return t.ParentHeader
5359
}

service.go

+29-78
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,16 @@ import (
88
"net/url"
99
"os"
1010
"path/filepath"
11-
"strconv"
1211
"strings"
1312

1413
"golang.org/x/net/context"
1514
)
1615

16+
const (
17+
// ErrorMediaIdentifier is the media type identifier used for error responses.
18+
ErrorMediaIdentifier = "application/vnd.api.error+json"
19+
)
20+
1721
type (
1822
// Service is the data structure supporting goa services.
1923
// It provides methods for configuring a service and running it.
@@ -46,21 +50,18 @@ type (
4650
Context context.Context
4751
// Middleware chain
4852
Middleware []Middleware
49-
// Service-wide error handler
50-
ErrorHandler ErrorHandler
5153

52-
cancel context.CancelFunc
54+
cancel context.CancelFunc // Service context cancel signal trigger
5355
decoderPools map[string]*decoderPool // Registered decoders for the service
5456
encoderPools map[string]*encoderPool // Registered encoders for the service
5557
encodableContentTypes []string // List of contentTypes for response negotiation
5658
}
5759

5860
// Controller provides the common state and behavior for generated controllers.
5961
Controller struct {
60-
Name string // Controller resource name
61-
Context context.Context // Controller root context
62-
ErrorHandler ErrorHandler // Controller specific error handler if any
63-
Middleware []Middleware // Controller specific middleware if any
62+
Name string // Controller resource name
63+
Context context.Context // Controller root context
64+
Middleware []Middleware // Controller specific middleware if any
6465
}
6566

6667
// Handler defines the controller handler signatures.
@@ -85,10 +86,9 @@ func New(name string) *Service {
8586
ctx := UseLogger(context.Background(), NewStdLogger(stdlog))
8687
ctx, cancel := context.WithCancel(ctx)
8788
return &Service{
88-
Name: name,
89-
ErrorHandler: DefaultErrorHandler,
90-
Context: ctx,
91-
Mux: NewMux(),
89+
Name: name,
90+
Context: ctx,
91+
Mux: NewMux(),
9292

9393
cancel: cancel,
9494
decoderPools: map[string]*decoderPool{},
@@ -143,10 +143,9 @@ func (service *Service) ListenAndServeTLS(addr, certFile, keyFile string) error
143143
func (service *Service) NewController(resName string) *Controller {
144144
ctx := context.WithValue(service.Context, serviceKey, service)
145145
return &Controller{
146-
Name: resName,
147-
Middleware: service.Middleware,
148-
ErrorHandler: service.ErrorHandler,
149-
Context: context.WithValue(ctx, "ctrl", resName),
146+
Name: resName,
147+
Middleware: service.Middleware,
148+
Context: context.WithValue(ctx, "ctrl", resName),
150149
}
151150
}
152151

@@ -205,23 +204,6 @@ func (ctrl *Controller) Use(m Middleware) {
205204
ctrl.Middleware = append(ctrl.Middleware, m)
206205
}
207206

208-
// HandleError invokes the controller error handler or - if there isn't one - the service error
209-
// handler.
210-
func (ctrl *Controller) HandleError(ctx context.Context, rw http.ResponseWriter, req *http.Request, err error) {
211-
status := 500
212-
if e, ok := err.(*Error); ok {
213-
status = e.Status
214-
}
215-
go IncrCounter([]string{"goa", "handler", "error", strconv.Itoa(status)}, 1.0)
216-
if ctrl.ErrorHandler != nil {
217-
ctrl.ErrorHandler(ctx, rw, req, err)
218-
return
219-
}
220-
if h := ContextService(ctx).ErrorHandler; h != nil {
221-
h(ctx, rw, req, err)
222-
}
223-
}
224-
225207
// MuxHandler wraps a request handler into a MuxHandler. The MuxHandler initializes the
226208
// request context by loading the request state, invokes the handler and in case of error invokes
227209
// the controller (if there is one) or Service error handler.
@@ -231,10 +213,7 @@ func (ctrl *Controller) MuxHandler(name string, hdlr Handler, unm Unmarshaler) M
231213
// Setup middleware outside of closure
232214
middleware := func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
233215
if !ContextResponse(ctx).Written() {
234-
if err := hdlr(ctx, rw, req); err != nil {
235-
ContextService(ctx).LogInfo("ERROR", "err", err)
236-
ctrl.HandleError(ctx, rw, req, err)
237-
}
216+
return hdlr(ctx, rw, req)
238217
}
239218
return nil
240219
}
@@ -258,55 +237,27 @@ func (ctrl *Controller) MuxHandler(name string, hdlr Handler, unm Unmarshaler) M
258237
handler := middleware
259238
if err != nil {
260239
handler = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
261-
ctrl.ErrorHandler(ctx, rw, req, ErrInvalidEncoding(err))
262-
return nil
240+
rw.Header().Set("Content-Type", ErrorMediaIdentifier)
241+
return ContextResponse(ctx).Send(ctx, 400, ErrInvalidEncoding(err))
263242
}
264243
for i := range chain {
265244
handler = chain[ml-i-1](handler)
266245
}
267246
}
268247

269248
// Invoke middleware chain, wrap writer to capture response status and length
270-
handler(ctx, ContextResponse(ctx), req)
271-
}
272-
}
273-
274-
// DefaultErrorHandler returns a response with status 500 or the status specified in the error if
275-
// an instance of HTTPStatusError.
276-
// It writes the error message to the response body in both cases.
277-
func DefaultErrorHandler(ctx context.Context, rw http.ResponseWriter, req *http.Request, e error) {
278-
status := 500
279-
var respBody interface{}
280-
switch err := e.(type) {
281-
case *Error:
282-
status = err.Status
283-
respBody = err
284-
default:
285-
respBody = e.Error()
286-
}
287-
if status == 500 {
288-
LogError(ctx, e.Error())
249+
if err := handler(ctx, ContextResponse(ctx), req); err != nil {
250+
LastResortErrorHandler(ctx, rw, req, err)
251+
}
289252
}
290-
ContextResponse(ctx).Send(ctx, status, respBody)
291253
}
292254

293-
// TerseErrorHandler behaves like DefaultErrorHandler except that it does not write to the response
294-
// body for internal errors.
295-
func TerseErrorHandler(ctx context.Context, rw http.ResponseWriter, req *http.Request, e error) {
296-
status := 500
297-
var respBody interface{}
298-
switch err := e.(type) {
299-
case *Error:
300-
status = err.Status
301-
if status != 500 {
302-
respBody = err
303-
}
304-
}
305-
if respBody == nil {
306-
respBody = "internal error"
307-
}
308-
if status == 500 {
309-
LogError(ctx, e.Error())
310-
}
311-
ContextResponse(ctx).Send(ctx, status, respBody)
255+
// LastResortErrorHandler is the last thing that can handle an error propagating up the middleware
256+
// chain and turns all errors into a response indicating an internal error.
257+
// Ideally all errors are handled at a lower level in the middleware chain so they
258+
// can be logged properly.
259+
func LastResortErrorHandler(ctx context.Context, rw http.ResponseWriter, req *http.Request, e error) {
260+
LogError(ctx, "Last resort error handler", "err", e)
261+
respBody := fmt.Sprintf("Internal error: %s", e) // Sprintf catches panics
262+
ContextResponse(ctx).Send(ctx, 500, respBody)
312263
}

service_test.go

+6-4
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ var _ = Describe("Service", func() {
151151
errorHandlerCalled := false
152152

153153
BeforeEach(func() {
154-
s.ErrorHandler = TErrorHandler(&errorHandlerCalled)
154+
s.Use(TErrorHandler(&errorHandlerCalled))
155155
})
156156

157157
Context("by returning an error", func() {
@@ -219,10 +219,12 @@ var _ = Describe("Service", func() {
219219
})
220220
})
221221

222-
func TErrorHandler(witness *bool) goa.ErrorHandler {
223-
return func(ctx context.Context, rw http.ResponseWriter, req *http.Request, err error) {
222+
func TErrorHandler(witness *bool) goa.Middleware {
223+
m, _ := goa.NewMiddleware(func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
224224
*witness = true
225-
}
225+
return nil
226+
})
227+
return m
226228
}
227229

228230
func TMiddleware(witness *bool) goa.Middleware {

0 commit comments

Comments
 (0)