Skip to content

Commit c9c4c32

Browse files
committed
Further cleanup error handling.
Prevent double logging of panic errors. Recover middleware solely converts panics to errors. ErrorHandler middleware takes care of logging / building the response.
1 parent de4052b commit c9c4c32

File tree

5 files changed

+81
-67
lines changed

5 files changed

+81
-67
lines changed

middleware/error_handler.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,7 @@ func ErrorHandler(suppressInternal bool) goa.Middleware {
2020
return nil
2121
}
2222

23-
goa.LogInfo(ctx, "Default error handler", "err", e)
24-
status := 500
23+
status := http.StatusInternalServerError
2524
var respBody interface{}
2625
if err, ok := e.(*goa.Error); ok {
2726
status = err.Status
@@ -32,10 +31,15 @@ func ErrorHandler(suppressInternal bool) goa.Middleware {
3231
rw.Header().Set("Content-Type", "text/plain")
3332
}
3433
if status >= 500 && status < 600 {
35-
goa.LogError(ctx, e.Error())
34+
reqID := ctx.Value(reqIDKey)
35+
if reqID == nil {
36+
reqID = shortID()
37+
ctx = context.WithValue(ctx, reqIDKey, reqID)
38+
}
39+
goa.LogError(ctx, "uncaught error", "id", reqID, "msg", respBody)
3640
if suppressInternal {
3741
rw.Header().Set("Content-Type", goa.ErrorMediaIdentifier)
38-
respBody = goa.ErrInternal("internal error, detail suppressed")
42+
respBody = goa.ErrInternal("internal error [%s]", reqID)
3943
}
4044
}
4145
return goa.ContextResponse(ctx).Send(ctx, status, respBody)

middleware/error_handler_test.go

+9-2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import (
55
"errors"
66
"fmt"
77
"net/http"
8+
"regexp"
9+
"strings"
810

911
"golang.org/x/net/context"
1012

@@ -39,9 +41,12 @@ var _ = Describe("ErrorHandler", func() {
3941
})
4042

4143
Context("with a handler returning a Go error", func() {
44+
var requestCtx context.Context
45+
4246
BeforeEach(func() {
4347
service = newService(nil)
4448
h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
49+
requestCtx = ctx
4550
return errors.New("boom")
4651
}
4752
})
@@ -63,9 +68,11 @@ var _ = Describe("ErrorHandler", func() {
6368
Ω(rw.ParentHeader["Content-Type"]).Should(Equal([]string{goa.ErrorMediaIdentifier}))
6469
err := service.Decode(&decoded, bytes.NewBuffer(rw.Body), "application/json")
6570
Ω(err).ShouldNot(HaveOccurred())
66-
Ω(fmt.Sprintf("%v", decoded)).Should(Equal(fmt.Sprintf("%v", *goa.ErrInternal("internal error, detail suppressed"))))
71+
msg := fmt.Sprintf("%v", *goa.ErrInternal(`internal error [zzz]`))
72+
msg = regexp.QuoteMeta(msg)
73+
msg = strings.Replace(msg, "zzz", ".+", 1)
74+
Ω(fmt.Sprintf("%v", decoded)).Should(MatchRegexp(msg))
6775
})
68-
6976
})
7077
})
7178

middleware/recover.go

+15-38
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package middleware
22

33
import (
4-
"errors"
54
"fmt"
65
"net/http"
76
"runtime"
@@ -12,49 +11,27 @@ import (
1211
"golang.org/x/net/context"
1312
)
1413

15-
// Recover is a middleware that recovers panics and returns an internal error response.
14+
// Recover is a middleware that recovers panics and maps them to errors.
1615
func Recover() goa.Middleware {
1716
return func(h goa.Handler) goa.Handler {
1817
return func(ctx context.Context, rw http.ResponseWriter, req *http.Request) (err error) {
1918
defer func() {
2019
if r := recover(); r != nil {
21-
if ctx != nil {
22-
switch x := r.(type) {
23-
case string:
24-
err = fmt.Errorf("panic: %s", x)
25-
case error:
26-
err = x
27-
default:
28-
err = errors.New("unknown panic")
29-
}
30-
const size = 64 << 10 // 64KB
31-
buf := make([]byte, size)
32-
buf = buf[:runtime.Stack(buf, false)]
33-
lines := strings.Split(string(buf), "\n")
34-
stack := lines[3:]
35-
status := http.StatusInternalServerError
36-
var message string
37-
reqID := ctx.Value(reqIDKey)
38-
if reqID != nil {
39-
message = fmt.Sprintf(
40-
"%s\nRefer to the following token when contacting support: %s",
41-
http.StatusText(status),
42-
reqID)
43-
}
44-
goa.LogError(ctx, "PANIC", "error", err, "stack", strings.Join(stack, "\n"))
45-
46-
// note we must respond or else a 500 with "unhandled request" is the
47-
// default response.
48-
if message == "" {
49-
// without the logger and/or request id (from middleware) we can
50-
// only return the full error message for reference purposes. it
51-
// is unlikely to make sense to the caller unless they understand
52-
// the source code.
53-
message = err.Error()
54-
}
55-
rw.WriteHeader(status)
56-
rw.Write([]byte(message))
20+
var msg string
21+
switch x := r.(type) {
22+
case string:
23+
msg = fmt.Sprintf("panic: %s", x)
24+
case error:
25+
msg = fmt.Sprintf("panic: %s", x)
26+
default:
27+
msg = "unknown panic"
5728
}
29+
const size = 64 << 10 // 64KB
30+
buf := make([]byte, size)
31+
buf = buf[:runtime.Stack(buf, false)]
32+
lines := strings.Split(string(buf), "\n")
33+
stack := lines[3:]
34+
err = fmt.Errorf("%s\n%s", msg, strings.Join(stack, "\n"))
5835
}
5936
}()
6037
return h(ctx, rw, req)

middleware/recover_test.go

+45-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package middleware_test
22

33
import (
4+
"fmt"
45
"net/http"
56

67
"golang.org/x/net/context"
@@ -12,17 +13,50 @@ import (
1213
)
1314

1415
var _ = Describe("Recover", func() {
15-
It("recovers", func() {
16-
service := newService(nil)
17-
h := func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
18-
panic("boom")
19-
}
16+
var h goa.Handler
17+
var err error
18+
19+
JustBeforeEach(func() {
2020
rg := middleware.Recover()(h)
21-
service.Encoder(goa.NewJSONEncoder, "*/*")
22-
rw := new(testResponseWriter)
23-
ctx := newContext(service, rw, nil, nil)
24-
err := rg(ctx, rw, nil)
25-
Ω(err).Should(HaveOccurred())
26-
Ω(err.Error()).Should(Equal("panic: boom"))
21+
err = rg(nil, nil, nil)
22+
})
23+
24+
Context("with a handler that panics with a string", func() {
25+
BeforeEach(func() {
26+
h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
27+
panic("boom")
28+
}
29+
})
30+
31+
It("creates an error from the panic message", func() {
32+
Ω(err).Should(HaveOccurred())
33+
Ω(err.Error()).Should(HavePrefix("panic: boom\n"))
34+
})
35+
})
36+
37+
Context("with a handler that panics with an error", func() {
38+
BeforeEach(func() {
39+
h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
40+
panic(fmt.Errorf("boom"))
41+
}
42+
})
43+
44+
It("creates an error from the panic error message", func() {
45+
Ω(err).Should(HaveOccurred())
46+
Ω(err.Error()).Should(HavePrefix("panic: boom\n"))
47+
})
48+
})
49+
50+
Context("with a handler that panics with something else", func() {
51+
BeforeEach(func() {
52+
h = func(ctx context.Context, rw http.ResponseWriter, req *http.Request) error {
53+
panic(42)
54+
}
55+
})
56+
57+
It("creates a generic error message", func() {
58+
Ω(err).Should(HaveOccurred())
59+
Ω(err.Error()).Should(HavePrefix("unknown panic\n"))
60+
})
2761
})
2862
})

service.go

+4-12
Original file line numberDiff line numberDiff line change
@@ -245,19 +245,11 @@ func (ctrl *Controller) MuxHandler(name string, hdlr Handler, unm Unmarshaler) M
245245
}
246246
}
247247

248-
// Invoke middleware chain, wrap writer to capture response status and length
248+
// Invoke middleware chain, errors should be caught earlier, e.g. by ErrorHandler middleware
249249
if err := handler(ctx, ContextResponse(ctx), req); err != nil {
250-
LastResortErrorHandler(ctx, rw, req, err)
250+
LogError(ctx, "Last resort error handler", "err", err)
251+
respBody := fmt.Sprintf("Internal error: %s", err) // Sprintf catches panics
252+
ContextResponse(ctx).Send(ctx, 500, respBody)
251253
}
252254
}
253255
}
254-
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)
263-
}

0 commit comments

Comments
 (0)