-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy patherrors.go
More file actions
361 lines (326 loc) · 12.1 KB
/
Copy patherrors.go
File metadata and controls
361 lines (326 loc) · 12.1 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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
// Code generated by the LoginRadius SDK generator; DO NOT EDIT.
//
// The predicates, the error-envelope shapes, the field-alias probing order, and
// the status hints all come from the shared SDK manifest, so every LoginRadius SDK
// classifies and describes the same failure the same way.
package loginradius
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"reflect"
"strconv"
"strings"
"github.com/LoginRadius/go-sdk/v12/openapi"
)
// Error is the typed error returned from every Client method on a non-2xx
// response or transport failure. Customers should errors.As to this type for
// structured handling.
type Error struct {
// StatusCode is the HTTP status code returned by the LoginRadius API.
// Zero when the request never reached the server (DNS, TLS, timeout).
StatusCode int
// Code is the LoginRadius API error code (when the response body parsed
// into a known error envelope). Empty for transport errors and for
// non-standard error bodies.
Code string
// Message is the human-readable error description. Falls back to the
// underlying error string when no envelope is present.
Message string
// Description is the optional long-form description from the envelope.
Description string
// RawBody is the response body verbatim. Useful for diagnostics when the
// envelope is unfamiliar.
RawBody []byte
// Cause is the underlying transport or decode error, if any. Use
// errors.Unwrap or errors.As to reach it.
Cause error
}
func (e *Error) Error() string {
switch {
case e.Code != "" && e.Message != "":
return fmt.Sprintf("loginradius: %d %s (%s)", e.StatusCode, e.Message, e.Code)
case e.Message != "":
return fmt.Sprintf("loginradius: %d %s", e.StatusCode, e.Message)
case e.Cause != nil:
return fmt.Sprintf("loginradius: %s", e.Cause.Error())
default:
return fmt.Sprintf("loginradius: http %d", e.StatusCode)
}
}
func (e *Error) Unwrap() error { return e.Cause }
// IsAuth reports whether the error is a 401.
// Authentication failed — missing or invalid credentials. Distinct from
// isForbidden: a 403 means the request authenticated but the principal
// isn't allowed to perform the operation.
func (e *Error) IsAuth() bool {
return e.StatusCode == http.StatusUnauthorized
}
// IsForbidden reports whether the error is a 403.
// Authenticated but not allowed. In LoginRadius this commonly indicates
// IP/domain access restrictions or a feature that isn't enabled on the
// tenant's plan, rather than a credential problem.
func (e *Error) IsForbidden() bool {
return e.StatusCode == http.StatusForbidden
}
// IsRateLimit reports whether the error is a 429.
// Rate limited by the API.
func (e *Error) IsRateLimit() bool {
return e.StatusCode == http.StatusTooManyRequests
}
// IsServer reports whether the status is in the 500–599 range.
// A 5xx from the API.
func (e *Error) IsServer() bool { return e.StatusCode >= 500 && e.StatusCode <= 599 }
// AsError converts an error from an SDK call into the facade's typed
// *loginradius.Error. Returns nil, false when err is nil, a transport
// error (DNS, TLS, timeout — anything not from the LoginRadius API), or
// any wrapper the SDK doesn't recognise.
//
// Use this in error-handling code to reach typed fields:
//
// resp, httpResp, err := client.Login.EmailByLoginUserNamePhone(ctx).Execute()
// if err != nil {
// if lrErr, ok := loginradius.AsError(err); ok {
// switch {
// case lrErr.IsAuth():
// case lrErr.IsForbidden():
// case lrErr.IsRateLimit():
// case lrErr.IsServer():
// }
// log.Printf("%d %s — %s", lrErr.StatusCode, lrErr.Code, lrErr.Description)
// }
// return err
// }
//
// If you already have the *http.Response from the SDK call, prefer
// AsErrorWithResponse — it gives a reliable status code. AsError parses
// status from the underlying error string ("403 Forbidden") as a fallback.
func AsError(err error) (*Error, bool) {
return AsErrorWithResponse(nil, err)
}
// AsErrorWithResponse is the AsError variant that uses the SDK's
// *http.Response directly for the status code, avoiding string-parsing.
// Pass httpResp = nil to fall back to AsError's behaviour.
func AsErrorWithResponse(httpResp *http.Response, err error) (*Error, bool) {
if err == nil {
return nil, false
}
// Already a *loginradius.Error somewhere in the chain — return it.
var existing *Error
if errors.As(err, &existing) {
return existing, true
}
// Wrap a *openapi.GenericOpenAPIError into a typed *Error.
var genErr *openapi.GenericOpenAPIError
if errors.As(err, &genErr) {
return wrapGenericOpenAPIError(httpResp, genErr), true
}
// Unknown error shape — caller should treat as a transport failure.
return nil, false
}
// wrapGenericOpenAPIError builds a *loginradius.Error from the generator's
// raw error. Tries the SDK-parsed Model() first, then the raw Body(), then a
// status-based default.
func wrapGenericOpenAPIError(httpResp *http.Response, genErr *openapi.GenericOpenAPIError) *Error {
out := &Error{Cause: genErr, Message: genErr.Error(), RawBody: genErr.Body()}
if httpResp != nil {
out.StatusCode = httpResp.StatusCode
} else {
out.StatusCode = parseStatusFromErrorString(genErr.Error())
}
// Two sources for envelope fields, in order of trust:
// 1. GenericOpenAPIError.Model() — where the spec defines a typed error
// response, the generator already decoded the body into one of the
// modelled schemas and stored it on the error. Cleanest source,
// because it is exactly what the spec says the shape is.
// 2. GenericOpenAPIError.Body() — for endpoints without typed error
// responses (or when the generator couldn't decode), parse the raw
// body via extractEnvelope, which tries each modelled schema in turn.
if env, ok := envelopeFromModel(genErr.Model()); ok {
out.applyEnvelope(env)
}
if out.Description == "" && out.Code == "" {
if env, ok := extractEnvelope(out.RawBody); ok {
out.applyEnvelope(env)
}
}
// Fallback Description for the cases the envelope didn't cover — empty
// body (common when an upstream gateway, WAF, or IP restriction blocks the
// request before the LoginRadius API layer emits an envelope), non-JSON
// body, or a shape we don't model. The status-based hint gives the caller
// something actionable instead of a bare "403 Forbidden".
if out.Description == "" && out.StatusCode != 0 {
out.Description = defaultDescriptionForStatus(out.StatusCode, out.RawBody)
}
return out
}
// parseStatusFromErrorString reads the generator's error string (typically the
// HTTP status line, e.g. "403 Forbidden") and returns the numeric status.
// Returns 0 when nothing 3-digit-ish is found.
func parseStatusFromErrorString(s string) int {
for _, tok := range strings.Fields(s) {
if n, err := strconv.Atoi(tok); err == nil && n >= 100 && n < 600 {
return n
}
}
return 0
}
func (e *Error) applyEnvelope(env errorEnvelope) {
if env.Message != "" {
e.Message = env.Message
} else if env.Description != "" {
// Some shapes omit Message and put the human-readable text in
// Description; surface that as the message rather than leave callers
// staring at the underlying transport string.
e.Message = env.Description
}
if env.Description != "" {
e.Description = env.Description
}
if env.Code != "" {
e.Code = env.Code
}
}
// errorEnvelope is the normalised LoginRadius error response.
type errorEnvelope struct {
Code string
Message string
Description string
}
func (e errorEnvelope) populated() bool {
return e.Code != "" || e.Message != "" || e.Description != ""
}
// envelopeFromModel pulls the envelope fields out of any decoded error model.
// Reflection is used because the generator emits a different concrete type per
// endpoint, all sharing roughly the same field names; the alias lists below
// come from the shared SDK manifest. Pointer (*string, *int32) and value fields are
// both handled.
func envelopeFromModel(model interface{}) (errorEnvelope, bool) {
if model == nil {
return errorEnvelope{}, false
}
v := reflect.ValueOf(model)
for v.Kind() == reflect.Ptr || v.Kind() == reflect.Interface {
if v.IsNil() {
return errorEnvelope{}, false
}
v = v.Elem()
}
if v.Kind() != reflect.Struct {
return errorEnvelope{}, false
}
env := errorEnvelope{
Code: firstNonEmptyField(v, "ErrorCode", "errorCode", "error_code", "Error", "error"),
Message: firstNonEmptyField(v, "Message", "message"),
Description: firstNonEmptyField(v, "Description", "description", "error_description", "ErrorDescription"),
}
if !env.populated() {
return errorEnvelope{}, false
}
return env, true
}
// extractEnvelope decodes a raw body against each error schema the spec
// declares, in the order the shared SDK manifest lists them, and returns the first
// that yields a populated envelope. Using the generated schemas means new
// fields on the LoginRadius error envelope land here for free on the next
// codegen run.
//
// Wire shapes currently modelled:
//
// - openapi.ApiError — The original LoginRadius shape, used by most endpoints.
// - openapi.ErrorResponse — Variant with an extra HTTP Code field.
// - openapi.ErrorResponseNative — camelCase fields used by /api/v2/access_token/*.
// - openapi.OAuthErrorResponse — { error, error_description } from OAuth token endpoints.
func extractEnvelope(body []byte) (errorEnvelope, bool) {
if len(body) == 0 {
return errorEnvelope{}, false
}
// Each candidate is decoded independently; a body that doesn't match a
// given shape simply leaves its fields empty and falls through.
candidates := []interface{}{
&openapi.ApiError{},
&openapi.ErrorResponse{},
&openapi.ErrorResponseNative{},
&openapi.OAuthErrorResponse{},
}
for _, candidate := range candidates {
if err := json.Unmarshal(body, candidate); err != nil {
continue
}
if env, ok := envelopeFromModel(candidate); ok {
return env, true
}
}
return errorEnvelope{}, false
}
// firstNonEmptyField returns the first non-empty value among the named struct
// fields, coerced to string. Handles *string, string, *int32, int32, *int64,
// int64. Returns "" when no field matches or all are empty.
func firstNonEmptyField(v reflect.Value, names ...string) string {
for _, name := range names {
f := v.FieldByName(name)
if !f.IsValid() {
continue
}
// Dereference pointer fields.
if f.Kind() == reflect.Ptr {
if f.IsNil() {
continue
}
f = f.Elem()
}
switch f.Kind() {
case reflect.String:
if s := f.String(); s != "" {
return s
}
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
n := f.Int()
if n != 0 {
return strconv.FormatInt(n, 10)
}
}
}
return ""
}
// defaultDescriptionForStatus supplies the operator-facing hint declared in
// the shared SDK manifest for statuses whose bodies commonly arrive empty.
func defaultDescriptionForStatus(status int, body []byte) string {
hint := ""
switch status {
case http.StatusUnauthorized:
hint = "authentication failed — verify API key / API secret / access token"
case http.StatusForbidden:
hint = "forbidden — typically an IP-access restriction, domain-access restriction, or a plan-level feature gate. Check the LoginRadius dashboard's security settings."
case http.StatusNotFound:
hint = "not found — verify the path and any IDs in the request"
case http.StatusTooManyRequests:
hint = "rate limited — back off and retry"
case http.StatusBadGateway, http.StatusServiceUnavailable, http.StatusGatewayTimeout:
hint = "transient upstream failure — retry with backoff"
}
// Append a body excerpt when present so the caller can see what the
// upstream actually said (often an HTML error page or a gateway message).
if trimmed := bodyExcerpt(body, 240); trimmed != "" {
if hint == "" {
return trimmed
}
return hint + " — body: " + trimmed
}
return hint
}
// bodyExcerpt returns a single-line snippet of body suitable for embedding in
// an error description.
func bodyExcerpt(body []byte, max int) string {
if len(body) == 0 {
return ""
}
// Collapse all whitespace runs to single spaces so the excerpt fits on
// one line.
trimmed := strings.Join(strings.Fields(string(body)), " ")
if len(trimmed) > max {
trimmed = trimmed[:max] + "…"
}
return trimmed
}