Repository navigation
Expand file tree
/
Copy patherrors.go
More file actions
173 lines (146 loc) · 5.12 KB
/
Copy patherrors.go
File metadata and controls
173 lines (146 loc) · 5.12 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
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
package mailtrap
import (
"encoding/json"
"fmt"
"net/http"
"sort"
"strconv"
"strings"
"time"
)
// Error is the base Mailtrap API error for non-2xx responses. Status-specific
// wrappers embed it (e.g. RateLimitError, ValidationError); match with errors.As.
type Error struct {
// StatusCode is the HTTP status code.
StatusCode int
// Messages holds the human-readable error message(s) returned by the API.
Messages []string
// Body is the raw, undecoded response body.
Body []byte
}
func (e *Error) Error() string {
status := fmt.Sprintf("%d %s", e.StatusCode, http.StatusText(e.StatusCode))
if len(e.Messages) == 0 {
return "mailtrap: " + status
}
return "mailtrap: " + status + ": " + strings.Join(e.Messages, "; ")
}
// UnauthorizedError indicates the API token is missing or invalid (401).
type UnauthorizedError struct{ Err *Error }
func (e *UnauthorizedError) Error() string { return e.Err.Error() }
// Unwrap exposes the base *Error to errors.As/Is.
func (e *UnauthorizedError) Unwrap() error { return e.Err }
// ForbiddenError indicates the token is valid but lacks access to the
// resource (403).
type ForbiddenError struct{ Err *Error }
func (e *ForbiddenError) Error() string { return e.Err.Error() }
// Unwrap exposes the base *Error to errors.As/Is.
func (e *ForbiddenError) Unwrap() error { return e.Err }
// NotFoundError indicates the requested resource does not exist (404).
type NotFoundError struct{ Err *Error }
func (e *NotFoundError) Error() string { return e.Err.Error() }
// Unwrap exposes the base *Error to errors.As/Is.
func (e *NotFoundError) Unwrap() error { return e.Err }
// RateLimitError indicates the API rate limit was exceeded (429).
type RateLimitError struct {
Err *Error
// RetryAfter is the delay advised by the Retry-After header, or 0 if absent.
RetryAfter time.Duration
}
func (e *RateLimitError) Error() string { return e.Err.Error() }
// Unwrap exposes the base *Error to errors.As/Is.
func (e *RateLimitError) Unwrap() error { return e.Err }
// ValidationError indicates request validation failed (422). Fields maps each
// invalid attribute to its messages; the API's record-level errors use "base".
type ValidationError struct {
Err *Error
Fields map[string][]string
}
func (e *ValidationError) Error() string { return e.Err.Error() }
// Unwrap exposes the base *Error to errors.As/Is.
func (e *ValidationError) Unwrap() error { return e.Err }
// parseError maps an HTTP error response to a typed error. It understands both
// Mailtrap error shapes: the sending hosts return {"success":false,"errors":[…]}
// and the general host returns {"error":…} or {"errors":…} (string or object).
func parseError(resp *http.Response, body []byte) error {
base := &Error{StatusCode: resp.StatusCode, Body: body}
var env struct {
Error string `json:"error"`
Errors json.RawMessage `json:"errors"`
}
_ = json.Unmarshal(body, &env) // best effort; body may be empty or non-JSON
var fields map[string][]string
switch {
case len(env.Errors) > 0:
base.Messages, fields = decodeErrors(env.Errors)
case env.Error != "":
base.Messages = []string{env.Error}
}
switch {
case resp.StatusCode == http.StatusTooManyRequests:
return &RateLimitError{Err: base, RetryAfter: parseRetryAfter(resp.Header)}
case resp.StatusCode == http.StatusUnauthorized:
return &UnauthorizedError{Err: base}
case resp.StatusCode == http.StatusForbidden:
return &ForbiddenError{Err: base}
case resp.StatusCode == http.StatusNotFound:
return &NotFoundError{Err: base}
case resp.StatusCode == http.StatusUnprocessableEntity, fields != nil:
if fields == nil {
fields = map[string][]string{}
}
return &ValidationError{Err: base, Fields: fields}
default:
return base
}
}
// decodeErrors interprets the polymorphic "errors" field: an array of strings
// (sending hosts), a single string (general 403/429), or an object mapping
// fields to messages (general 422 validation).
func decodeErrors(raw json.RawMessage) (messages []string, fields map[string][]string) {
var arr []string
if json.Unmarshal(raw, &arr) == nil {
return arr, nil
}
var s string
if json.Unmarshal(raw, &s) == nil {
return []string{s}, nil
}
var obj map[string][]string
if json.Unmarshal(raw, &obj) == nil {
fields := make([]string, 0, len(obj))
for field := range obj {
fields = append(fields, field)
}
sort.Strings(fields) // deterministic message order
for _, field := range fields {
joined := strings.Join(obj[field], ", ")
// "base" carries record-level errors and is shown without a prefix,
// matching the other Mailtrap SDKs.
if field == "base" {
messages = append(messages, joined)
} else {
messages = append(messages, field+": "+joined)
}
}
return messages, obj
}
return nil, nil
}
// parseRetryAfter reads the Retry-After header, supporting both the delay-seconds
// and HTTP-date forms.
func parseRetryAfter(h http.Header) time.Duration {
v := h.Get("Retry-After")
if v == "" {
return 0
}
if secs, err := strconv.Atoi(v); err == nil {
return time.Duration(secs) * time.Second
}
if t, err := http.ParseTime(v); err == nil {
if d := time.Until(t); d > 0 {
return d
}
}
return 0
}