@atproto/lex-client 0.1.5 → 0.2.1
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 +25 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js.map +1 -1
- package/dist/client.d.ts +42 -21
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +97 -16
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +6 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +3 -3
- package/dist/errors.js.map +1 -1
- package/dist/response.d.ts +3 -2
- package/dist/response.d.ts.map +1 -1
- package/dist/response.js +2 -1
- package/dist/response.js.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +6 -2
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +25 -0
- package/dist/util.js.map +1 -1
- package/dist/write-operation-builder.d.ts +3 -2
- package/dist/write-operation-builder.d.ts.map +1 -1
- package/dist/write-operation-builder.js +2 -2
- package/dist/write-operation-builder.js.map +1 -1
- package/dist/xrpc.d.ts +8 -6
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +1 -1
- package/dist/xrpc.js.map +1 -1
- package/package.json +11 -14
- package/src/agent.test.ts +0 -216
- package/src/agent.ts +0 -186
- package/src/client.ts +0 -1086
- package/src/errors.test.ts +0 -626
- package/src/errors.ts +0 -570
- package/src/index.ts +0 -6
- package/src/lexicons/com/atproto/repo/applyWrites.defs.ts +0 -201
- package/src/lexicons/com/atproto/repo/applyWrites.ts +0 -6
- package/src/lexicons/com/atproto/repo/createRecord.defs.ts +0 -58
- package/src/lexicons/com/atproto/repo/createRecord.ts +0 -6
- package/src/lexicons/com/atproto/repo/defs.defs.ts +0 -28
- package/src/lexicons/com/atproto/repo/defs.ts +0 -5
- package/src/lexicons/com/atproto/repo/deleteRecord.defs.ts +0 -52
- package/src/lexicons/com/atproto/repo/deleteRecord.ts +0 -6
- package/src/lexicons/com/atproto/repo/getRecord.defs.ts +0 -37
- package/src/lexicons/com/atproto/repo/getRecord.ts +0 -6
- package/src/lexicons/com/atproto/repo/listRecords.defs.ts +0 -65
- package/src/lexicons/com/atproto/repo/listRecords.ts +0 -6
- package/src/lexicons/com/atproto/repo/putRecord.defs.ts +0 -59
- package/src/lexicons/com/atproto/repo/putRecord.ts +0 -6
- package/src/lexicons/com/atproto/repo/uploadBlob.defs.ts +0 -35
- package/src/lexicons/com/atproto/repo/uploadBlob.ts +0 -6
- package/src/lexicons/com/atproto/repo.ts +0 -12
- package/src/lexicons/com/atproto/sync/getBlob.defs.ts +0 -37
- package/src/lexicons/com/atproto/sync/getBlob.ts +0 -6
- package/src/lexicons/com/atproto/sync.ts +0 -5
- package/src/lexicons/com/atproto.ts +0 -6
- package/src/lexicons/com.ts +0 -5
- package/src/lexicons/index.ts +0 -5
- package/src/response.bench.ts +0 -113
- package/src/response.ts +0 -366
- package/src/types.ts +0 -71
- package/src/util.test.ts +0 -333
- package/src/util.ts +0 -182
- package/src/write-operation-builder.ts +0 -110
- package/src/www-authenticate.test.ts +0 -227
- package/src/www-authenticate.ts +0 -101
- package/src/xrpc.test.ts +0 -1450
- package/src/xrpc.ts +0 -446
- package/tsconfig.build.json +0 -12
- package/tsconfig.json +0 -7
- package/tsconfig.tests.json +0 -8
package/src/errors.test.ts
DELETED
|
@@ -1,626 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { IssueInvalidType, LexValidationError, l } from '@atproto/lex-schema'
|
|
3
|
-
import {
|
|
4
|
-
XrpcAuthenticationError,
|
|
5
|
-
XrpcFetchError,
|
|
6
|
-
XrpcInternalError,
|
|
7
|
-
XrpcInvalidResponseError,
|
|
8
|
-
XrpcResponseError,
|
|
9
|
-
XrpcResponseValidationError,
|
|
10
|
-
asXrpcFailure,
|
|
11
|
-
} from './errors.js'
|
|
12
|
-
|
|
13
|
-
// Minimal method fixture
|
|
14
|
-
const testQuery = l.query(
|
|
15
|
-
'io.example.test',
|
|
16
|
-
l.params(),
|
|
17
|
-
l.jsonPayload({ value: l.string() }),
|
|
18
|
-
['TestError', 'AnotherError'],
|
|
19
|
-
)
|
|
20
|
-
|
|
21
|
-
const testQueryNoErrors = l.query(
|
|
22
|
-
'io.example.noErrors',
|
|
23
|
-
l.params(),
|
|
24
|
-
l.jsonPayload({ value: l.string() }),
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
// ============================================================================
|
|
28
|
-
// XrpcResponseError
|
|
29
|
-
// ============================================================================
|
|
30
|
-
|
|
31
|
-
describe(XrpcResponseError, () => {
|
|
32
|
-
function createResponseError(
|
|
33
|
-
status: number,
|
|
34
|
-
errorCode: string,
|
|
35
|
-
message?: string,
|
|
36
|
-
) {
|
|
37
|
-
const response = new Response(null, { status })
|
|
38
|
-
return new XrpcResponseError(testQuery, response, {
|
|
39
|
-
encoding: 'application/json',
|
|
40
|
-
body: { error: errorCode, message },
|
|
41
|
-
})
|
|
42
|
-
}
|
|
43
|
-
|
|
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
|
-
})
|
|
176
|
-
})
|
|
177
|
-
|
|
178
|
-
it('exposes the response object', () => {
|
|
179
|
-
const response = new Response(null, {
|
|
180
|
-
status: 400,
|
|
181
|
-
headers: { 'X-Test': 'value' },
|
|
182
|
-
})
|
|
183
|
-
const err = new XrpcResponseError(testQuery, response, {
|
|
184
|
-
encoding: 'application/json',
|
|
185
|
-
body: { error: 'TestError' },
|
|
186
|
-
})
|
|
187
|
-
expect(err.reason).toBe(err)
|
|
188
|
-
expect(err.response.status).toBe(400)
|
|
189
|
-
expect(err.response.headers.get('X-Test')).toBe('value')
|
|
190
|
-
})
|
|
191
|
-
|
|
192
|
-
it('exposes body from the payload', () => {
|
|
193
|
-
const err = createResponseError(400, 'TestError', 'details')
|
|
194
|
-
expect(err.toJSON()).toEqual({ error: 'TestError', message: 'details' })
|
|
195
|
-
})
|
|
196
|
-
|
|
197
|
-
describe('toDownstreamError', () => {
|
|
198
|
-
it('returns 502 for upstream 500 errors', () => {
|
|
199
|
-
const err = createResponseError(
|
|
200
|
-
500,
|
|
201
|
-
'InternalServerError',
|
|
202
|
-
'Upstream crashed',
|
|
203
|
-
)
|
|
204
|
-
const downstream = err.toDownstreamError()
|
|
205
|
-
|
|
206
|
-
expect(downstream.status).toBe(502)
|
|
207
|
-
expect(downstream.body).toEqual({
|
|
208
|
-
error: 'InternalServerError',
|
|
209
|
-
message: 'Upstream crashed',
|
|
210
|
-
})
|
|
211
|
-
})
|
|
212
|
-
|
|
213
|
-
it('preserves original status for non-500 5xx errors', () => {
|
|
214
|
-
const err = createResponseError(503, 'ServiceUnavailable', 'Try later')
|
|
215
|
-
const downstream = err.toDownstreamError()
|
|
216
|
-
|
|
217
|
-
expect(downstream.status).toBe(503)
|
|
218
|
-
expect(downstream.body).toEqual({
|
|
219
|
-
error: 'ServiceUnavailable',
|
|
220
|
-
message: 'Try later',
|
|
221
|
-
})
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
it('preserves original status for 4xx errors', () => {
|
|
225
|
-
const err = createResponseError(404, 'NotFound', 'Record not found')
|
|
226
|
-
const downstream = err.toDownstreamError()
|
|
227
|
-
|
|
228
|
-
expect(downstream.status).toBe(404)
|
|
229
|
-
expect(downstream.body).toEqual({
|
|
230
|
-
error: 'NotFound',
|
|
231
|
-
message: 'Record not found',
|
|
232
|
-
})
|
|
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
|
-
})
|
|
272
|
-
})
|
|
273
|
-
|
|
274
|
-
describe('toJSON', () => {
|
|
275
|
-
it('returns the payload body for valid XRPC errors', () => {
|
|
276
|
-
const err = createResponseError(400, 'TestError', 'message')
|
|
277
|
-
expect(err.toJSON()).toEqual({ error: 'TestError', message: 'message' })
|
|
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
|
-
})
|
|
317
|
-
})
|
|
318
|
-
|
|
319
|
-
describe('matchesSchemaErrors', () => {
|
|
320
|
-
it('returns true when error matches method declared errors', () => {
|
|
321
|
-
const err = createResponseError(400, 'TestError')
|
|
322
|
-
expect(err.matchesSchemaErrors()).toBe(true)
|
|
323
|
-
})
|
|
324
|
-
|
|
325
|
-
it('returns false for undeclared error codes', () => {
|
|
326
|
-
const err = createResponseError(400, 'UnknownError')
|
|
327
|
-
expect(err.matchesSchemaErrors()).toBe(false)
|
|
328
|
-
})
|
|
329
|
-
|
|
330
|
-
it('returns false when method has no declared errors', () => {
|
|
331
|
-
const response = new Response(null, { status: 400 })
|
|
332
|
-
const err = new XrpcResponseError(testQueryNoErrors, response, {
|
|
333
|
-
encoding: 'application/json',
|
|
334
|
-
body: { error: 'SomeError' },
|
|
335
|
-
})
|
|
336
|
-
expect(err.matchesSchemaErrors()).toBe(false)
|
|
337
|
-
})
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
describe('shouldRetry', () => {
|
|
341
|
-
it('returns true for retryable status codes', () => {
|
|
342
|
-
expect(createResponseError(429, 'RateLimit').shouldRetry()).toBe(true)
|
|
343
|
-
expect(createResponseError(500, 'Internal').shouldRetry()).toBe(true)
|
|
344
|
-
expect(createResponseError(502, 'BadGateway').shouldRetry()).toBe(true)
|
|
345
|
-
expect(createResponseError(503, 'Unavailable').shouldRetry()).toBe(true)
|
|
346
|
-
})
|
|
347
|
-
|
|
348
|
-
it('returns false for non-retryable status codes', () => {
|
|
349
|
-
expect(createResponseError(400, 'BadRequest').shouldRetry()).toBe(false)
|
|
350
|
-
expect(createResponseError(401, 'Unauthorized').shouldRetry()).toBe(false)
|
|
351
|
-
expect(createResponseError(404, 'NotFound').shouldRetry()).toBe(false)
|
|
352
|
-
})
|
|
353
|
-
})
|
|
354
|
-
})
|
|
355
|
-
|
|
356
|
-
// ============================================================================
|
|
357
|
-
// XrpcAuthenticationError
|
|
358
|
-
// ============================================================================
|
|
359
|
-
|
|
360
|
-
describe(XrpcAuthenticationError, () => {
|
|
361
|
-
it('is never retryable', () => {
|
|
362
|
-
const response = new Response(null, { status: 401 })
|
|
363
|
-
const err = new XrpcAuthenticationError(testQuery, response, {
|
|
364
|
-
encoding: 'application/json',
|
|
365
|
-
body: { error: 'AuthenticationRequired' },
|
|
366
|
-
})
|
|
367
|
-
expect(err.shouldRetry()).toBe(false)
|
|
368
|
-
})
|
|
369
|
-
|
|
370
|
-
it('parses WWW-Authenticate header', () => {
|
|
371
|
-
const response = new Response(null, {
|
|
372
|
-
status: 401,
|
|
373
|
-
headers: {
|
|
374
|
-
'WWW-Authenticate': 'Bearer realm="api", error="InvalidToken"',
|
|
375
|
-
},
|
|
376
|
-
})
|
|
377
|
-
const err = new XrpcAuthenticationError(testQuery, response, {
|
|
378
|
-
encoding: 'application/json',
|
|
379
|
-
body: { error: 'AuthenticationRequired' },
|
|
380
|
-
})
|
|
381
|
-
expect(err.reason).toBe(err)
|
|
382
|
-
expect(err.wwwAuthenticate).toHaveProperty('Bearer')
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
it('returns empty object when no WWW-Authenticate header', () => {
|
|
386
|
-
const response = new Response(null, { status: 401 })
|
|
387
|
-
const err = new XrpcAuthenticationError(testQuery, response, {
|
|
388
|
-
encoding: 'application/json',
|
|
389
|
-
body: { error: 'AuthenticationRequired' },
|
|
390
|
-
})
|
|
391
|
-
expect(err.wwwAuthenticate).toEqual({})
|
|
392
|
-
})
|
|
393
|
-
|
|
394
|
-
it('toDownstreamError always returns 401', () => {
|
|
395
|
-
const response = new Response(null, { status: 401 })
|
|
396
|
-
const err = new XrpcAuthenticationError(testQuery, response, {
|
|
397
|
-
encoding: 'application/json',
|
|
398
|
-
body: { error: 'AuthenticationRequired', message: 'No token' },
|
|
399
|
-
})
|
|
400
|
-
const downstream = err.toDownstreamError()
|
|
401
|
-
|
|
402
|
-
expect(downstream.status).toBe(401)
|
|
403
|
-
expect(downstream.body).toEqual({
|
|
404
|
-
error: 'AuthenticationRequired',
|
|
405
|
-
message: 'No token',
|
|
406
|
-
})
|
|
407
|
-
})
|
|
408
|
-
})
|
|
409
|
-
|
|
410
|
-
// ============================================================================
|
|
411
|
-
// XrpcInvalidResponseError
|
|
412
|
-
// ============================================================================
|
|
413
|
-
|
|
414
|
-
describe(XrpcInvalidResponseError, () => {
|
|
415
|
-
it('has error code InvalidResponse', () => {
|
|
416
|
-
const response = new Response(null, { status: 399 })
|
|
417
|
-
const err = new XrpcInvalidResponseError(testQuery, response)
|
|
418
|
-
expect(err.reason).toBe(err)
|
|
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
|
-
})
|
|
427
|
-
})
|
|
428
|
-
|
|
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)
|
|
433
|
-
})
|
|
434
|
-
|
|
435
|
-
it('shouldRetry is true for retryable status codes', () => {
|
|
436
|
-
const response = new Response(null, { status: 502 })
|
|
437
|
-
const err = new XrpcInvalidResponseError(testQuery, response)
|
|
438
|
-
expect(err.shouldRetry()).toBe(true)
|
|
439
|
-
})
|
|
440
|
-
|
|
441
|
-
it('shouldRetry is false for non-retryable status codes', () => {
|
|
442
|
-
const response = new Response(null, { status: 400 })
|
|
443
|
-
const err = new XrpcInvalidResponseError(testQuery, response)
|
|
444
|
-
expect(err.shouldRetry()).toBe(false)
|
|
445
|
-
})
|
|
446
|
-
})
|
|
447
|
-
|
|
448
|
-
// ============================================================================
|
|
449
|
-
// XrpcResponseValidationError
|
|
450
|
-
// ============================================================================
|
|
451
|
-
|
|
452
|
-
describe(XrpcResponseValidationError, () => {
|
|
453
|
-
it('extends XrpcInvalidResponseError', () => {
|
|
454
|
-
const response = new Response(null, { status: 200 })
|
|
455
|
-
const validationError = new LexValidationError([
|
|
456
|
-
new IssueInvalidType([], 42, ['string']),
|
|
457
|
-
])
|
|
458
|
-
const err = new XrpcResponseValidationError(
|
|
459
|
-
testQuery,
|
|
460
|
-
response,
|
|
461
|
-
{ encoding: 'application/json', body: { value: 42 } },
|
|
462
|
-
validationError,
|
|
463
|
-
)
|
|
464
|
-
|
|
465
|
-
expect(err).toBeInstanceOf(XrpcInvalidResponseError)
|
|
466
|
-
expect(err.reason).toBe(err)
|
|
467
|
-
expect(err.error).toBe('InvalidResponse')
|
|
468
|
-
expect(err.cause).toBe(validationError)
|
|
469
|
-
})
|
|
470
|
-
|
|
471
|
-
it('includes validation error message', () => {
|
|
472
|
-
const validationError = new LexValidationError([
|
|
473
|
-
new IssueInvalidType([], 42, ['string']),
|
|
474
|
-
])
|
|
475
|
-
const err = new XrpcResponseValidationError(
|
|
476
|
-
testQuery,
|
|
477
|
-
new Response(null, { status: 200 }),
|
|
478
|
-
{ encoding: 'application/json', body: { value: 42 } },
|
|
479
|
-
validationError,
|
|
480
|
-
)
|
|
481
|
-
|
|
482
|
-
expect(err.message).toContain('Invalid response payload:')
|
|
483
|
-
expect(err.message).toContain(validationError.message)
|
|
484
|
-
})
|
|
485
|
-
|
|
486
|
-
it('toDownstreamError returns 502', () => {
|
|
487
|
-
const validationError = new LexValidationError([
|
|
488
|
-
new IssueInvalidType([], 42, ['string']),
|
|
489
|
-
])
|
|
490
|
-
const err = new XrpcResponseValidationError(
|
|
491
|
-
testQuery,
|
|
492
|
-
new Response(null, { status: 200 }),
|
|
493
|
-
{ encoding: 'application/json', body: { value: 42 } },
|
|
494
|
-
validationError,
|
|
495
|
-
)
|
|
496
|
-
const downstream = err.toDownstreamError()
|
|
497
|
-
expect(downstream.status).toBe(502)
|
|
498
|
-
})
|
|
499
|
-
})
|
|
500
|
-
|
|
501
|
-
// ============================================================================
|
|
502
|
-
// XrpcInternalError
|
|
503
|
-
// ============================================================================
|
|
504
|
-
|
|
505
|
-
describe(XrpcInternalError, () => {
|
|
506
|
-
it('has error code InternalServerError', () => {
|
|
507
|
-
const err = new XrpcInternalError(testQuery)
|
|
508
|
-
expect(err.reason).toBe(err)
|
|
509
|
-
expect(err.error).toBe('InternalServerError')
|
|
510
|
-
})
|
|
511
|
-
|
|
512
|
-
it('toJSON does not expose internal details', () => {
|
|
513
|
-
const err = new XrpcInternalError(
|
|
514
|
-
testQuery,
|
|
515
|
-
'Secret database connection string leaked',
|
|
516
|
-
)
|
|
517
|
-
const json = err.toJSON()
|
|
518
|
-
|
|
519
|
-
expect(json.error).toBe('InternalServerError')
|
|
520
|
-
expect(json.message).toBe('Internal Server Error')
|
|
521
|
-
expect(json.message).not.toContain('Secret')
|
|
522
|
-
})
|
|
523
|
-
|
|
524
|
-
it('toDownstreamError returns 500', () => {
|
|
525
|
-
const err = new XrpcInternalError(testQuery, 'internal details')
|
|
526
|
-
const downstream = err.toDownstreamError()
|
|
527
|
-
|
|
528
|
-
expect(downstream.status).toBe(500)
|
|
529
|
-
expect(downstream.body.error).toBe('InternalServerError')
|
|
530
|
-
expect(downstream.body.message).toBe('Internal Server Error')
|
|
531
|
-
})
|
|
532
|
-
|
|
533
|
-
it('is not retryable', () => {
|
|
534
|
-
const err = new XrpcInternalError(testQuery, 'something broke')
|
|
535
|
-
expect(err.shouldRetry()).toBe(false)
|
|
536
|
-
})
|
|
537
|
-
})
|
|
538
|
-
|
|
539
|
-
// ============================================================================
|
|
540
|
-
// XrpcFetchError
|
|
541
|
-
// ============================================================================
|
|
542
|
-
|
|
543
|
-
describe(XrpcFetchError, () => {
|
|
544
|
-
it('extends XrpcInternalError', () => {
|
|
545
|
-
const err = new XrpcFetchError(testQuery, new TypeError('fetch failed'))
|
|
546
|
-
expect(err).toBeInstanceOf(XrpcInternalError)
|
|
547
|
-
expect(err.error).toBe('InternalServerError')
|
|
548
|
-
})
|
|
549
|
-
|
|
550
|
-
it('uses cause message when cause is an Error', () => {
|
|
551
|
-
const cause = new TypeError('Failed to fetch')
|
|
552
|
-
const err = new XrpcFetchError(testQuery, cause)
|
|
553
|
-
expect(err.message).toBe('Unexpected fetchHandler() error: Failed to fetch')
|
|
554
|
-
expect(err.cause).toBe(cause)
|
|
555
|
-
})
|
|
556
|
-
|
|
557
|
-
it('uses fallback message when cause is not an Error', () => {
|
|
558
|
-
const err = new XrpcFetchError(testQuery, 'string cause')
|
|
559
|
-
expect(err.message).toBe('Unexpected fetchHandler() error: string cause')
|
|
560
|
-
expect(err.cause).toBe('string cause')
|
|
561
|
-
})
|
|
562
|
-
|
|
563
|
-
it('is retryable', () => {
|
|
564
|
-
const err = new XrpcFetchError(testQuery, new Error('network timeout'))
|
|
565
|
-
expect(err.shouldRetry()).toBe(true)
|
|
566
|
-
})
|
|
567
|
-
|
|
568
|
-
it('toJSON does not expose internal details', () => {
|
|
569
|
-
const err = new XrpcFetchError(
|
|
570
|
-
testQuery,
|
|
571
|
-
new Error('ECONNREFUSED 10.0.0.1:443'),
|
|
572
|
-
)
|
|
573
|
-
const json = err.toJSON()
|
|
574
|
-
|
|
575
|
-
expect(json.error).toBe('InternalServerError')
|
|
576
|
-
expect(json.message).toBe('Failed to perform upstream request')
|
|
577
|
-
expect(json.message).not.toContain('ECONNREFUSED')
|
|
578
|
-
})
|
|
579
|
-
|
|
580
|
-
it('toDownstreamError returns 502', () => {
|
|
581
|
-
const err = new XrpcFetchError(testQuery, new Error('DNS lookup failed'))
|
|
582
|
-
const downstream = err.toDownstreamError()
|
|
583
|
-
|
|
584
|
-
expect(downstream.status).toBe(502)
|
|
585
|
-
expect(downstream.body.error).toBe('InternalServerError')
|
|
586
|
-
expect(downstream.body.message).toBe('Failed to perform upstream request')
|
|
587
|
-
})
|
|
588
|
-
})
|
|
589
|
-
|
|
590
|
-
// ============================================================================
|
|
591
|
-
// asXrpcFailure
|
|
592
|
-
// ============================================================================
|
|
593
|
-
|
|
594
|
-
describe('asXrpcFailure', () => {
|
|
595
|
-
it('returns existing XrpcResponseError for the same method', () => {
|
|
596
|
-
const response = new Response(null, { status: 400 })
|
|
597
|
-
const err = new XrpcResponseError(testQuery, response, {
|
|
598
|
-
encoding: 'application/json',
|
|
599
|
-
body: { error: 'TestError' },
|
|
600
|
-
})
|
|
601
|
-
expect(asXrpcFailure(testQuery, err)).toBe(err)
|
|
602
|
-
})
|
|
603
|
-
|
|
604
|
-
it('wraps unknown errors in XrpcInternalError', () => {
|
|
605
|
-
const err = new TypeError('fetch failed')
|
|
606
|
-
const failure = asXrpcFailure(testQuery, err)
|
|
607
|
-
|
|
608
|
-
expect(failure).toBeInstanceOf(XrpcInternalError)
|
|
609
|
-
expect(failure.cause).toBe(err)
|
|
610
|
-
})
|
|
611
|
-
|
|
612
|
-
it('wraps XrpcError for a different method in XrpcInternalError', () => {
|
|
613
|
-
const otherQuery = l.query(
|
|
614
|
-
'io.example.other',
|
|
615
|
-
l.params(),
|
|
616
|
-
l.payload('application/json', l.object({ value: l.string() })),
|
|
617
|
-
)
|
|
618
|
-
const response = new Response(null, { status: 400 })
|
|
619
|
-
const err = new XrpcResponseError(otherQuery, response, {
|
|
620
|
-
encoding: 'application/json',
|
|
621
|
-
body: { error: 'TestError' },
|
|
622
|
-
})
|
|
623
|
-
const failure = asXrpcFailure(testQuery, err)
|
|
624
|
-
expect(failure).toBeInstanceOf(XrpcInternalError)
|
|
625
|
-
})
|
|
626
|
-
})
|