@atproto/lex-client 0.0.16 → 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/src/errors.ts CHANGED
@@ -1,4 +1,9 @@
1
- import { LexError, LexErrorCode, LexErrorData } from '@atproto/lex-data'
1
+ import {
2
+ LexError,
3
+ LexErrorCode,
4
+ LexErrorData,
5
+ LexValue,
6
+ } from '@atproto/lex-data'
2
7
  import {
3
8
  InferMethodError,
4
9
  LexValidationError,
@@ -9,12 +14,36 @@ import {
9
14
  } from '@atproto/lex-schema'
10
15
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
11
16
  import { Agent } from './agent.js'
12
- import { XrpcResponsePayload } from './util.js'
17
+ import { XrpcUnknownResponsePayload } from './types.js'
13
18
  import {
14
19
  WWWAuthenticate,
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
+
45
+ export type { XrpcUnknownResponsePayload }
46
+
18
47
  export type DownstreamError<N extends LexErrorCode = LexErrorCode> = {
19
48
  status: number
20
49
  headers?: Headers
@@ -68,7 +97,7 @@ export type XrpcErrorPayload<N extends LexErrorCode = LexErrorCode> = {
68
97
  * This function checks whether a given payload matches this schema.
69
98
  */
70
99
  export function isXrpcErrorPayload(
71
- payload: XrpcResponsePayload | null | undefined,
100
+ payload: XrpcUnknownResponsePayload | null | undefined,
72
101
  ): payload is XrpcErrorPayload {
73
102
  return (
74
103
  payload != null &&
@@ -88,7 +117,7 @@ export function isXrpcErrorPayload(
88
117
  * @typeParam TReason - The reason type for ResultFailure
89
118
  *
90
119
  * @see {@link XrpcResponseError} - For valid XRPC error responses
91
- * @see {@link XrpcUpstreamError} - For invalid/unexpected responses
120
+ * @see {@link XrpcInvalidResponseError} - For invalid/unexpected responses
92
121
  * @see {@link XrpcInternalError} - For network/internal errors
93
122
  */
94
123
  export abstract class XrpcError<
@@ -158,17 +187,23 @@ export abstract class XrpcError<
158
187
  */
159
188
  export class XrpcResponseError<
160
189
  M extends Procedure | Query = Procedure | Query,
161
- N extends LexErrorCode = InferMethodError<M> | LexErrorCode,
162
- > extends XrpcError<M, N, XrpcResponseError<M, N>> {
190
+ > extends XrpcError<M, LexErrorCode, XrpcResponseError<M>> {
163
191
  name = 'XrpcResponseError'
164
192
 
165
193
  constructor(
166
194
  method: M,
167
195
  readonly response: Response,
168
- readonly payload: XrpcErrorPayload<N>,
196
+ readonly payload?: XrpcUnknownResponsePayload,
169
197
  options?: ErrorOptions,
170
198
  ) {
171
- const { error, message } = payload.body
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
+ }
172
207
  super(method, error, message, options)
173
208
  }
174
209
 
@@ -180,19 +215,27 @@ export class XrpcResponseError<
180
215
  return RETRYABLE_HTTP_STATUS_CODES.has(this.response.status)
181
216
  }
182
217
 
183
- override toJSON(): LexErrorData<N> {
184
- return this.payload.body
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()
185
227
  }
186
228
 
187
229
  override toDownstreamError(): DownstreamError {
188
- // If the upstream server returned a 5xx error, we want to return a 502 Bad
230
+ const { status, headers } = this.response
231
+ // If the upstream server returned a 500 error, we want to return a 502 Bad
189
232
  // Gateway to downstream clients, as the issue is with the upstream server,
190
233
  // not us. We still return the original error code and message in the body
191
234
  // for transparency, but we do not want to expose internal server errors
192
235
  // from the upstream server as-is to downstream clients.
193
236
  return {
194
- status: this.response.status === 500 ? 502 : this.status,
195
- headers: stripHopByHopHeaders(this.headers),
237
+ status: status === 500 ? 502 : status,
238
+ headers: stripHopByHopHeaders(headers),
196
239
  body: this.toJSON(),
197
240
  }
198
241
  }
@@ -205,8 +248,8 @@ export class XrpcResponseError<
205
248
  return this.response.headers
206
249
  }
207
250
 
208
- get body(): LexErrorData<N> {
209
- return this.payload.body
251
+ get body(): undefined | Uint8Array | LexValue {
252
+ return this.payload?.body
210
253
  }
211
254
  }
212
255
 
@@ -240,8 +283,7 @@ export type { WWWAuthenticate }
240
283
  */
241
284
  export class XrpcAuthenticationError<
242
285
  M extends Procedure | Query = Procedure | Query,
243
- N extends LexErrorCode = LexErrorCode,
244
- > extends XrpcResponseError<M, N> {
286
+ > extends XrpcResponseError<M> {
245
287
  name = 'XrpcAuthenticationError'
246
288
 
247
289
  override shouldRetry(): boolean {
@@ -259,14 +301,6 @@ export class XrpcAuthenticationError<
259
301
  this.response.headers.get('www-authenticate'),
260
302
  ) ?? {})
261
303
  }
262
-
263
- override toDownstreamError(): DownstreamError {
264
- return {
265
- status: 401,
266
- headers: stripHopByHopHeaders(this.headers),
267
- body: this.toJSON(),
268
- }
269
- }
270
304
  }
271
305
 
272
306
  /**
@@ -279,24 +313,25 @@ export class XrpcAuthenticationError<
279
313
  * - Non-JSON error responses
280
314
  * - Responses from non-XRPC endpoints
281
315
  *
282
- * The error code is always 'UpstreamFailure' and maps to HTTP 502 Bad Gateway
283
- * 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.
284
319
  *
285
320
  * @typeParam M - The XRPC method type
286
321
  */
287
- export class XrpcUpstreamError<
322
+ export class XrpcInvalidResponseError<
288
323
  M extends Procedure | Query = Procedure | Query,
289
- > extends XrpcError<M, 'UpstreamFailure', XrpcUpstreamError<M>> {
290
- name = 'XrpcUpstreamError'
324
+ > extends XrpcError<M, 'InvalidResponse', XrpcInvalidResponseError<M>> {
325
+ name = 'XrpcInvalidResponseError'
291
326
 
292
327
  constructor(
293
328
  method: M,
294
329
  readonly response: Response,
295
- readonly payload: XrpcResponsePayload | null = null,
296
- message: string = `Unexpected upstream XRPC response`,
330
+ readonly payload?: XrpcUnknownResponsePayload,
331
+ message: string = buildResponseOverviewMessage(response),
297
332
  options?: ErrorOptions,
298
333
  ) {
299
- super(method, 'UpstreamFailure', message, options)
334
+ super(method, 'InvalidResponse', message, options)
300
335
  }
301
336
 
302
337
  override get reason(): this {
@@ -315,7 +350,7 @@ export class XrpcUpstreamError<
315
350
  /**
316
351
  * Error class for invalid XRPC responses that fail schema validation.
317
352
  *
318
- * This is a specific type of {@link XrpcUpstreamError} that indicates the
353
+ * This is a specific type of {@link XrpcInvalidResponseError} that indicates the
319
354
  * upstream server returned a response that was structurally valid but did not
320
355
  * conform to the expected schema for the method. This likely indicates a
321
356
  * mismatch between client and server versions or an issue with the server's
@@ -323,28 +358,24 @@ export class XrpcUpstreamError<
323
358
  *
324
359
  * @typeParam M - The XRPC method type
325
360
  */
326
- export class XrpcInvalidResponseError<
361
+ export class XrpcResponseValidationError<
327
362
  M extends Procedure | Query = Procedure | Query,
328
- > extends XrpcUpstreamError<M> {
329
- name = 'XrpcInvalidResponseError'
363
+ > extends XrpcInvalidResponseError<M> {
364
+ name = 'XrpcResponseValidationError'
330
365
 
331
366
  constructor(
332
367
  method: M,
333
368
  response: Response,
334
- payload: XrpcResponsePayload,
369
+ payload: XrpcUnknownResponsePayload,
335
370
  readonly cause: LexValidationError,
336
371
  ) {
337
- super(method, response, payload, `Invalid response: ${cause.message}`, {
338
- cause,
339
- })
340
- }
341
-
342
- override toDownstreamError(): DownstreamError {
343
- // @NOTE This could be reflected as both a 500 ("we" are at fault) and 502
344
- // ("they" are at fault). We are using 502 here to allow downstream clients
345
- // to determine that the issue lies at the interface between us and the
346
- // upstream server, rather than an issue with our internal processing.
347
- return { status: 502, body: this.toJSON() }
372
+ super(
373
+ method,
374
+ response,
375
+ payload,
376
+ `Invalid response payload: ${cause.message}`,
377
+ { cause },
378
+ )
348
379
  }
349
380
  }
350
381
 
@@ -446,7 +477,7 @@ export class XrpcFetchError<
446
477
  * if (result.success) {
447
478
  * console.log(result.body) // XrpcResponse
448
479
  * } else {
449
- * // result is XrpcFailure (XrpcResponseError | XrpcUpstreamError | XrpcInternalError)
480
+ * // result is XrpcFailure (XrpcResponseError | XrpcInvalidResponseError | XrpcInternalError)
450
481
  * console.error(result.error, result.message)
451
482
  * }
452
483
  * ```
@@ -455,7 +486,7 @@ export type XrpcFailure<M extends Procedure | Query = Procedure | Query> =
455
486
  // The server returned a valid XRPC error response
456
487
  | XrpcResponseError<M>
457
488
  // The response was not a valid XRPC response, or it does not match the schema
458
- | XrpcUpstreamError<M>
489
+ | XrpcInvalidResponseError<M>
459
490
  // Something went wrong (network error, etc.)
460
491
  | XrpcInternalError<M>
461
492
 
@@ -485,7 +516,7 @@ export function asXrpcFailure<M extends Procedure | Query>(
485
516
  ): XrpcFailure<M> {
486
517
  if (
487
518
  cause instanceof XrpcResponseError ||
488
- cause instanceof XrpcUpstreamError ||
519
+ cause instanceof XrpcInvalidResponseError ||
489
520
  cause instanceof XrpcInternalError
490
521
  ) {
491
522
  if (cause.method === method) return cause
@@ -529,3 +560,11 @@ function stripHopByHopHeaders(headers: Headers): Headers {
529
560
 
530
561
  return result
531
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
+ }