Repository navigation
Expand file tree
/
Copy patherrors.go
More file actions
122 lines (108 loc) · 3.26 KB
/
Copy patherrors.go
File metadata and controls
122 lines (108 loc) · 3.26 KB
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
package wormholescan
import (
"bytes"
"encoding/json"
"errors"
"net/http"
"strconv"
)
const (
errPrefix = "wormholescan: "
maxErrorBodyBytes = 64 * 1024
)
// ErrNotFound is returned when the API responds with 404.
var ErrNotFound = errors.New("wormholescan: not found")
// ErrRateLimited is returned when the API responds with 429.
var ErrRateLimited = errors.New("wormholescan: rate limited")
// ErrBadRequest is returned when the API responds with 400.
var ErrBadRequest = errors.New("wormholescan: bad request")
// APIError is a non-2xx Wormholescan API response.
type APIError struct {
// StatusCode is the HTTP status code.
StatusCode int
// Code is the API error code from the JSON body, or 0 when absent.
Code int
// Message is the API error message from the JSON body, or empty when absent.
Message string
// RequestID is the request id from the JSON details, or empty when absent.
RequestID string
// Body is the raw response body, truncated to 64 KiB.
Body []byte
}
// Error returns a summary of the API error.
func (e *APIError) Error() string {
msg := e.Message
if msg == "" {
msg = http.StatusText(e.StatusCode)
}
out := errPrefix + strconv.Itoa(e.StatusCode)
if msg != "" {
out += " " + msg
}
if e.RequestID != "" {
out += " (request_id " + e.RequestID + ")"
}
return out
}
// Is reports whether target is the sentinel that matches e's status code.
func (e *APIError) Is(target error) bool {
switch e.StatusCode {
case http.StatusNotFound:
return target == ErrNotFound
case http.StatusTooManyRequests:
return target == ErrRateLimited
case http.StatusBadRequest:
return target == ErrBadRequest
default:
return false
}
}
// apiErrorBody is the JSON error envelope the API returns on non-2xx responses.
type apiErrorBody struct {
// Code is the API error code.
Code int `json:"code"`
// Message is the API error message.
Message string `json:"message"`
// Details holds extra fields such as the request id.
Details []apiErrorDetail `json:"details"`
}
// apiErrorDetail is one entry in the API error details array.
type apiErrorDetail struct {
// RequestID is the server-assigned request id.
RequestID string `json:"request_id"`
}
// CheckResponse returns nil for 2xx responses and an [APIError] otherwise.
func CheckResponse(rsp *http.Response, body []byte) error {
if rsp.StatusCode >= http.StatusOK && rsp.StatusCode < http.StatusMultipleChoices {
return nil
}
apiErr := &APIError{
StatusCode: rsp.StatusCode,
Body: clipBody(body),
}
fillAPIErrorFromBody(apiErr, body)
return apiErr
}
// checkResponse returns nil for 2xx responses and an [APIError] otherwise.
func checkResponse(rsp *http.Response, body []byte) error {
return CheckResponse(rsp, body)
}
// fillAPIErrorFromBody copies code, message, and request id out of a JSON body.
func fillAPIErrorFromBody(apiErr *APIError, body []byte) {
var parsed apiErrorBody
if err := json.Unmarshal(body, &parsed); err != nil {
return
}
apiErr.Code = parsed.Code
apiErr.Message = parsed.Message
if len(parsed.Details) > 0 {
apiErr.RequestID = parsed.Details[0].RequestID
}
}
// clipBody returns a copy of body truncated to maxErrorBodyBytes.
func clipBody(body []byte) []byte {
if len(body) > maxErrorBodyBytes {
body = body[:maxErrorBodyBytes]
}
return bytes.Clone(body)
}