-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy patherr.go
165 lines (139 loc) · 4.2 KB
/
err.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
package xhttp
import (
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"oss.terrastruct.com/util-go/cmdlog"
)
// Error represents an HTTP error.
// It's exported only for comparison in tests.
type Error struct {
Code int
Resp interface{}
Err error
}
var _ interface {
Is(error) bool
Unwrap() error
} = Error{}
// Errorf creates a new error with code, resp, msg and v.
//
// When returned from an xhttp.HandlerFunc, it will be correctly logged
// and written to the connection. See xhttp.WrapHandlerFunc
func Errorf(code int, resp interface{}, msg string, v ...interface{}) error {
return errorWrap(code, resp, fmt.Errorf(msg, v...))
}
// ErrorWrap wraps err with the code and resp for xhttp.HandlerFunc.
//
// When returned from an xhttp.HandlerFunc, it will be correctly logged
// and written to the connection. See xhttp.WrapHandlerFunc
func ErrorWrap(code int, resp interface{}, err error) error {
return errorWrap(code, resp, err)
}
func errorWrap(code int, resp interface{}, err error) error {
if resp == nil {
resp = http.StatusText(code)
}
return Error{code, resp, err}
}
func (e Error) Unwrap() error {
return e.Err
}
func (e Error) Is(err error) bool {
e2, ok := err.(Error)
if !ok {
return false
}
return e.Code == e2.Code && e.Resp == e2.Resp && errors.Is(e.Err, e2.Err)
}
func (e Error) Error() string {
return fmt.Sprintf("http error with code %v and resp %#v: %v", e.Code, e.Resp, e.Err)
}
// HandlerFunc is like http.HandlerFunc but returns an error.
// See Errorf and ErrorWrap.
type HandlerFunc func(w http.ResponseWriter, r *http.Request) error
type HandlerFuncAdapter struct {
Log *cmdlog.Logger
Func HandlerFunc
}
// ServeHTTP adapts xhttp.HandlerFunc into http.Handler for usage with standard
// HTTP routers like chi.
//
// It logs and writes any error from xhttp.HandlerFunc to the connection.
//
// If err was created with xhttp.Errorf or wrapped with xhttp.WrapError, then the error
// will be logged at the correct level for the status code and xhttp.JSON will be called
// with the code and resp.
//
// 400s are logged as warns and 500s as errors.
//
// If the error was not created with the xhttp helpers then a 500 will be written.
//
// If resp is nil, then resp is set to http.StatusText(code)
//
// If the code is not a 400 or a 500, then an error about about the unexpected error code
// will be logged and a 500 will be written. The original error will also be logged.
func (a HandlerFuncAdapter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
var h http.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
err := a.Func(w, r)
if err != nil {
handleError(a.Log, w, err)
}
})
h.ServeHTTP(w, r)
}
func handleError(clog *cmdlog.Logger, w http.ResponseWriter, err error) {
var herr Error
ok := errors.As(err, &herr)
if !ok {
herr = ErrorWrap(http.StatusInternalServerError, nil, err).(Error)
}
var logger *log.Logger
switch {
case 400 <= herr.Code && herr.Code < 500:
logger = clog.Warn
case 500 <= herr.Code && herr.Code < 600:
logger = clog.Error
default:
logger = clog.Error
clog.Error.Printf("unexpected non error http status code %d with resp: %#v", herr.Code, herr.Resp)
herr.Code = http.StatusInternalServerError
herr.Resp = nil
}
if herr.Resp == nil {
herr.Resp = http.StatusText(herr.Code)
}
logger.Printf("error handling http request: %v", err)
ww, ok := w.(writtenResponseWriter)
if !ok {
clog.Warn.Printf("response writer does not implement Written, double write logs possible: %#v", w)
} else if ww.Written() {
// Avoid double writes if an error occurred while the response was
// being written.
return
}
JSON(clog, w, herr.Code, map[string]interface{}{
"error": herr.Resp,
})
}
type writtenResponseWriter interface {
Written() bool
}
func JSON(clog *cmdlog.Logger, w http.ResponseWriter, code int, v interface{}) {
if v == nil {
v = map[string]interface{}{
"status": http.StatusText(code),
}
}
b, err := json.Marshal(v)
if err != nil {
clog.Error.Printf("json marshal error: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
_, _ = w.Write(b)
}