-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patherrors.go
More file actions
115 lines (106 loc) · 3.75 KB
/
Copy patherrors.go
File metadata and controls
115 lines (106 loc) · 3.75 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
package hellio
import "fmt"
// Kind classifies a Hellio API error by its HTTP status. Callers can switch on it
// or use errors.Is against the exported sentinels below.
type Kind int
const (
// KindGeneric is any non-2xx response that is not one of the specific kinds.
KindGeneric Kind = iota
// KindInvalidApiToken maps to HTTP 401.
KindInvalidApiToken
// KindInsufficientBalance maps to HTTP 402.
KindInsufficientBalance
// KindValidation maps to HTTP 422.
KindValidation
// KindRateLimit maps to HTTP 429.
KindRateLimit
// KindServiceUnavailable maps to HTTP 503 (a service switched off by an admin,
// or the API paused). Transient: retry later.
KindServiceUnavailable
// KindConflict maps to HTTP 409 (the request conflicts with server state, e.g. a
// USSD extension that is no longer available). The body "error" field carries the
// machine-readable reason, such as "extension_unavailable".
KindConflict
// KindExtensionRequired maps to HTTP 402 with the body "error" slug
// "extension_required": switching a USSD app to live mode before the USSD
// extension add-on has been purchased. Distinct from KindInsufficientBalance so
// callers can tell "buy the add-on" apart from "top up the balance".
KindExtensionRequired
)
// Error is returned for every non-2xx response. It carries the HTTP status code,
// a human message (from the body "message" field or a default), the parsed body,
// and a Kind for convenient switching. Use errors.As to reach these fields:
//
// var e *hellio.Error
// if errors.As(err, &e) { fmt.Println(e.StatusCode, e.Message) }
//
// Or errors.Is against a sentinel:
//
// if errors.Is(err, hellio.ErrRateLimit) { time.Sleep(time.Second) }
type Error struct {
Kind Kind
StatusCode int
Message string
// Body is the decoded JSON response, if any.
Body map[string]any
}
// Error implements the error interface.
func (e *Error) Error() string {
if e.Message == "" {
return fmt.Sprintf("hellio: request failed with status %d", e.StatusCode)
}
return fmt.Sprintf("hellio: %s (status %d)", e.Message, e.StatusCode)
}
// Is lets errors.Is match by Kind, so callers can compare against the sentinels.
func (e *Error) Is(target error) bool {
t, ok := target.(*Error)
if !ok {
return false
}
return e.Kind == t.Kind
}
// Errors returns the validation details a 422 response carries under "errors",
// or nil when the body has no such field.
func (e *Error) Errors() map[string]any {
if e.Body == nil {
return nil
}
if v, ok := e.Body["errors"].(map[string]any); ok {
return v
}
return nil
}
// Sentinel errors for use with errors.Is. Each matches any Error of the same Kind.
var (
ErrInvalidApiToken = &Error{Kind: KindInvalidApiToken}
ErrInsufficientBalance = &Error{Kind: KindInsufficientBalance}
ErrValidation = &Error{Kind: KindValidation}
ErrRateLimit = &Error{Kind: KindRateLimit}
ErrServiceUnavailable = &Error{Kind: KindServiceUnavailable}
ErrConflict = &Error{Kind: KindConflict}
ErrExtensionRequired = &Error{Kind: KindExtensionRequired}
)
func newError(status int, message string, body map[string]any) *Error {
kind := KindGeneric
switch status {
case 401:
kind = KindInvalidApiToken
case 402:
kind = KindInsufficientBalance
case 409:
kind = KindConflict
case 422:
kind = KindValidation
case 429:
kind = KindRateLimit
case 503:
kind = KindServiceUnavailable
}
// Some 402 responses share a status but mean different things; the body "error"
// slug is authoritative. extension_required means the USSD extension add-on must
// be purchased before switching an app to live mode.
if slug, ok := body["error"].(string); ok && slug == "extension_required" {
kind = KindExtensionRequired
}
return &Error{Kind: kind, StatusCode: status, Message: message, Body: body}
}