@atproto/lex-client 0.0.17 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -0
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +18 -19
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +62 -37
- package/dist/errors.js.map +1 -1
- package/dist/response.d.ts +7 -3
- package/dist/response.d.ts.map +1 -1
- package/dist/response.js +37 -35
- package/dist/response.js.map +1 -1
- package/dist/xrpc.js +1 -1
- package/dist/xrpc.js.map +1 -1
- package/package.json +3 -3
- package/src/client.ts +2 -0
- package/src/errors.test.ts +243 -32
- package/src/errors.ts +86 -49
- package/src/response.ts +55 -51
- package/src/xrpc.test.ts +76 -76
- package/src/xrpc.ts +1 -3
package/src/errors.test.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
XrpcInternalError,
|
|
7
7
|
XrpcInvalidResponseError,
|
|
8
8
|
XrpcResponseError,
|
|
9
|
-
|
|
9
|
+
XrpcResponseValidationError,
|
|
10
10
|
asXrpcFailure,
|
|
11
11
|
} from './errors.js'
|
|
12
12
|
|
|
@@ -41,13 +41,141 @@ describe(XrpcResponseError, () => {
|
|
|
41
41
|
})
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
describe('StatusErrorCodes mapping for non-XRPC responses', () => {
|
|
45
|
+
it('maps 400 to InvalidRequest', () => {
|
|
46
|
+
const err = new XrpcResponseError(
|
|
47
|
+
testQuery,
|
|
48
|
+
new Response(null, { status: 400 }),
|
|
49
|
+
)
|
|
50
|
+
expect(err.error).toBe('InvalidRequest')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('maps 401 to AuthenticationRequired', () => {
|
|
54
|
+
const err = new XrpcResponseError(
|
|
55
|
+
testQuery,
|
|
56
|
+
new Response(null, { status: 401 }),
|
|
57
|
+
)
|
|
58
|
+
expect(err.error).toBe('AuthenticationRequired')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('maps 403 to Forbidden', () => {
|
|
62
|
+
const err = new XrpcResponseError(
|
|
63
|
+
testQuery,
|
|
64
|
+
new Response(null, { status: 403 }),
|
|
65
|
+
)
|
|
66
|
+
expect(err.error).toBe('Forbidden')
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('maps 404 to XRPCNotSupported', () => {
|
|
70
|
+
const err = new XrpcResponseError(
|
|
71
|
+
testQuery,
|
|
72
|
+
new Response(null, { status: 404 }),
|
|
73
|
+
)
|
|
74
|
+
expect(err.error).toBe('XRPCNotSupported')
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('maps 406 to NotAcceptable', () => {
|
|
78
|
+
const err = new XrpcResponseError(
|
|
79
|
+
testQuery,
|
|
80
|
+
new Response(null, { status: 406 }),
|
|
81
|
+
)
|
|
82
|
+
expect(err.error).toBe('NotAcceptable')
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
it('maps 413 to PayloadTooLarge', () => {
|
|
86
|
+
const err = new XrpcResponseError(
|
|
87
|
+
testQuery,
|
|
88
|
+
new Response(null, { status: 413 }),
|
|
89
|
+
)
|
|
90
|
+
expect(err.error).toBe('PayloadTooLarge')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('maps 415 to UnsupportedMediaType', () => {
|
|
94
|
+
const err = new XrpcResponseError(
|
|
95
|
+
testQuery,
|
|
96
|
+
new Response(null, { status: 415 }),
|
|
97
|
+
)
|
|
98
|
+
expect(err.error).toBe('UnsupportedMediaType')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('maps 429 to RateLimitExceeded', () => {
|
|
102
|
+
const err = new XrpcResponseError(
|
|
103
|
+
testQuery,
|
|
104
|
+
new Response(null, { status: 429 }),
|
|
105
|
+
)
|
|
106
|
+
expect(err.error).toBe('RateLimitExceeded')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('maps 500 to InternalServerError', () => {
|
|
110
|
+
const err = new XrpcResponseError(
|
|
111
|
+
testQuery,
|
|
112
|
+
new Response(null, { status: 500 }),
|
|
113
|
+
)
|
|
114
|
+
expect(err.error).toBe('InternalServerError')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('maps 501 to MethodNotImplemented', () => {
|
|
118
|
+
const err = new XrpcResponseError(
|
|
119
|
+
testQuery,
|
|
120
|
+
new Response(null, { status: 501 }),
|
|
121
|
+
)
|
|
122
|
+
expect(err.error).toBe('MethodNotImplemented')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('maps 502 to UpstreamFailure', () => {
|
|
126
|
+
const err = new XrpcResponseError(
|
|
127
|
+
testQuery,
|
|
128
|
+
new Response(null, { status: 502 }),
|
|
129
|
+
)
|
|
130
|
+
expect(err.error).toBe('UpstreamFailure')
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it('maps 503 to NotEnoughResources', () => {
|
|
134
|
+
const err = new XrpcResponseError(
|
|
135
|
+
testQuery,
|
|
136
|
+
new Response(null, { status: 503 }),
|
|
137
|
+
)
|
|
138
|
+
expect(err.error).toBe('NotEnoughResources')
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
it('maps 504 to UpstreamTimeout', () => {
|
|
142
|
+
const err = new XrpcResponseError(
|
|
143
|
+
testQuery,
|
|
144
|
+
new Response(null, { status: 504 }),
|
|
145
|
+
)
|
|
146
|
+
expect(err.error).toBe('UpstreamTimeout')
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('defaults to InvalidRequest for unmapped 4xx status codes', () => {
|
|
150
|
+
const err = new XrpcResponseError(
|
|
151
|
+
testQuery,
|
|
152
|
+
new Response(null, { status: 418 }),
|
|
153
|
+
)
|
|
154
|
+
expect(err.error).toBe('InvalidRequest')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('defaults to UpstreamFailure for unmapped 5xx status codes', () => {
|
|
158
|
+
const err = new XrpcResponseError(
|
|
159
|
+
testQuery,
|
|
160
|
+
new Response(null, { status: 599 }),
|
|
161
|
+
)
|
|
162
|
+
expect(err.error).toBe('UpstreamFailure')
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
it('uses error from valid XRPC payload instead of status code mapping', () => {
|
|
166
|
+
const err = new XrpcResponseError(
|
|
167
|
+
testQuery,
|
|
168
|
+
new Response(null, { status: 400 }),
|
|
169
|
+
{
|
|
170
|
+
encoding: 'application/json',
|
|
171
|
+
body: { error: 'CustomError', message: 'Custom message' },
|
|
172
|
+
},
|
|
173
|
+
)
|
|
174
|
+
expect(err.error).toBe('CustomError')
|
|
175
|
+
})
|
|
48
176
|
})
|
|
49
177
|
|
|
50
|
-
it('exposes
|
|
178
|
+
it('exposes the response object', () => {
|
|
51
179
|
const response = new Response(null, {
|
|
52
180
|
status: 400,
|
|
53
181
|
headers: { 'X-Test': 'value' },
|
|
@@ -57,12 +185,13 @@ describe(XrpcResponseError, () => {
|
|
|
57
185
|
body: { error: 'TestError' },
|
|
58
186
|
})
|
|
59
187
|
expect(err.reason).toBe(err)
|
|
60
|
-
expect(err.
|
|
188
|
+
expect(err.response.status).toBe(400)
|
|
189
|
+
expect(err.response.headers.get('X-Test')).toBe('value')
|
|
61
190
|
})
|
|
62
191
|
|
|
63
192
|
it('exposes body from the payload', () => {
|
|
64
193
|
const err = createResponseError(400, 'TestError', 'details')
|
|
65
|
-
expect(err.
|
|
194
|
+
expect(err.toJSON()).toEqual({ error: 'TestError', message: 'details' })
|
|
66
195
|
})
|
|
67
196
|
|
|
68
197
|
describe('toDownstreamError', () => {
|
|
@@ -102,13 +231,89 @@ describe(XrpcResponseError, () => {
|
|
|
102
231
|
message: 'Record not found',
|
|
103
232
|
})
|
|
104
233
|
})
|
|
234
|
+
|
|
235
|
+
it('preserves 429 status for rate limiting', () => {
|
|
236
|
+
const err = new XrpcResponseError(
|
|
237
|
+
testQuery,
|
|
238
|
+
new Response(null, { status: 429 }),
|
|
239
|
+
)
|
|
240
|
+
expect(err.toDownstreamError().status).toBe(429)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('converts 500 to 502', () => {
|
|
244
|
+
const err = new XrpcResponseError(
|
|
245
|
+
testQuery,
|
|
246
|
+
new Response(null, { status: 500 }),
|
|
247
|
+
)
|
|
248
|
+
expect(err.toDownstreamError().status).toBe(502)
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('strips hop-by-hop headers', () => {
|
|
252
|
+
const response = new Response(null, {
|
|
253
|
+
status: 400,
|
|
254
|
+
headers: {
|
|
255
|
+
'Content-Type': 'application/json',
|
|
256
|
+
Connection: 'keep-alive',
|
|
257
|
+
'Keep-Alive': 'timeout=5',
|
|
258
|
+
'Transfer-Encoding': 'chunked',
|
|
259
|
+
},
|
|
260
|
+
})
|
|
261
|
+
const err = new XrpcResponseError(testQuery, response, {
|
|
262
|
+
encoding: 'application/json',
|
|
263
|
+
body: { error: 'TestError' },
|
|
264
|
+
})
|
|
265
|
+
const downstream = err.toDownstreamError()
|
|
266
|
+
|
|
267
|
+
expect(downstream.headers?.has('Content-Type')).toBe(true)
|
|
268
|
+
expect(downstream.headers?.has('Connection')).toBe(false)
|
|
269
|
+
expect(downstream.headers?.has('Keep-Alive')).toBe(false)
|
|
270
|
+
expect(downstream.headers?.has('Transfer-Encoding')).toBe(false)
|
|
271
|
+
})
|
|
105
272
|
})
|
|
106
273
|
|
|
107
274
|
describe('toJSON', () => {
|
|
108
|
-
it('returns the payload body', () => {
|
|
275
|
+
it('returns the payload body for valid XRPC errors', () => {
|
|
109
276
|
const err = createResponseError(400, 'TestError', 'message')
|
|
110
277
|
expect(err.toJSON()).toEqual({ error: 'TestError', message: 'message' })
|
|
111
278
|
})
|
|
279
|
+
|
|
280
|
+
it('constructs XRPC error from status code when payload is not valid XRPC', () => {
|
|
281
|
+
const err = new XrpcResponseError(
|
|
282
|
+
testQuery,
|
|
283
|
+
new Response(null, { status: 429 }),
|
|
284
|
+
{ encoding: 'text/plain', body: 'Rate limit exceeded' },
|
|
285
|
+
)
|
|
286
|
+
expect(err.toJSON()).toEqual({
|
|
287
|
+
error: 'RateLimitExceeded',
|
|
288
|
+
message: 'Upstream server responded with a 429 error',
|
|
289
|
+
})
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('constructs XRPC error from status code when payload is missing', () => {
|
|
293
|
+
const err = new XrpcResponseError(
|
|
294
|
+
testQuery,
|
|
295
|
+
new Response(null, { status: 503 }),
|
|
296
|
+
)
|
|
297
|
+
expect(err.toJSON()).toEqual({
|
|
298
|
+
error: 'NotEnoughResources',
|
|
299
|
+
message: 'Upstream server responded with a 503 error',
|
|
300
|
+
})
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
it('returns valid XRPC payload unchanged', () => {
|
|
304
|
+
const err = new XrpcResponseError(
|
|
305
|
+
testQuery,
|
|
306
|
+
new Response(null, { status: 400 }),
|
|
307
|
+
{
|
|
308
|
+
encoding: 'application/json',
|
|
309
|
+
body: { error: 'CustomError', message: 'Custom message' },
|
|
310
|
+
},
|
|
311
|
+
)
|
|
312
|
+
expect(err.toJSON()).toEqual({
|
|
313
|
+
error: 'CustomError',
|
|
314
|
+
message: 'Custom message',
|
|
315
|
+
})
|
|
316
|
+
})
|
|
112
317
|
})
|
|
113
318
|
|
|
114
319
|
describe('matchesSchemaErrors', () => {
|
|
@@ -203,57 +408,63 @@ describe(XrpcAuthenticationError, () => {
|
|
|
203
408
|
})
|
|
204
409
|
|
|
205
410
|
// ============================================================================
|
|
206
|
-
//
|
|
411
|
+
// XrpcInvalidResponseError
|
|
207
412
|
// ============================================================================
|
|
208
413
|
|
|
209
|
-
describe(
|
|
210
|
-
it('has error code
|
|
211
|
-
const response = new Response(null, { status:
|
|
212
|
-
const err = new
|
|
414
|
+
describe(XrpcInvalidResponseError, () => {
|
|
415
|
+
it('has error code InvalidResponse', () => {
|
|
416
|
+
const response = new Response(null, { status: 399 })
|
|
417
|
+
const err = new XrpcInvalidResponseError(testQuery, response)
|
|
213
418
|
expect(err.reason).toBe(err)
|
|
214
|
-
expect(err.error).toBe('
|
|
419
|
+
expect(err.error).toBe('InvalidResponse')
|
|
420
|
+
expect(err.toDownstreamError()).toMatchObject({
|
|
421
|
+
status: 502,
|
|
422
|
+
body: {
|
|
423
|
+
error: 'InvalidResponse',
|
|
424
|
+
message: 'Upstream server responded with an invalid status code (399)',
|
|
425
|
+
},
|
|
426
|
+
})
|
|
215
427
|
})
|
|
216
428
|
|
|
217
|
-
it('toDownstreamError returns 502', () => {
|
|
218
|
-
const response = new Response(null, { status:
|
|
219
|
-
const err = new
|
|
220
|
-
|
|
221
|
-
expect(downstream.status).toBe(502)
|
|
429
|
+
it('toDownstreamError returns 502 for 500 upstream errors', () => {
|
|
430
|
+
const response = new Response(null, { status: 500 })
|
|
431
|
+
const err = new XrpcInvalidResponseError(testQuery, response)
|
|
432
|
+
expect(err.toDownstreamError().status).toBe(502)
|
|
222
433
|
})
|
|
223
434
|
|
|
224
435
|
it('shouldRetry is true for retryable status codes', () => {
|
|
225
436
|
const response = new Response(null, { status: 502 })
|
|
226
|
-
const err = new
|
|
437
|
+
const err = new XrpcInvalidResponseError(testQuery, response)
|
|
227
438
|
expect(err.shouldRetry()).toBe(true)
|
|
228
439
|
})
|
|
229
440
|
|
|
230
441
|
it('shouldRetry is false for non-retryable status codes', () => {
|
|
231
|
-
const response = new Response(null, { status:
|
|
232
|
-
const err = new
|
|
442
|
+
const response = new Response(null, { status: 400 })
|
|
443
|
+
const err = new XrpcInvalidResponseError(testQuery, response)
|
|
233
444
|
expect(err.shouldRetry()).toBe(false)
|
|
234
445
|
})
|
|
235
446
|
})
|
|
236
447
|
|
|
237
448
|
// ============================================================================
|
|
238
|
-
//
|
|
449
|
+
// XrpcResponseValidationError
|
|
239
450
|
// ============================================================================
|
|
240
451
|
|
|
241
|
-
describe(
|
|
242
|
-
it('extends
|
|
452
|
+
describe(XrpcResponseValidationError, () => {
|
|
453
|
+
it('extends XrpcInvalidResponseError', () => {
|
|
243
454
|
const response = new Response(null, { status: 200 })
|
|
244
455
|
const validationError = new LexValidationError([
|
|
245
456
|
new IssueInvalidType([], 42, ['string']),
|
|
246
457
|
])
|
|
247
|
-
const err = new
|
|
458
|
+
const err = new XrpcResponseValidationError(
|
|
248
459
|
testQuery,
|
|
249
460
|
response,
|
|
250
461
|
{ encoding: 'application/json', body: { value: 42 } },
|
|
251
462
|
validationError,
|
|
252
463
|
)
|
|
253
464
|
|
|
254
|
-
expect(err).toBeInstanceOf(
|
|
465
|
+
expect(err).toBeInstanceOf(XrpcInvalidResponseError)
|
|
255
466
|
expect(err.reason).toBe(err)
|
|
256
|
-
expect(err.error).toBe('
|
|
467
|
+
expect(err.error).toBe('InvalidResponse')
|
|
257
468
|
expect(err.cause).toBe(validationError)
|
|
258
469
|
})
|
|
259
470
|
|
|
@@ -261,14 +472,14 @@ describe(XrpcInvalidResponseError, () => {
|
|
|
261
472
|
const validationError = new LexValidationError([
|
|
262
473
|
new IssueInvalidType([], 42, ['string']),
|
|
263
474
|
])
|
|
264
|
-
const err = new
|
|
475
|
+
const err = new XrpcResponseValidationError(
|
|
265
476
|
testQuery,
|
|
266
477
|
new Response(null, { status: 200 }),
|
|
267
478
|
{ encoding: 'application/json', body: { value: 42 } },
|
|
268
479
|
validationError,
|
|
269
480
|
)
|
|
270
481
|
|
|
271
|
-
expect(err.message).toContain('Invalid response:')
|
|
482
|
+
expect(err.message).toContain('Invalid response payload:')
|
|
272
483
|
expect(err.message).toContain(validationError.message)
|
|
273
484
|
})
|
|
274
485
|
|
|
@@ -276,7 +487,7 @@ describe(XrpcInvalidResponseError, () => {
|
|
|
276
487
|
const validationError = new LexValidationError([
|
|
277
488
|
new IssueInvalidType([], 42, ['string']),
|
|
278
489
|
])
|
|
279
|
-
const err = new
|
|
490
|
+
const err = new XrpcResponseValidationError(
|
|
280
491
|
testQuery,
|
|
281
492
|
new Response(null, { status: 200 }),
|
|
282
493
|
{ encoding: 'application/json', body: { value: 42 } },
|
package/src/errors.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
LexError,
|
|
3
|
+
LexErrorCode,
|
|
4
|
+
LexErrorData,
|
|
5
|
+
LexValue,
|
|
6
|
+
} from '@atproto/lex-data'
|
|
2
7
|
import {
|
|
3
8
|
InferMethodError,
|
|
4
9
|
LexValidationError,
|
|
@@ -15,6 +20,28 @@ import {
|
|
|
15
20
|
parseWWWAuthenticateHeader,
|
|
16
21
|
} from './www-authenticate.js'
|
|
17
22
|
|
|
23
|
+
/**
|
|
24
|
+
* Mapping that allows generating an XRPC error code from an HTTP status code
|
|
25
|
+
* when the response does not contain a valid XRPC error payload. This is used
|
|
26
|
+
* to convert non-XRPC error responses from upstream servers into a standardized
|
|
27
|
+
* XRPC error for downstream clients.
|
|
28
|
+
*/
|
|
29
|
+
const StatusErrorCodes = new Map<number, LexErrorCode>([
|
|
30
|
+
[400, 'InvalidRequest'],
|
|
31
|
+
[401, 'AuthenticationRequired'],
|
|
32
|
+
[403, 'Forbidden'],
|
|
33
|
+
[404, 'XRPCNotSupported'],
|
|
34
|
+
[406, 'NotAcceptable'],
|
|
35
|
+
[413, 'PayloadTooLarge'],
|
|
36
|
+
[415, 'UnsupportedMediaType'],
|
|
37
|
+
[429, 'RateLimitExceeded'],
|
|
38
|
+
[500, 'InternalServerError'],
|
|
39
|
+
[501, 'MethodNotImplemented'],
|
|
40
|
+
[502, 'UpstreamFailure'],
|
|
41
|
+
[503, 'NotEnoughResources'],
|
|
42
|
+
[504, 'UpstreamTimeout'],
|
|
43
|
+
])
|
|
44
|
+
|
|
18
45
|
export type { XrpcUnknownResponsePayload }
|
|
19
46
|
|
|
20
47
|
export type DownstreamError<N extends LexErrorCode = LexErrorCode> = {
|
|
@@ -90,7 +117,7 @@ export function isXrpcErrorPayload(
|
|
|
90
117
|
* @typeParam TReason - The reason type for ResultFailure
|
|
91
118
|
*
|
|
92
119
|
* @see {@link XrpcResponseError} - For valid XRPC error responses
|
|
93
|
-
* @see {@link
|
|
120
|
+
* @see {@link XrpcInvalidResponseError} - For invalid/unexpected responses
|
|
94
121
|
* @see {@link XrpcInternalError} - For network/internal errors
|
|
95
122
|
*/
|
|
96
123
|
export abstract class XrpcError<
|
|
@@ -160,17 +187,23 @@ export abstract class XrpcError<
|
|
|
160
187
|
*/
|
|
161
188
|
export class XrpcResponseError<
|
|
162
189
|
M extends Procedure | Query = Procedure | Query,
|
|
163
|
-
|
|
164
|
-
> extends XrpcError<M, N, XrpcResponseError<M, N>> {
|
|
190
|
+
> extends XrpcError<M, LexErrorCode, XrpcResponseError<M>> {
|
|
165
191
|
name = 'XrpcResponseError'
|
|
166
192
|
|
|
167
193
|
constructor(
|
|
168
194
|
method: M,
|
|
169
195
|
readonly response: Response,
|
|
170
|
-
readonly payload
|
|
196
|
+
readonly payload?: XrpcUnknownResponsePayload,
|
|
171
197
|
options?: ErrorOptions,
|
|
172
198
|
) {
|
|
173
|
-
const { error, message } = payload
|
|
199
|
+
const { error, message } = isXrpcErrorPayload(payload)
|
|
200
|
+
? payload.body
|
|
201
|
+
: {
|
|
202
|
+
error:
|
|
203
|
+
StatusErrorCodes.get(response.status) ??
|
|
204
|
+
(response.status >= 500 ? 'UpstreamFailure' : 'InvalidRequest'),
|
|
205
|
+
message: buildResponseOverviewMessage(response),
|
|
206
|
+
}
|
|
174
207
|
super(method, error, message, options)
|
|
175
208
|
}
|
|
176
209
|
|
|
@@ -182,19 +215,27 @@ export class XrpcResponseError<
|
|
|
182
215
|
return RETRYABLE_HTTP_STATUS_CODES.has(this.response.status)
|
|
183
216
|
}
|
|
184
217
|
|
|
185
|
-
override toJSON(): LexErrorData
|
|
186
|
-
|
|
218
|
+
override toJSON(): LexErrorData {
|
|
219
|
+
// Return the original error payload if it's a valid XRPC error, otherwise
|
|
220
|
+
// convert to an XRPC error format.
|
|
221
|
+
const { payload } = this
|
|
222
|
+
if (isXrpcErrorPayload(payload)) {
|
|
223
|
+
return payload.body
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return super.toJSON()
|
|
187
227
|
}
|
|
188
228
|
|
|
189
229
|
override toDownstreamError(): DownstreamError {
|
|
190
|
-
|
|
230
|
+
const { status, headers } = this.response
|
|
231
|
+
// If the upstream server returned a 500 error, we want to return a 502 Bad
|
|
191
232
|
// Gateway to downstream clients, as the issue is with the upstream server,
|
|
192
233
|
// not us. We still return the original error code and message in the body
|
|
193
234
|
// for transparency, but we do not want to expose internal server errors
|
|
194
235
|
// from the upstream server as-is to downstream clients.
|
|
195
236
|
return {
|
|
196
|
-
status:
|
|
197
|
-
headers: stripHopByHopHeaders(
|
|
237
|
+
status: status === 500 ? 502 : status,
|
|
238
|
+
headers: stripHopByHopHeaders(headers),
|
|
198
239
|
body: this.toJSON(),
|
|
199
240
|
}
|
|
200
241
|
}
|
|
@@ -207,8 +248,8 @@ export class XrpcResponseError<
|
|
|
207
248
|
return this.response.headers
|
|
208
249
|
}
|
|
209
250
|
|
|
210
|
-
get body():
|
|
211
|
-
return this.payload
|
|
251
|
+
get body(): undefined | Uint8Array | LexValue {
|
|
252
|
+
return this.payload?.body
|
|
212
253
|
}
|
|
213
254
|
}
|
|
214
255
|
|
|
@@ -242,8 +283,7 @@ export type { WWWAuthenticate }
|
|
|
242
283
|
*/
|
|
243
284
|
export class XrpcAuthenticationError<
|
|
244
285
|
M extends Procedure | Query = Procedure | Query,
|
|
245
|
-
|
|
246
|
-
> extends XrpcResponseError<M, N> {
|
|
286
|
+
> extends XrpcResponseError<M> {
|
|
247
287
|
name = 'XrpcAuthenticationError'
|
|
248
288
|
|
|
249
289
|
override shouldRetry(): boolean {
|
|
@@ -261,14 +301,6 @@ export class XrpcAuthenticationError<
|
|
|
261
301
|
this.response.headers.get('www-authenticate'),
|
|
262
302
|
) ?? {})
|
|
263
303
|
}
|
|
264
|
-
|
|
265
|
-
override toDownstreamError(): DownstreamError {
|
|
266
|
-
return {
|
|
267
|
-
status: 401,
|
|
268
|
-
headers: stripHopByHopHeaders(this.headers),
|
|
269
|
-
body: this.toJSON(),
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
304
|
}
|
|
273
305
|
|
|
274
306
|
/**
|
|
@@ -281,24 +313,25 @@ export class XrpcAuthenticationError<
|
|
|
281
313
|
* - Non-JSON error responses
|
|
282
314
|
* - Responses from non-XRPC endpoints
|
|
283
315
|
*
|
|
284
|
-
* The error code is always '
|
|
285
|
-
* when converted to a response.
|
|
316
|
+
* The error code is always 'InvalidResponse' and maps to HTTP 502 Bad Gateway
|
|
317
|
+
* when converted to a response. This should allow downstream clients to
|
|
318
|
+
* determine at which boundary the error occurred.
|
|
286
319
|
*
|
|
287
320
|
* @typeParam M - The XRPC method type
|
|
288
321
|
*/
|
|
289
|
-
export class
|
|
322
|
+
export class XrpcInvalidResponseError<
|
|
290
323
|
M extends Procedure | Query = Procedure | Query,
|
|
291
|
-
> extends XrpcError<M, '
|
|
292
|
-
name = '
|
|
324
|
+
> extends XrpcError<M, 'InvalidResponse', XrpcInvalidResponseError<M>> {
|
|
325
|
+
name = 'XrpcInvalidResponseError'
|
|
293
326
|
|
|
294
327
|
constructor(
|
|
295
328
|
method: M,
|
|
296
329
|
readonly response: Response,
|
|
297
|
-
readonly payload
|
|
298
|
-
message: string =
|
|
330
|
+
readonly payload?: XrpcUnknownResponsePayload,
|
|
331
|
+
message: string = buildResponseOverviewMessage(response),
|
|
299
332
|
options?: ErrorOptions,
|
|
300
333
|
) {
|
|
301
|
-
super(method, '
|
|
334
|
+
super(method, 'InvalidResponse', message, options)
|
|
302
335
|
}
|
|
303
336
|
|
|
304
337
|
override get reason(): this {
|
|
@@ -317,7 +350,7 @@ export class XrpcUpstreamError<
|
|
|
317
350
|
/**
|
|
318
351
|
* Error class for invalid XRPC responses that fail schema validation.
|
|
319
352
|
*
|
|
320
|
-
* This is a specific type of {@link
|
|
353
|
+
* This is a specific type of {@link XrpcInvalidResponseError} that indicates the
|
|
321
354
|
* upstream server returned a response that was structurally valid but did not
|
|
322
355
|
* conform to the expected schema for the method. This likely indicates a
|
|
323
356
|
* mismatch between client and server versions or an issue with the server's
|
|
@@ -325,10 +358,10 @@ export class XrpcUpstreamError<
|
|
|
325
358
|
*
|
|
326
359
|
* @typeParam M - The XRPC method type
|
|
327
360
|
*/
|
|
328
|
-
export class
|
|
361
|
+
export class XrpcResponseValidationError<
|
|
329
362
|
M extends Procedure | Query = Procedure | Query,
|
|
330
|
-
> extends
|
|
331
|
-
name = '
|
|
363
|
+
> extends XrpcInvalidResponseError<M> {
|
|
364
|
+
name = 'XrpcResponseValidationError'
|
|
332
365
|
|
|
333
366
|
constructor(
|
|
334
367
|
method: M,
|
|
@@ -336,17 +369,13 @@ export class XrpcInvalidResponseError<
|
|
|
336
369
|
payload: XrpcUnknownResponsePayload,
|
|
337
370
|
readonly cause: LexValidationError,
|
|
338
371
|
) {
|
|
339
|
-
super(
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
// ("they" are at fault). We are using 502 here to allow downstream clients
|
|
347
|
-
// to determine that the issue lies at the interface between us and the
|
|
348
|
-
// upstream server, rather than an issue with our internal processing.
|
|
349
|
-
return { status: 502, body: this.toJSON() }
|
|
372
|
+
super(
|
|
373
|
+
method,
|
|
374
|
+
response,
|
|
375
|
+
payload,
|
|
376
|
+
`Invalid response payload: ${cause.message}`,
|
|
377
|
+
{ cause },
|
|
378
|
+
)
|
|
350
379
|
}
|
|
351
380
|
}
|
|
352
381
|
|
|
@@ -448,7 +477,7 @@ export class XrpcFetchError<
|
|
|
448
477
|
* if (result.success) {
|
|
449
478
|
* console.log(result.body) // XrpcResponse
|
|
450
479
|
* } else {
|
|
451
|
-
* // result is XrpcFailure (XrpcResponseError |
|
|
480
|
+
* // result is XrpcFailure (XrpcResponseError | XrpcInvalidResponseError | XrpcInternalError)
|
|
452
481
|
* console.error(result.error, result.message)
|
|
453
482
|
* }
|
|
454
483
|
* ```
|
|
@@ -457,7 +486,7 @@ export type XrpcFailure<M extends Procedure | Query = Procedure | Query> =
|
|
|
457
486
|
// The server returned a valid XRPC error response
|
|
458
487
|
| XrpcResponseError<M>
|
|
459
488
|
// The response was not a valid XRPC response, or it does not match the schema
|
|
460
|
-
|
|
|
489
|
+
| XrpcInvalidResponseError<M>
|
|
461
490
|
// Something went wrong (network error, etc.)
|
|
462
491
|
| XrpcInternalError<M>
|
|
463
492
|
|
|
@@ -487,7 +516,7 @@ export function asXrpcFailure<M extends Procedure | Query>(
|
|
|
487
516
|
): XrpcFailure<M> {
|
|
488
517
|
if (
|
|
489
518
|
cause instanceof XrpcResponseError ||
|
|
490
|
-
cause instanceof
|
|
519
|
+
cause instanceof XrpcInvalidResponseError ||
|
|
491
520
|
cause instanceof XrpcInternalError
|
|
492
521
|
) {
|
|
493
522
|
if (cause.method === method) return cause
|
|
@@ -531,3 +560,11 @@ function stripHopByHopHeaders(headers: Headers): Headers {
|
|
|
531
560
|
|
|
532
561
|
return result
|
|
533
562
|
}
|
|
563
|
+
|
|
564
|
+
function buildResponseOverviewMessage(response: Response): string {
|
|
565
|
+
if (response.status < 400) {
|
|
566
|
+
return `Upstream server responded with an invalid status code (${response.status})`
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return `Upstream server responded with a ${response.status} error`
|
|
570
|
+
}
|