-
Notifications
You must be signed in to change notification settings - Fork 57
/
errors.go
158 lines (143 loc) · 5.52 KB
/
errors.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
package typhon
import (
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
"unicode/utf8"
legacyproto "github.com/golang/protobuf/proto"
"github.com/monzo/slog"
"github.com/monzo/terrors"
terrorsproto "github.com/monzo/terrors/proto"
)
var (
mapTerr2Status = map[string]int{
terrors.ErrBadRequest: http.StatusBadRequest, // 400
terrors.ErrBadResponse: http.StatusNotAcceptable, // 406
terrors.ErrForbidden: http.StatusForbidden, // 403
terrors.ErrInternalService: http.StatusInternalServerError, // 500
terrors.ErrNotFound: http.StatusNotFound, // 404
terrors.ErrPreconditionFailed: http.StatusPreconditionFailed, // 412
terrors.ErrTimeout: http.StatusGatewayTimeout, // 504
terrors.ErrUnauthorized: http.StatusUnauthorized, // 401
terrors.ErrRateLimited: http.StatusTooManyRequests, // 429
}
mapStatus2Terr map[int]string
)
func init() {
mapStatus2Terr = make(map[int]string, len(mapTerr2Status))
for k, v := range mapTerr2Status {
mapStatus2Terr[v] = k
}
}
// ErrorStatusCode returns a HTTP status code for the given error.
//
// If the error is not a terror, this will always be 500 (Internal Server Error).
func ErrorStatusCode(err error) int {
code := terrors.Wrap(err, nil).(*terrors.Error).Code
if c, ok := mapTerr2Status[strings.SplitN(code, ".", 2)[0]]; ok {
return c
}
return http.StatusInternalServerError
}
// terr2StatusCode converts HTTP status codes to a roughly equivalent terrors' code
func status2TerrCode(code int) string {
if c, ok := mapStatus2Terr[code]; ok {
return c
}
return terrors.ErrInternalService
}
// ErrorFilter serialises and deserialises response errors. Without this filter, errors may not be passed across
// the network properly so it is recommended to use this in most/all cases.
// It tries to do everything it can to give you all the information that it can about why your request might have failed.
// Because of this, it has some weird behavior.
func ErrorFilter(req Request, svc Service) Response {
var rsp Response
// If the request contains an error, short-circuit and don't apply the given Service to the request.
// req.err being non-nil normally means we got an error trying to constructing the underlying http.Request and so there
// is no request to pass through to svc.
if req.err != nil {
rsp = NewResponse(req)
rsp.Error = req.err
} else {
// rsp.Error could be non-nil. This normally represents an error during the round trip (e.g. connection closed).
// It isn't expected to repsent an error received from the remote.
rsp = svc(req)
// If we never got a response then construct a default response. This is only expected to be needed if rsp.Error is non-nil.
if rsp.Response == nil {
// Status defaults to StatusOK here. It will be updated to the correct status later in the function.
rsp.Response = newHTTPResponse(req, http.StatusOK)
}
}
// ErrorFilter tries to make sure there is as much information as possible to debug the fault.
// If for some reason the Request is not currently set (e.g. if it was lost by another Filter) then re-set it to req.
if rsp.Request == nil {
rsp.Request = &req
}
// If there is an error we want to turn it into a Terror on the response.
if rsp.Error != nil {
// We should be here if we hit one of the first two cases in this function.
// We could also be here if something weird happened e.g. an error was set and a 200 response was returned by the server.
if rsp.StatusCode == http.StatusOK {
// We got an error, but there is no error in the underlying response; marshal
if rsp.Body != nil {
rsp.Body.Close()
}
rsp.Body = &bufCloser{}
terr := terrors.Wrap(rsp.Error, nil).(*terrors.Error)
rsp.Encode(terrors.Marshal(terr))
// We now set the status to the ACTUAL status code based on the Terror.
rsp.StatusCode = ErrorStatusCode(terr)
rsp.Header.Set("Terror", "1")
}
} else if rsp.StatusCode >= 400 && rsp.StatusCode <= 599 {
// There is an error in the underlying response; unmarshal
b, err := rsp.BodyBytes(false)
if err != nil {
var errParams map[string]string
if utf8.Valid(b) {
errParams = map[string]string{
"partially_read_body": string(b),
}
} else {
errParams = map[string]string{
"partially_read_body_base_64": base64.StdEncoding.EncodeToString(b),
}
}
// Don't attempt to parse a partially read error response. Return the underlying read error when this occurs.
rsp.Error = terrors.NewInternalWithCause(err, "reading error response body", errParams, "body_read_error")
} else {
switch rsp.Header.Get("Terror") {
case "1":
var err error
tp := &terrorsproto.Error{}
switch rsp.Header.Get("Content-Type") {
case "application/octet-stream", "application/x-protobuf", "application/protobuf":
err = legacyproto.Unmarshal(b, tp)
default:
err = json.Unmarshal(b, tp)
}
if err != nil {
slog.Warn(rsp.Request, "Failed to unmarshal terror: %v", err)
rsp.Error = errors.New(string(b))
} else {
rsp.Error = terrors.Unmarshal(tp)
}
default:
rsp.Error = errors.New(string(b))
}
}
}
// If there was an error but the error string is empty re-write the error with the information we have.
if rsp.Error != nil && rsp.Error.Error() == "" {
if rsp.Response != nil {
rsp.Error = fmt.Errorf("Response error (%d)", rsp.StatusCode)
} else {
// We don't expect to ever hit this case right now.
rsp.Error = fmt.Errorf("Response error")
}
}
return rsp
}