Skip to content

Commit 8e7f0a1

Browse files
committed
Introduce error handler middleware.
Replaces the service and controller error handlers. This has several benefits, mainly it fixes a bug where error returned by middlewares would not be caught because the error handler was at the bottom of the middleware chain. Putting the error handler at the top of the chain does not work with middlewares that rely on the HTTP response being initialized like LogRequest. Using a middleware allows the client code to control where the mapping from error to HTTP response is done. The bootstrap code generated by gen_main builds the middleware stack in the "right order" (i.e. the error handler middleware is closer to the action handler then the LogRequest middleware).
1 parent 136f2be commit 8e7f0a1

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)