@atproto/lex-client 0.0.14 → 0.0.15
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 +15 -0
- package/dist/client.d.ts +2 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +3 -4
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +35 -7
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +94 -19
- package/dist/errors.js.map +1 -1
- package/dist/response.d.ts +1 -1
- package/dist/response.d.ts.map +1 -1
- package/dist/response.js +2 -2
- package/dist/response.js.map +1 -1
- package/package.json +6 -6
- package/src/client.ts +4 -4
- package/src/errors.test.ts +346 -0
- package/src/errors.ts +120 -19
- package/src/response.ts +4 -4
package/src/errors.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { LexError, LexErrorCode, LexErrorData } from '@atproto/lex-data'
|
|
2
2
|
import {
|
|
3
3
|
InferMethodError,
|
|
4
|
+
LexValidationError,
|
|
4
5
|
Procedure,
|
|
5
6
|
Query,
|
|
6
7
|
ResultFailure,
|
|
@@ -12,6 +13,13 @@ import {
|
|
|
12
13
|
parseWWWAuthenticateHeader,
|
|
13
14
|
} from './www-authenticate.js'
|
|
14
15
|
|
|
16
|
+
export type DownstreamError<N extends LexErrorCode = LexErrorCode> = {
|
|
17
|
+
status: number
|
|
18
|
+
headers?: Headers
|
|
19
|
+
encoding?: 'application/json'
|
|
20
|
+
body: LexErrorData<N>
|
|
21
|
+
}
|
|
22
|
+
|
|
15
23
|
/**
|
|
16
24
|
* HTTP status codes that indicate a transient error that may succeed on retry.
|
|
17
25
|
*
|
|
@@ -115,7 +123,9 @@ export abstract class XrpcError<
|
|
|
115
123
|
*/
|
|
116
124
|
abstract shouldRetry(): boolean
|
|
117
125
|
|
|
118
|
-
|
|
126
|
+
abstract toDownstreamError(): DownstreamError
|
|
127
|
+
|
|
128
|
+
matchesSchemaErrors(): this is XrpcError<M, InferMethodError<M>> {
|
|
119
129
|
return this.method.errors?.includes(this.error) ?? false
|
|
120
130
|
}
|
|
121
131
|
}
|
|
@@ -127,7 +137,7 @@ export abstract class XrpcError<
|
|
|
127
137
|
* a non-2xx status with a valid JSON error payload containing `error` and
|
|
128
138
|
* optional `message` fields.
|
|
129
139
|
*
|
|
130
|
-
* Use {@link
|
|
140
|
+
* Use {@link matchesSchemaErrors} to check if the error matches the method's declared
|
|
131
141
|
* error types for type-safe error handling.
|
|
132
142
|
*
|
|
133
143
|
* @typeParam M - The XRPC method type
|
|
@@ -168,25 +178,32 @@ export class XrpcResponseError<
|
|
|
168
178
|
return RETRYABLE_HTTP_STATUS_CODES.has(this.response.status)
|
|
169
179
|
}
|
|
170
180
|
|
|
171
|
-
override toJSON() {
|
|
181
|
+
override toJSON(): LexErrorData<N> {
|
|
172
182
|
return this.payload.body
|
|
173
183
|
}
|
|
174
184
|
|
|
175
|
-
override
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
185
|
+
override toDownstreamError(): DownstreamError {
|
|
186
|
+
// If the upstream server returned a 5xx error, we want to return a 502 Bad
|
|
187
|
+
// Gateway to downstream clients, as the issue is with the upstream server,
|
|
188
|
+
// not us. We still return the original error code and message in the body
|
|
189
|
+
// for transparency, but we do not want to expose internal server errors
|
|
190
|
+
// from the upstream server as-is to downstream clients.
|
|
191
|
+
return {
|
|
192
|
+
status: this.response.status === 500 ? 502 : this.status,
|
|
193
|
+
headers: stripHopByHopHeaders(this.headers),
|
|
194
|
+
body: this.toJSON(),
|
|
180
195
|
}
|
|
196
|
+
}
|
|
181
197
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
Response.json({ error: 'UpstreamFailure' }, { status: 502 })
|
|
185
|
-
: // If the error is on our side, return a generic internal server error
|
|
186
|
-
Response.json({ error: 'InternalServerError' }, { status: 500 })
|
|
198
|
+
get status(): number {
|
|
199
|
+
return this.response.status
|
|
187
200
|
}
|
|
188
201
|
|
|
189
|
-
get
|
|
202
|
+
get headers(): Headers {
|
|
203
|
+
return this.response.headers
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
get body(): LexErrorData<N> {
|
|
190
207
|
return this.payload.body
|
|
191
208
|
}
|
|
192
209
|
}
|
|
@@ -240,6 +257,14 @@ export class XrpcAuthenticationError<
|
|
|
240
257
|
this.response.headers.get('www-authenticate'),
|
|
241
258
|
) ?? {})
|
|
242
259
|
}
|
|
260
|
+
|
|
261
|
+
override toDownstreamError(): DownstreamError {
|
|
262
|
+
return {
|
|
263
|
+
status: 401,
|
|
264
|
+
headers: stripHopByHopHeaders(this.headers),
|
|
265
|
+
body: this.toJSON(),
|
|
266
|
+
}
|
|
267
|
+
}
|
|
243
268
|
}
|
|
244
269
|
|
|
245
270
|
/**
|
|
@@ -280,8 +305,44 @@ export class XrpcUpstreamError<
|
|
|
280
305
|
return RETRYABLE_HTTP_STATUS_CODES.has(this.response.status)
|
|
281
306
|
}
|
|
282
307
|
|
|
283
|
-
override
|
|
284
|
-
return
|
|
308
|
+
override toDownstreamError(): DownstreamError {
|
|
309
|
+
return { status: 502, body: this.toJSON() }
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Error class for invalid XRPC responses that fail schema validation.
|
|
315
|
+
*
|
|
316
|
+
* This is a specific type of {@link XrpcUpstreamError} that indicates the
|
|
317
|
+
* upstream server returned a response that was structurally valid but did not
|
|
318
|
+
* conform to the expected schema for the method. This likely indicates a
|
|
319
|
+
* mismatch between client and server versions or an issue with the server's
|
|
320
|
+
* XRPC implementation.
|
|
321
|
+
*
|
|
322
|
+
* @typeParam M - The XRPC method type
|
|
323
|
+
*/
|
|
324
|
+
export class XrpcInvalidResponseError<
|
|
325
|
+
M extends Procedure | Query = Procedure | Query,
|
|
326
|
+
> extends XrpcUpstreamError<M> {
|
|
327
|
+
name = 'XrpcInvalidResponseError'
|
|
328
|
+
|
|
329
|
+
constructor(
|
|
330
|
+
method: M,
|
|
331
|
+
response: Response,
|
|
332
|
+
payload: XrpcResponsePayload,
|
|
333
|
+
readonly cause: LexValidationError,
|
|
334
|
+
) {
|
|
335
|
+
super(method, response, payload, `Invalid response: ${cause.message}`, {
|
|
336
|
+
cause,
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
override toDownstreamError(): DownstreamError {
|
|
341
|
+
// @NOTE This could be reflected as both a 500 ("we" are at fault) and 502
|
|
342
|
+
// ("they" are at fault). We are using 502 here to allow downstream clients
|
|
343
|
+
// to determine that the issue lies at the interface between us and the
|
|
344
|
+
// upstream server, rather than an issue with our internal processing.
|
|
345
|
+
return { status: 502, body: this.toJSON() }
|
|
285
346
|
}
|
|
286
347
|
}
|
|
287
348
|
|
|
@@ -326,9 +387,13 @@ export class XrpcInternalError<
|
|
|
326
387
|
return true
|
|
327
388
|
}
|
|
328
389
|
|
|
329
|
-
override
|
|
330
|
-
// Do not expose internal error details to downstream clients
|
|
331
|
-
return
|
|
390
|
+
override toJSON(): LexErrorData<'InternalServerError'> {
|
|
391
|
+
// @NOTE Do not expose internal error details to downstream clients
|
|
392
|
+
return { error: this.error, message: 'Internal Server Error' }
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
override toDownstreamError(): DownstreamError {
|
|
396
|
+
return { status: 500, body: this.toJSON() }
|
|
332
397
|
}
|
|
333
398
|
}
|
|
334
399
|
|
|
@@ -393,3 +458,39 @@ export function asXrpcFailure<M extends Procedure | Query>(
|
|
|
393
458
|
|
|
394
459
|
return new XrpcInternalError(method, undefined, { cause })
|
|
395
460
|
}
|
|
461
|
+
|
|
462
|
+
const HOP_BY_HOP_HEADERS = new Set([
|
|
463
|
+
'connection',
|
|
464
|
+
'keep-alive',
|
|
465
|
+
'proxy-authenticate',
|
|
466
|
+
'proxy-authorization',
|
|
467
|
+
'te',
|
|
468
|
+
'trailer',
|
|
469
|
+
'transfer-encoding',
|
|
470
|
+
'upgrade',
|
|
471
|
+
])
|
|
472
|
+
|
|
473
|
+
function stripHopByHopHeaders(headers: Headers): Headers {
|
|
474
|
+
const result = new Headers(headers)
|
|
475
|
+
|
|
476
|
+
// Remove statically known hop-by-hop headers
|
|
477
|
+
for (const name of HOP_BY_HOP_HEADERS) {
|
|
478
|
+
result.delete(name)
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// Remove headers listed in the "Connection" header
|
|
482
|
+
const connection = headers.get('connection')
|
|
483
|
+
if (connection) {
|
|
484
|
+
for (const name of connection.split(',')) {
|
|
485
|
+
result.delete(name.trim())
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// These are not actually hop-by-hop headers, but we remove them because the
|
|
490
|
+
// upstream payload gets parsed and re-serialized, so content length and
|
|
491
|
+
// encoding may no longer be accurate.
|
|
492
|
+
result.delete('content-length')
|
|
493
|
+
result.delete('content-encoding')
|
|
494
|
+
|
|
495
|
+
return result
|
|
496
|
+
}
|
package/src/response.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from '@atproto/lex-schema'
|
|
8
8
|
import {
|
|
9
9
|
XrpcAuthenticationError,
|
|
10
|
+
XrpcInvalidResponseError,
|
|
10
11
|
XrpcResponseError,
|
|
11
12
|
XrpcUpstreamError,
|
|
12
13
|
isXrpcErrorPayload,
|
|
@@ -70,7 +71,7 @@ export class XrpcResponse<M extends Procedure | Query>
|
|
|
70
71
|
|
|
71
72
|
/**
|
|
72
73
|
* @throws {XrpcResponseError} in case of (valid) XRPC error responses. Use
|
|
73
|
-
* {@link XrpcResponseError.
|
|
74
|
+
* {@link XrpcResponseError.matchesSchemaErrors} to narrow the error type based on
|
|
74
75
|
* the method's declared error schema. This can be narrowed further as a
|
|
75
76
|
* {@link XrpcAuthenticationError} if the error is an authentication error.
|
|
76
77
|
* @throws {XrpcUpstreamError} when the response is not a valid XRPC
|
|
@@ -162,12 +163,11 @@ export class XrpcResponse<M extends Procedure | Query>
|
|
|
162
163
|
const result = method.output.schema.safeParse(payload.body)
|
|
163
164
|
|
|
164
165
|
if (!result.success) {
|
|
165
|
-
throw new
|
|
166
|
+
throw new XrpcInvalidResponseError(
|
|
166
167
|
method,
|
|
167
168
|
response,
|
|
168
169
|
payload,
|
|
169
|
-
|
|
170
|
-
{ cause: result.reason },
|
|
170
|
+
result.reason,
|
|
171
171
|
)
|
|
172
172
|
}
|
|
173
173
|
}
|