@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.
@@ -6,7 +6,7 @@ import {
6
6
  XrpcInternalError,
7
7
  XrpcInvalidResponseError,
8
8
  XrpcResponseError,
9
- XrpcUpstreamError,
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
- it('exposes status from the response', () => {
45
- const err = createResponseError(404, 'NotFound')
46
- expect(err.reason).toBe(err)
47
- expect(err.status).toBe(404)
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 headers from the response', () => {
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.headers.get('X-Test')).toBe('value')
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.body).toEqual({ error: 'TestError', message: 'details' })
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
- // XrpcUpstreamError
411
+ // XrpcInvalidResponseError
207
412
  // ============================================================================
208
413
 
209
- describe(XrpcUpstreamError, () => {
210
- it('has error code UpstreamFailure', () => {
211
- const response = new Response(null, { status: 200 })
212
- const err = new XrpcUpstreamError(testQuery, response)
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('UpstreamFailure')
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: 200 })
219
- const err = new XrpcUpstreamError(testQuery, response)
220
- const downstream = err.toDownstreamError()
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 XrpcUpstreamError(testQuery, response)
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: 200 })
232
- const err = new XrpcUpstreamError(testQuery, response)
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
- // XrpcInvalidResponseError
449
+ // XrpcResponseValidationError
239
450
  // ============================================================================
240
451
 
241
- describe(XrpcInvalidResponseError, () => {
242
- it('extends XrpcUpstreamError', () => {
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 XrpcInvalidResponseError(
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(XrpcUpstreamError)
465
+ expect(err).toBeInstanceOf(XrpcInvalidResponseError)
255
466
  expect(err.reason).toBe(err)
256
- expect(err.error).toBe('UpstreamFailure')
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 XrpcInvalidResponseError(
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 XrpcInvalidResponseError(
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 { 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,
@@ -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 XrpcUpstreamError} - For invalid/unexpected responses
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
- N extends LexErrorCode = InferMethodError<M> | LexErrorCode,
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: XrpcErrorPayload<N>,
196
+ readonly payload?: XrpcUnknownResponsePayload,
171
197
  options?: ErrorOptions,
172
198
  ) {
173
- 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
+ }
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<N> {
186
- 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()
187
227
  }
188
228
 
189
229
  override toDownstreamError(): DownstreamError {
190
- // 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
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: this.response.status === 500 ? 502 : this.status,
197
- headers: stripHopByHopHeaders(this.headers),
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(): LexErrorData<N> {
211
- return this.payload.body
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
- N extends LexErrorCode = LexErrorCode,
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 'UpstreamFailure' and maps to HTTP 502 Bad Gateway
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 XrpcUpstreamError<
322
+ export class XrpcInvalidResponseError<
290
323
  M extends Procedure | Query = Procedure | Query,
291
- > extends XrpcError<M, 'UpstreamFailure', XrpcUpstreamError<M>> {
292
- name = 'XrpcUpstreamError'
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: XrpcUnknownResponsePayload | null = null,
298
- message: string = `Unexpected upstream XRPC response`,
330
+ readonly payload?: XrpcUnknownResponsePayload,
331
+ message: string = buildResponseOverviewMessage(response),
299
332
  options?: ErrorOptions,
300
333
  ) {
301
- super(method, 'UpstreamFailure', message, options)
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 XrpcUpstreamError} that indicates the
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 XrpcInvalidResponseError<
361
+ export class XrpcResponseValidationError<
329
362
  M extends Procedure | Query = Procedure | Query,
330
- > extends XrpcUpstreamError<M> {
331
- name = 'XrpcInvalidResponseError'
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(method, response, payload, `Invalid response: ${cause.message}`, {
340
- cause,
341
- })
342
- }
343
-
344
- override toDownstreamError(): DownstreamError {
345
- // @NOTE This could be reflected as both a 500 ("we" are at fault) and 502
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 | XrpcUpstreamError | XrpcInternalError)
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
- | XrpcUpstreamError<M>
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 XrpcUpstreamError ||
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
+ }