Skip to content

Commit a2aea2a

Browse files
authored
Merge pull request #98 from remind101/extract-error-handling
Extract error handling from middleware
2 parents 286bd21 + c295781 commit a2aea2a

4 files changed

Lines changed: 142 additions & 130 deletions

File tree

httpx/error.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package httpx
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
8+
"github.com/pkg/errors"
9+
"github.com/remind101/pkg/reporter"
10+
)
11+
12+
func Error(ctx context.Context, err error, rw http.ResponseWriter, r *http.Request) {
13+
reporter.Report(ctx, err)
14+
EncodeError(err, rw)
15+
}
16+
17+
type temporaryError interface {
18+
Temporary() bool // Is the error temporary?
19+
}
20+
21+
type timeoutError interface {
22+
Timeout() bool // Is the error a timeout?
23+
}
24+
25+
type statusCoder interface {
26+
StatusCode() int
27+
}
28+
29+
func EncodeError(err error, rw http.ResponseWriter) {
30+
rw.Header().Set("Content-Type", "application/json")
31+
rw.WriteHeader(ErrorStatusCode(err))
32+
33+
errorResp := map[string]string{
34+
"error": err.Error(),
35+
}
36+
37+
json.NewEncoder(rw).Encode(errorResp)
38+
}
39+
40+
func ErrorStatusCode(err error) int {
41+
rootErr := errors.Cause(err)
42+
if e, ok := rootErr.(statusCoder); ok {
43+
return e.StatusCode()
44+
}
45+
if e, ok := rootErr.(temporaryError); ok && e.Temporary() {
46+
return http.StatusServiceUnavailable
47+
}
48+
49+
if e, ok := rootErr.(timeoutError); ok && e.Timeout() {
50+
return http.StatusServiceUnavailable
51+
}
52+
53+
return http.StatusInternalServerError
54+
}

httpx/error_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package httpx_test
2+
3+
import (
4+
"context"
5+
"errors"
6+
"net"
7+
"net/http"
8+
"net/http/httptest"
9+
"testing"
10+
11+
"github.com/remind101/pkg/httpx"
12+
"github.com/remind101/pkg/reporter"
13+
)
14+
15+
type tmpError string
16+
17+
func (te tmpError) Error() string {
18+
return string(te)
19+
}
20+
21+
func (te tmpError) Temporary() bool {
22+
return true
23+
}
24+
25+
type statusCodeError struct {
26+
Err error
27+
statusCode int
28+
}
29+
30+
func (s statusCodeError) Error() string {
31+
return s.Err.Error()
32+
}
33+
34+
func (s statusCodeError) StatusCode() int {
35+
return s.statusCode
36+
}
37+
38+
func TestError(t *testing.T) {
39+
tests := []struct {
40+
Error error
41+
Body string
42+
Code int
43+
}{
44+
{
45+
Error: errors.New("boom"),
46+
Body: `{"error":"boom"}` + "\n",
47+
Code: 500,
48+
},
49+
{
50+
Error: tmpError("service unavailable"),
51+
Body: `{"error":"service unavailable"}` + "\n",
52+
Code: 503,
53+
},
54+
{
55+
Error: &net.DNSError{Err: "no such host", IsTimeout: true},
56+
Body: `{"error":"lookup : no such host"}` + "\n",
57+
Code: 503,
58+
},
59+
{
60+
Error: statusCodeError{Err: errors.New("invalid request"), statusCode: 400},
61+
Body: `{"error":"invalid request"}` + "\n",
62+
Code: 400,
63+
},
64+
}
65+
66+
for _, tt := range tests {
67+
r, _ := http.NewRequest("GET", "/", nil)
68+
rw := httptest.NewRecorder()
69+
ctx := reporter.WithReporter(context.Background(), reporter.NewLogReporter())
70+
httpx.Error(ctx, tt.Error, rw, r)
71+
72+
if got, want := rw.Body.String(), tt.Body; got != want {
73+
t.Fatalf("Body => %#v; want %#v", got, want)
74+
}
75+
76+
if got, want := rw.Code, tt.Code; got != want {
77+
t.Fatalf("Status => %v; want %v", got, want)
78+
}
79+
}
80+
}

httpx/middleware/error.go

Lines changed: 2 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,16 @@
11
package middleware
22

33
import (
4-
"encoding/json"
54
"net/http"
65

76
"context"
87

9-
"github.com/pkg/errors"
108
"github.com/remind101/pkg/httpx"
119
"github.com/remind101/pkg/reporter"
1210
)
1311

1412
type ErrorHandlerFunc func(context.Context, error, http.ResponseWriter, *http.Request)
1513

16-
type temporaryError interface {
17-
Temporary() bool // Is the error temporary?
18-
}
19-
20-
type timeoutError interface {
21-
Timeout() bool // Is the error a timeout?
22-
}
23-
24-
type statusCoder interface {
25-
StatusCode() int
26-
}
27-
2814
// DefaultErrorHandler is an error handler that will respond with the error
2915
// message and a 500 status.
3016
var DefaultErrorHandler = func(ctx context.Context, err error, w http.ResponseWriter, r *http.Request) {
@@ -38,37 +24,10 @@ var ReportingErrorHandler = func(ctx context.Context, err error, w http.Response
3824
writeError(w, err)
3925
}
4026

41-
var JSONReportingErrorHandler = func(ctx context.Context, err error, w http.ResponseWriter, r *http.Request) {
42-
reporter.Report(ctx, err)
43-
status := statusCodeForError(err)
44-
w.Header().Set("Content-Type", "application/json")
45-
w.WriteHeader(status)
46-
47-
errorResp := map[string]string{
48-
"error": err.Error(),
49-
}
50-
51-
json.NewEncoder(w).Encode(errorResp)
52-
}
27+
var JSONReportingErrorHandler = httpx.Error
5328

5429
func writeError(w http.ResponseWriter, err error) {
55-
http.Error(w, err.Error(), statusCodeForError(err))
56-
}
57-
58-
func statusCodeForError(err error) int {
59-
rootErr := errors.Cause(err)
60-
if e, ok := rootErr.(statusCoder); ok {
61-
return e.StatusCode()
62-
}
63-
if e, ok := rootErr.(temporaryError); ok && e.Temporary() {
64-
return http.StatusServiceUnavailable
65-
}
66-
67-
if e, ok := rootErr.(timeoutError); ok && e.Timeout() {
68-
return http.StatusServiceUnavailable
69-
}
70-
71-
return http.StatusInternalServerError
30+
http.Error(w, err.Error(), httpx.ErrorStatusCode(err))
7231
}
7332

7433
// Error is an httpx.Handler that will handle errors with an ErrorHandler.

httpx/middleware/error_test.go

Lines changed: 6 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -2,117 +2,36 @@ package middleware
22

33
import (
44
"errors"
5-
"net"
65
"net/http"
76
"net/http/httptest"
87
"testing"
98

109
"context"
1110

1211
"github.com/remind101/pkg/httpx"
13-
"github.com/remind101/pkg/reporter"
1412
)
1513

16-
type tmpError string
17-
18-
func (te tmpError) Error() string {
19-
return string(te)
20-
}
21-
22-
func (te tmpError) Temporary() bool {
23-
return true
24-
}
25-
26-
type statusCodeError struct {
27-
Err error
28-
statusCode int
29-
}
30-
31-
func (s statusCodeError) Error() string {
32-
return s.Err.Error()
33-
}
34-
35-
func (s statusCodeError) StatusCode() int {
36-
return s.statusCode
37-
}
38-
39-
func TestErrorMiddleware(t *testing.T) {
40-
tests := []struct {
41-
Error error
42-
Body string
43-
Code int
44-
ErrorHandler ErrorHandlerFunc
45-
}{
46-
{
47-
Error: errors.New("boom"),
48-
Body: "boom\n",
49-
Code: 500,
50-
},
51-
{
52-
Error: tmpError("service unavailable"),
53-
Body: "service unavailable\n",
54-
Code: 503,
55-
},
56-
{
57-
Error: &net.DNSError{Err: "no such host", IsTimeout: true},
58-
Body: "lookup : no such host\n",
59-
Code: 503,
60-
},
61-
{
62-
Error: statusCodeError{Err: errors.New("invalid request"), statusCode: 400},
63-
Body: "invalid request\n",
64-
Code: 400,
65-
},
66-
{
67-
Error: errors.New("boom"),
68-
Body: "{\"error\":\"boom\"}\n",
69-
Code: 500,
70-
ErrorHandler: JSONReportingErrorHandler,
71-
},
72-
}
73-
74-
for _, tt := range tests {
75-
h := &Error{
76-
handler: httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
77-
return tt.Error
78-
}),
79-
ErrorHandler: tt.ErrorHandler,
80-
}
81-
req, _ := http.NewRequest("GET", "/", nil)
82-
resp := httptest.NewRecorder()
83-
ctx := reporter.WithReporter(context.Background(), reporter.NewLogReporter())
84-
err := h.ServeHTTPContext(ctx, resp, req)
85-
if err != tt.Error {
86-
t.Fatal("Expected error to be returned")
87-
}
88-
89-
if got, want := resp.Body.String(), tt.Body; got != want {
90-
t.Fatalf("Body => %#v; want %#v", got, want)
91-
}
92-
93-
if got, want := resp.Code, tt.Code; got != want {
94-
t.Fatalf("Status => %v; want %v", got, want)
95-
}
96-
}
97-
}
98-
9914
func TestErrorWithHandler(t *testing.T) {
10015
var called bool
16+
boomErr := errors.New("boom")
10117

10218
h := &Error{
10319
ErrorHandler: func(ctx context.Context, err error, w http.ResponseWriter, r *http.Request) {
10420
called = true
10521
},
10622
handler: httpx.HandlerFunc(func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
107-
return errors.New("boom")
23+
return boomErr
10824
}),
10925
}
11026

11127
ctx := context.Background()
11228
req, _ := http.NewRequest("GET", "/path", nil)
11329
resp := httptest.NewRecorder()
11430

115-
h.ServeHTTPContext(ctx, resp, req)
31+
err := h.ServeHTTPContext(ctx, resp, req)
32+
if err != boomErr {
33+
t.Fatal("Expected error to be returned")
34+
}
11635

11736
if !called {
11837
t.Fatal("Expected the error handler to be called")

0 commit comments

Comments
 (0)