-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy patherrors.go
More file actions
185 lines (169 loc) · 7.75 KB
/
Copy patherrors.go
File metadata and controls
185 lines (169 loc) · 7.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
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
package b24gosdk
import (
"errors"
"strings"
)
// ErrorCode is the machine-readable code Bitrix24 puts in an error body.
//
// It is a string type rather than an enum on purpose: the portal ships new codes
// without warning, and a closed set would make an unknown one unexpressible.
// The constants below are spelling aids for the ones met most often, not a
// complete list — b24gosdk.Code("SOME_NEW_CODE") is equally valid.
type ErrorCode string
// Normalize folds a code to a canonical form for comparison.
//
// Bitrix24 is not consistent about case: REST errors come upper-cased
// (QUERY_LIMIT_EXCEEDED) while the OAuth ones come lower-cased (expired_token),
// and the same code has been seen both ways in the documentation. Comparing
// normalized forms means a caller never has to guess which spelling arrived.
func (c ErrorCode) Normalize() ErrorCode {
return ErrorCode(strings.ToUpper(strings.TrimSpace(string(c))))
}
// Codes seen most often. The list is deliberately short: it covers what a caller
// routinely branches on, not everything the portal can answer.
const (
// Rate and resource limits.
CodeQueryLimitExceeded ErrorCode = "QUERY_LIMIT_EXCEEDED"
CodeOperationTimeLimit ErrorCode = "OPERATION_TIME_LIMIT"
CodeOverloadLimit ErrorCode = "OVERLOAD_LIMIT"
// Authorization.
CodeExpiredToken ErrorCode = "expired_token"
CodeInvalidToken ErrorCode = "invalid_token"
CodeInvalidGrant ErrorCode = "invalid_grant"
CodeNoAuthFound ErrorCode = "NO_AUTH_FOUND"
CodeInsufficientScope ErrorCode = "insufficient_scope"
// Method and rights.
CodeMethodNotFound ErrorCode = "ERROR_METHOD_NOT_FOUND"
CodeAccessDenied ErrorCode = "ACCESS_DENIED"
CodePaymentRequired ErrorCode = "PAYMENT_REQUIRED"
// Batch.
CodeBatchLengthExceeded ErrorCode = "ERROR_BATCH_LENGTH_EXCEEDED"
CodeBatchMethodNotAllow ErrorCode = "ERROR_BATCH_METHOD_NOT_ALLOWED"
)
// REST 3.0 codes, all met on a live portal.
//
// They are their own constants rather than new spellings of the ones above
// because most of them describe a condition v1 has no code for at all: v1
// rejects a bad parameter with whatever the module felt like saying, v3 always
// with a validation error naming the field.
//
// The BITRIX_REST_V3_EXCEPTION_ prefix is NOT universal, so do not derive a
// code from it: crm.deal.timeline.activity.email.list answers a bad id with
// CRM_EMAIL_INVALID_REQUEST, in the v3 envelope, with no prefix. Anything not
// listed here is still matchable — Code("SOME_NEW_CODE") takes any string.
const (
CodeV3MethodNotFound ErrorCode = "BITRIX_REST_V3_EXCEPTION_METHODNOTFOUNDEXCEPTION"
CodeV3Validation ErrorCode = "BITRIX_REST_V3_EXCEPTION_VALIDATION_REQUESTVALIDATIONEXCEPTION"
CodeV3EntityNotFound ErrorCode = "BITRIX_REST_V3_EXCEPTION_ENTITYNOTFOUNDEXCEPTION"
CodeV3AccessDenied ErrorCode = "BITRIX_REST_V3_EXCEPTION_ACCESSDENIEDEXCEPTION"
CodeV3UnknownDTOProperty ErrorCode = "BITRIX_REST_V3_EXCEPTION_UNKNOWNDTOPROPERTYEXCEPTION"
CodeV3InvalidSelect ErrorCode = "BITRIX_REST_V3_EXCEPTION_INVALIDSELECTEXCEPTION"
CodeV3InvalidFilter ErrorCode = "BITRIX_REST_V3_EXCEPTION_INVALIDFILTEREXCEPTION"
CodeV3InvalidJSON ErrorCode = "BITRIX_REST_V3_EXCEPTION_INVALIDJSONEXCEPTION"
)
// Frequently matched codes as ready sentinels.
var (
ErrQueryLimitExceeded = Code(CodeQueryLimitExceeded)
ErrOperationTimeLimit = Code(CodeOperationTimeLimit)
ErrExpiredToken = Code(CodeExpiredToken)
ErrInvalidToken = Code(CodeInvalidToken)
ErrInvalidGrant = Code(CodeInvalidGrant)
ErrInsufficientScope = Code(CodeInsufficientScope)
ErrMethodNotFound = Code(CodeMethodNotFound)
ErrAccessDenied = Code(CodeAccessDenied)
ErrPaymentRequired = Code(CodePaymentRequired)
// REST 3.0 conditions with no v1 counterpart to fold into.
ErrV3Validation = Code(CodeV3Validation)
ErrV3EntityNotFound = Code(CodeV3EntityNotFound)
ErrV3AccessDenied = Code(CodeV3AccessDenied)
)
// v3Aliases folds a REST 3.0 code onto the v1 code for the SAME condition, so
// that errors.Is keeps working when a caller moves to a v3 URL.
//
// An entry is added only when both versions have been seen answering the same
// SITUATION, not merely when the two names read alike. That rule is why the
// table has one entry and not eight:
//
// - Method not found qualifies. A method name the portal does not know
// answers ERROR_METHOD_NOT_FOUND on v1 and
// BITRIX_REST_V3_EXCEPTION_METHODNOTFOUNDEXCEPTION on v3 — same situation,
// same meaning.
//
// - Access denied does NOT, even though the names match. A wrong webhook
// token answers INVALID_CREDENTIALS on v1 but
// BITRIX_REST_V3_EXCEPTION_ACCESSDENIEDEXCEPTION on v3: v3 spends one code
// where v1 spends two. Folding it onto ACCESS_DENIED would make
// errors.Is(err, ErrAccessDenied) fire on a bad token — a branch that on v1
// means "the credentials are fine, the rights are not". Match it as itself,
// with ErrV3AccessDenied.
//
// Keys must be written already normalized; TestV3AliasKeysAreNormalized holds
// that, because an unnormalized key would never be looked up.
var v3Aliases = map[ErrorCode]ErrorCode{
CodeV3MethodNotFound: CodeMethodNotFound,
}
// errCode is the sentinel type Code returns. It is unexported so the only way to
// build one is Code, which normalizes — a sentinel with an unnormalized code
// would silently never match.
type errCode ErrorCode
func (e errCode) Error() string { return "b24gosdk: " + string(e) }
// Code returns a sentinel error that errors.Is matches against any error
// carrying that code:
//
// if errors.Is(err, b24gosdk.ErrMethodNotFound) { … }
// if errors.Is(err, b24gosdk.Code("CREATE_DYNAMIC_TYPE_RESTRICTED")) { … }
//
// # Why not compare the string
//
// apiErr.Code == "ERROR_METHOD_NOT_FUND" compiles, runs, and quietly takes the
// wrong branch forever. Going through Code means the comparison is
// case-insensitive and the well-known codes have a named constant the compiler
// checks. A code the SDK has never heard of still works — the argument is a
// plain string type.
func Code(c ErrorCode) error { return errCode(c.Normalize()) }
// CodeOf reports the Bitrix24 error code an error carries, if any.
//
// The code is the one that ARRIVED, normalized in case only. On REST 3.0 that
// is the v3 spelling: a missing method gives
// BITRIX_REST_V3_EXCEPTION_METHODNOTFOUNDEXCEPTION here, while
// errors.Is(err, ErrMethodNotFound) is true for the same error. The two answer
// different questions — what the portal said, and what condition it was — so
// prefer errors.Is for branching and CodeOf for logging.
//
// ok is false for an error that is not an *APIError, and for an *APIError whose
// body carried no code — which happens when a proxy answers instead of the
// portal.
func CodeOf(err error) (ErrorCode, bool) {
var apiErr *APIError
if errors.As(err, &apiErr) && apiErr.Code != "" {
return ErrorCode(apiErr.Code).Normalize(), true
}
var sentinel errCode
if errors.As(err, &sentinel) {
return ErrorCode(sentinel), true
}
return "", false
}
// Is lets errors.Is match an *APIError against a Code sentinel.
//
// Matching is on the CODE ALONE, never on the HTTP status: OVERLOAD_LIMIT and
// QUERY_LIMIT_EXCEEDED both arrive as 503, so a status-based match would treat a
// manual block as a rate limit and retry something that must not be retried.
//
// A REST 3.0 code additionally matches the v1 sentinel for the same condition;
// see v3Aliases for which, and for why that list is short.
func (e *APIError) Is(target error) bool {
var want errCode
if !errors.As(target, &want) {
return false
}
if e == nil || e.Code == "" {
return false
}
got := ErrorCode(e.Code).Normalize()
if got == ErrorCode(want) {
return true
}
alias, ok := v3Aliases[got]
return ok && alias.Normalize() == ErrorCode(want)
}