-
Notifications
You must be signed in to change notification settings - Fork 6
/
fancyerror.go
277 lines (250 loc) · 7.9 KB
/
fancyerror.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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
package httprequest
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"mime"
"net/http"
"strings"
"unicode"
"golang.org/x/net/html"
"golang.org/x/net/html/atom"
"gopkg.in/errgo.v1"
)
func isDecodeResponseError(err error) bool {
_, ok := err.(*DecodeResponseError)
return ok
}
// DecodeResponseError represents an error when an HTTP
// response could not be decoded.
type DecodeResponseError struct {
// Response holds the problematic HTTP response.
// The body of this does not need to be closed
// and may be truncated if the response is large.
Response *http.Response
// DecodeError holds the error that was encountered
// when decoding.
DecodeError error
}
func (e *DecodeResponseError) Error() string {
return e.DecodeError.Error()
}
// newDecodeResponseError returns a new DecodeResponseError that
// uses the given error for its message. The Response field
// holds a copy of req. If bodyData is non-nil, it
// will be used as the data in the Response.Body field;
// otherwise body data will be read from req.Body.
func newDecodeResponseError(resp *http.Response, bodyData []byte, err error) *DecodeResponseError {
if bodyData == nil {
bodyData = readBodyForError(resp.Body)
}
resp1 := *resp
resp1.Body = ioutil.NopCloser(bytes.NewReader(bodyData))
return &DecodeResponseError{
Response: &resp1,
DecodeError: errgo.Mask(err, errgo.Any),
}
}
// newDecodeRequestError returns a new DecodeRequestError that
// uses the given error for its message. The Request field
// holds a copy of req. If bodyData is non-nil, it
// will be used as the data in the Request.Body field;
// otherwise body data will be read from req.Body.
func newDecodeRequestError(req *http.Request, bodyData []byte, err error) *DecodeRequestError {
if bodyData == nil {
bodyData = readBodyForError(req.Body)
}
req1 := *req
req1.Body = ioutil.NopCloser(bytes.NewReader(bodyData))
return &DecodeRequestError{
Request: &req1,
DecodeError: errgo.Mask(err, errgo.Any),
}
}
// DecodeRequestError represents an error when an HTTP
// request could not be decoded.
type DecodeRequestError struct {
// Request holds the problematic HTTP request.
// The body of this does not need to be closed
// and may be truncated if the response is large.
Request *http.Request
// DecodeError holds the error that was encountered
// when decoding.
DecodeError error
}
func (e *DecodeRequestError) Error() string {
return e.DecodeError.Error()
}
// fancyDecodeError is an error type that tries to
// produce a nice error message when the content
// type of a request or response is wrong.
type fancyDecodeError struct {
// contentType holds the contentType of the request or response.
contentType string
// body holds up to maxErrorBodySize saved bytes of the
// request or response body.
body []byte
}
func newFancyDecodeError(h http.Header, body io.Reader) *fancyDecodeError {
return &fancyDecodeError{
contentType: h.Get("Content-Type"),
body: readBodyForError(body),
}
}
func readBodyForError(r io.Reader) []byte {
data, _ := ioutil.ReadAll(io.LimitReader(noErrorReader{r}, int64(maxErrorBodySize)))
return data
}
// maxErrorBodySize holds the maximum amount of body that
// we try to read for an error before extracting text from it.
// It's reasonably large because:
// a) HTML often has large embedded scripts which we want
// to skip and
// b) it should be an relatively unusual case so the size
// shouldn't harm.
//
// It's defined as a variable so that it can be redefined in tests.
var maxErrorBodySize = 200 * 1024
// isJSONMediaType reports whether the content type of the given header implies
// that the content is JSON.
func isJSONMediaType(header http.Header) bool {
contentType := header.Get("Content-Type")
mediaType, _, _ := mime.ParseMediaType(contentType)
m := strings.TrimPrefix(mediaType, "application/")
if len(m) == len(mediaType) {
return false
}
// Look for +json suffix. See https://tools.ietf.org/html/rfc6838#section-4.2.8
// We recognize multiple suffixes too (e.g. application/something+json+other)
// as that seems to be a possibility.
for {
i := strings.Index(m, "+")
if i == -1 {
return m == "json"
}
if m[0:i] == "json" {
return true
}
m = m[i+1:]
}
}
// Error implements error.Error by trying to produce a decent
// error message derived from the body content.
func (e *fancyDecodeError) Error() string {
mediaType, _, err := mime.ParseMediaType(e.contentType)
if err != nil {
// Even if there's no media type, we want to see something useful.
mediaType = fmt.Sprintf("%q", e.contentType)
}
// TODO use charset.NewReader to convert from non-utf8 content?
switch mediaType {
case "text/html":
text, err := htmlToText(bytes.NewReader(e.body))
if err != nil {
// Note: it seems that this can never actually
// happen - the only way that the HTML parser
// can fail is if there's a read error and we've
// removed that possibility by using
// noErrorReader above.
return fmt.Sprintf("unexpected (and invalid) content text/html; want application/json; content: %q", sizeLimit(e.body))
}
if len(text) == 0 {
return fmt.Sprintf(`unexpected content type text/html; want application/json; content: %q`, sizeLimit(e.body))
}
return fmt.Sprintf(`unexpected content type text/html; want application/json; content: %s`, sizeLimit(text))
case "text/plain":
return fmt.Sprintf(`unexpected content type text/plain; want application/json; content: %s`, sizeLimit(sanitizeText(string(e.body), true)))
default:
return fmt.Sprintf(`unexpected content type %s; want application/json; content: %q`, mediaType, sizeLimit(e.body))
}
}
// noErrorReader wraps a reader, turning any errors into io.EOF
// so that we can extract some content even if we get an io error.
type noErrorReader struct {
r io.Reader
}
func (r noErrorReader) Read(buf []byte) (int, error) {
n, err := r.r.Read(buf)
if err != nil {
err = io.EOF
}
return n, err
}
func sizeLimit(data []byte) []byte {
const max = 1024
if len(data) < max {
return data
}
return append(data[0:max], fmt.Sprintf(" ... [%d bytes omitted]", len(data)-max)...)
}
// htmlToText attempts to return some relevant textual content
// from the HTML content in the given reader, formatted
// as a single line.
func htmlToText(r io.Reader) ([]byte, error) {
n, err := html.Parse(r)
if err != nil {
return nil, err
}
var buf bytes.Buffer
htmlNodeToText(&buf, n)
return buf.Bytes(), nil
}
// htmlNodeToText tries to extract some text from an arbitrary HTML
// page. It doesn't try to avoid looking in the header, because the
// title is in the header and is often the most succinct description of
// the page.
func htmlNodeToText(w *bytes.Buffer, n *html.Node) {
for ; n != nil; n = n.NextSibling {
switch n.Type {
case html.TextNode:
data := sanitizeText(n.Data, false)
if len(data) == 0 {
break
}
if w.Len() > 0 {
w.WriteString("; ")
}
w.Write(data)
case html.ElementNode:
if n.DataAtom != atom.Script {
htmlNodeToText(w, n.FirstChild)
}
case html.DocumentNode:
htmlNodeToText(w, n.FirstChild)
}
}
}
// sanitizeText tries to make the given string easier to read when presented
// as a single line. It squashes each run of white space into a single
// space, trims leading and trailing white space and trailing full
// stops. If newlineSemi is true, any newlines will be replaced with a
// semicolon.
func sanitizeText(s string, newlineSemi bool) []byte {
out := make([]byte, 0, len(s))
prevWhite := false
for _, r := range s {
if newlineSemi && r == '\n' && len(out) > 0 {
out = append(out, ';')
prevWhite = true
continue
}
if unicode.IsSpace(r) {
if len(out) > 0 {
prevWhite = true
}
continue
}
if prevWhite {
out = append(out, ' ')
prevWhite = false
}
out = append(out, string(r)...)
}
// Remove final space, any full stops and any final semicolon
// we might have added.
out = bytes.TrimRightFunc(out, func(r rune) bool {
return r == '.' || r == ' ' || r == ';'
})
return out
}