@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.ts
DELETED
|
@@ -1,570 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
LexError,
|
|
3
|
-
LexErrorCode,
|
|
4
|
-
LexErrorData,
|
|
5
|
-
LexValue,
|
|
6
|
-
} from '@atproto/lex-data'
|
|
7
|
-
import {
|
|
8
|
-
InferMethodError,
|
|
9
|
-
LexValidationError,
|
|
10
|
-
Procedure,
|
|
11
|
-
Query,
|
|
12
|
-
ResultFailure,
|
|
13
|
-
lexErrorDataSchema,
|
|
14
|
-
} from '@atproto/lex-schema'
|
|
15
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
16
|
-
import { Agent } from './agent.js'
|
|
17
|
-
import { XrpcUnknownResponsePayload } from './types.js'
|
|
18
|
-
import {
|
|
19
|
-
WWWAuthenticate,
|
|
20
|
-
parseWWWAuthenticateHeader,
|
|
21
|
-
} from './www-authenticate.js'
|
|
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
|
-
|
|
47
|
-
export type DownstreamError<N extends LexErrorCode = LexErrorCode> = {
|
|
48
|
-
status: number
|
|
49
|
-
headers?: Headers
|
|
50
|
-
encoding?: 'application/json'
|
|
51
|
-
body: LexErrorData<N>
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* HTTP status codes that indicate a transient error that may succeed on retry.
|
|
56
|
-
*
|
|
57
|
-
* Includes:
|
|
58
|
-
* - 408 Request Timeout
|
|
59
|
-
* - 425 Too Early
|
|
60
|
-
* - 429 Too Many Requests (rate limited)
|
|
61
|
-
* - 500 Internal Server Error
|
|
62
|
-
* - 502 Bad Gateway
|
|
63
|
-
* - 503 Service Unavailable
|
|
64
|
-
* - 504 Gateway Timeout
|
|
65
|
-
* - 522 Connection Timed Out (Cloudflare)
|
|
66
|
-
* - 524 A Timeout Occurred (Cloudflare)
|
|
67
|
-
*/
|
|
68
|
-
export const RETRYABLE_HTTP_STATUS_CODES: ReadonlySet<number> = new Set([
|
|
69
|
-
408, 425, 429, 500, 502, 503, 504, 522, 524,
|
|
70
|
-
])
|
|
71
|
-
|
|
72
|
-
export { LexError }
|
|
73
|
-
export type { LexErrorCode, LexErrorData }
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* The payload structure for XRPC error responses.
|
|
77
|
-
*
|
|
78
|
-
* All XRPC errors return JSON with an `error` code and optional `message`.
|
|
79
|
-
*
|
|
80
|
-
* @typeParam N - The specific error code type
|
|
81
|
-
*/
|
|
82
|
-
export type XrpcErrorPayload<N extends LexErrorCode = LexErrorCode> = {
|
|
83
|
-
body: LexErrorData<N>
|
|
84
|
-
encoding: 'application/json'
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* All unsuccessful responses should follow a standard error response
|
|
89
|
-
* schema. The Content-Type should be application/json, and the payload
|
|
90
|
-
* should be a JSON object with the following fields:
|
|
91
|
-
*
|
|
92
|
-
* - `error` (string, required): type name of the error (generic ASCII
|
|
93
|
-
* constant, no whitespace)
|
|
94
|
-
* - `message` (string, optional): description of the error, appropriate for
|
|
95
|
-
* display to humans
|
|
96
|
-
*
|
|
97
|
-
* This function checks whether a given payload matches this schema.
|
|
98
|
-
*/
|
|
99
|
-
export function isXrpcErrorPayload(
|
|
100
|
-
payload: XrpcUnknownResponsePayload | null | undefined,
|
|
101
|
-
): payload is XrpcErrorPayload {
|
|
102
|
-
return (
|
|
103
|
-
payload != null &&
|
|
104
|
-
payload.encoding === 'application/json' &&
|
|
105
|
-
lexErrorDataSchema.matches(payload.body)
|
|
106
|
-
)
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Abstract base class for all XRPC errors.
|
|
111
|
-
*
|
|
112
|
-
* Extends {@link LexError} and implements {@link ResultFailure} for use with
|
|
113
|
-
* safe/result-based error handling patterns.
|
|
114
|
-
*
|
|
115
|
-
* @typeParam M - The XRPC method type (Procedure or Query)
|
|
116
|
-
* @typeParam N - The error code type
|
|
117
|
-
* @typeParam TReason - The reason type for ResultFailure
|
|
118
|
-
*
|
|
119
|
-
* @see {@link XrpcResponseError} - For valid XRPC error responses
|
|
120
|
-
* @see {@link XrpcInvalidResponseError} - For invalid/unexpected responses
|
|
121
|
-
* @see {@link XrpcInternalError} - For network/internal errors
|
|
122
|
-
*/
|
|
123
|
-
export abstract class XrpcError<
|
|
124
|
-
M extends Procedure | Query = Procedure | Query,
|
|
125
|
-
N extends LexErrorCode = LexErrorCode,
|
|
126
|
-
TReason = unknown,
|
|
127
|
-
>
|
|
128
|
-
extends LexError<N>
|
|
129
|
-
implements ResultFailure<TReason>
|
|
130
|
-
{
|
|
131
|
-
name = 'XrpcError'
|
|
132
|
-
|
|
133
|
-
constructor(
|
|
134
|
-
readonly method: M,
|
|
135
|
-
error: N,
|
|
136
|
-
message: string = `${error} Lexicon RPC error`,
|
|
137
|
-
options?: ErrorOptions,
|
|
138
|
-
) {
|
|
139
|
-
super(error, message, options)
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* @see {@link ResultFailure.success}
|
|
144
|
-
*/
|
|
145
|
-
readonly success = false as const
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* @see {@link ResultFailure.reason}
|
|
149
|
-
*/
|
|
150
|
-
abstract readonly reason: TReason
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Indicates whether the error is transient and can be retried.
|
|
154
|
-
*/
|
|
155
|
-
abstract shouldRetry(): boolean
|
|
156
|
-
|
|
157
|
-
abstract toDownstreamError(): DownstreamError
|
|
158
|
-
|
|
159
|
-
matchesSchemaErrors(): this is XrpcError<M, InferMethodError<M>> {
|
|
160
|
-
return this.method.errors?.includes(this.error) ?? false
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Error class for valid XRPC error responses from the server.
|
|
166
|
-
*
|
|
167
|
-
* This represents a properly formatted XRPC error where the server returned
|
|
168
|
-
* a non-2xx status with a valid JSON error payload containing `error` and
|
|
169
|
-
* optional `message` fields.
|
|
170
|
-
*
|
|
171
|
-
* Use {@link matchesSchemaErrors} to check if the error matches the method's declared
|
|
172
|
-
* error types for type-safe error handling.
|
|
173
|
-
*
|
|
174
|
-
* @typeParam M - The XRPC method type
|
|
175
|
-
* @typeParam N - The error code type (inferred from method or generic)
|
|
176
|
-
*
|
|
177
|
-
* @example Handling specific errors
|
|
178
|
-
* ```typescript
|
|
179
|
-
* try {
|
|
180
|
-
* await client.xrpc(someMethod, options)
|
|
181
|
-
* } catch (err) {
|
|
182
|
-
* if (err instanceof XrpcResponseError && err.error === 'RecordNotFound') {
|
|
183
|
-
* // Handle not found case
|
|
184
|
-
* }
|
|
185
|
-
* }
|
|
186
|
-
* ```
|
|
187
|
-
*/
|
|
188
|
-
export class XrpcResponseError<
|
|
189
|
-
M extends Procedure | Query = Procedure | Query,
|
|
190
|
-
> extends XrpcError<M, LexErrorCode, XrpcResponseError<M>> {
|
|
191
|
-
name = 'XrpcResponseError'
|
|
192
|
-
|
|
193
|
-
constructor(
|
|
194
|
-
method: M,
|
|
195
|
-
readonly response: Response,
|
|
196
|
-
readonly payload?: XrpcUnknownResponsePayload,
|
|
197
|
-
options?: ErrorOptions,
|
|
198
|
-
) {
|
|
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
|
-
}
|
|
207
|
-
super(method, error, message, options)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
override get reason(): this {
|
|
211
|
-
return this
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
override shouldRetry(): boolean {
|
|
215
|
-
return RETRYABLE_HTTP_STATUS_CODES.has(this.response.status)
|
|
216
|
-
}
|
|
217
|
-
|
|
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()
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
override toDownstreamError(): DownstreamError {
|
|
230
|
-
const { status, headers } = this.response
|
|
231
|
-
// If the upstream server returned a 500 error, we want to return a 502 Bad
|
|
232
|
-
// Gateway to downstream clients, as the issue is with the upstream server,
|
|
233
|
-
// not us. We still return the original error code and message in the body
|
|
234
|
-
// for transparency, but we do not want to expose internal server errors
|
|
235
|
-
// from the upstream server as-is to downstream clients.
|
|
236
|
-
return {
|
|
237
|
-
status: status === 500 ? 502 : status,
|
|
238
|
-
headers: stripHopByHopHeaders(headers),
|
|
239
|
-
body: this.toJSON(),
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
get status(): number {
|
|
244
|
-
return this.response.status
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
get headers(): Headers {
|
|
248
|
-
return this.response.headers
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
get body(): undefined | Uint8Array | LexValue {
|
|
252
|
-
return this.payload?.body
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
export type { WWWAuthenticate }
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Error class for 401 Unauthorized XRPC responses.
|
|
260
|
-
*
|
|
261
|
-
* Extends {@link XrpcResponseError} with access to parsed WWW-Authenticate header
|
|
262
|
-
* information, useful for implementing authentication flows.
|
|
263
|
-
*
|
|
264
|
-
* Authentication errors are never retryable as they require user intervention
|
|
265
|
-
* (e.g., re-authentication, token refresh).
|
|
266
|
-
*
|
|
267
|
-
* @typeParam M - The XRPC method type
|
|
268
|
-
* @typeParam N - The error code type
|
|
269
|
-
*
|
|
270
|
-
* @example Handling authentication errors
|
|
271
|
-
* ```typescript
|
|
272
|
-
* try {
|
|
273
|
-
* await client.xrpc(someMethod, options)
|
|
274
|
-
* } catch (err) {
|
|
275
|
-
* if (err instanceof XrpcAuthenticationError) {
|
|
276
|
-
* const { DPoP } = err.wwwAuthenticate
|
|
277
|
-
* if (DPoP?.error === 'use_dpop_nonce') {
|
|
278
|
-
* // Handle DPoP nonce requirement
|
|
279
|
-
* }
|
|
280
|
-
* }
|
|
281
|
-
* }
|
|
282
|
-
* ```
|
|
283
|
-
*/
|
|
284
|
-
export class XrpcAuthenticationError<
|
|
285
|
-
M extends Procedure | Query = Procedure | Query,
|
|
286
|
-
> extends XrpcResponseError<M> {
|
|
287
|
-
name = 'XrpcAuthenticationError'
|
|
288
|
-
|
|
289
|
-
override shouldRetry(): boolean {
|
|
290
|
-
return false
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
#wwwAuthenticateCached?: WWWAuthenticate
|
|
294
|
-
/**
|
|
295
|
-
* Parsed WWW-Authenticate header from the response.
|
|
296
|
-
* Contains authentication scheme parameters (e.g., Bearer realm, DPoP nonce).
|
|
297
|
-
*/
|
|
298
|
-
get wwwAuthenticate(): WWWAuthenticate {
|
|
299
|
-
return (this.#wwwAuthenticateCached ??=
|
|
300
|
-
parseWWWAuthenticateHeader(
|
|
301
|
-
this.response.headers.get('www-authenticate'),
|
|
302
|
-
) ?? {})
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/**
|
|
307
|
-
* Error class for invalid or unprocessable XRPC responses from upstream servers.
|
|
308
|
-
*
|
|
309
|
-
* This occurs when the server returns a response that doesn't conform to the
|
|
310
|
-
* XRPC protocol, such as:
|
|
311
|
-
* - Missing or invalid Content-Type header
|
|
312
|
-
* - Response body that doesn't match the method's output schema
|
|
313
|
-
* - Non-JSON error responses
|
|
314
|
-
* - Responses from non-XRPC endpoints
|
|
315
|
-
*
|
|
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.
|
|
319
|
-
*
|
|
320
|
-
* @typeParam M - The XRPC method type
|
|
321
|
-
*/
|
|
322
|
-
export class XrpcInvalidResponseError<
|
|
323
|
-
M extends Procedure | Query = Procedure | Query,
|
|
324
|
-
> extends XrpcError<M, 'InvalidResponse', XrpcInvalidResponseError<M>> {
|
|
325
|
-
name = 'XrpcInvalidResponseError'
|
|
326
|
-
|
|
327
|
-
constructor(
|
|
328
|
-
method: M,
|
|
329
|
-
readonly response: Response,
|
|
330
|
-
readonly payload?: XrpcUnknownResponsePayload,
|
|
331
|
-
message: string = buildResponseOverviewMessage(response),
|
|
332
|
-
options?: ErrorOptions,
|
|
333
|
-
) {
|
|
334
|
-
super(method, 'InvalidResponse', message, options)
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
override get reason(): this {
|
|
338
|
-
return this
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
override shouldRetry(): boolean {
|
|
342
|
-
return RETRYABLE_HTTP_STATUS_CODES.has(this.response.status)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
override toDownstreamError(): DownstreamError {
|
|
346
|
-
return { status: 502, body: this.toJSON() }
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/**
|
|
351
|
-
* Error class for invalid XRPC responses that fail schema validation.
|
|
352
|
-
*
|
|
353
|
-
* This is a specific type of {@link XrpcInvalidResponseError} that indicates the
|
|
354
|
-
* upstream server returned a response that was structurally valid but did not
|
|
355
|
-
* conform to the expected schema for the method. This likely indicates a
|
|
356
|
-
* mismatch between client and server versions or an issue with the server's
|
|
357
|
-
* XRPC implementation.
|
|
358
|
-
*
|
|
359
|
-
* @typeParam M - The XRPC method type
|
|
360
|
-
*/
|
|
361
|
-
export class XrpcResponseValidationError<
|
|
362
|
-
M extends Procedure | Query = Procedure | Query,
|
|
363
|
-
> extends XrpcInvalidResponseError<M> {
|
|
364
|
-
name = 'XrpcResponseValidationError'
|
|
365
|
-
|
|
366
|
-
constructor(
|
|
367
|
-
method: M,
|
|
368
|
-
response: Response,
|
|
369
|
-
payload: XrpcUnknownResponsePayload,
|
|
370
|
-
readonly cause: LexValidationError,
|
|
371
|
-
) {
|
|
372
|
-
super(
|
|
373
|
-
method,
|
|
374
|
-
response,
|
|
375
|
-
payload,
|
|
376
|
-
`Invalid response payload: ${cause.message}`,
|
|
377
|
-
{ cause },
|
|
378
|
-
)
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* Error class for unexpected internal/client-side errors during XRPC requests.
|
|
384
|
-
*
|
|
385
|
-
* The error code is always 'InternalServerError' and these errors not
|
|
386
|
-
* considered retryable as they stem from unforeseen issues in the
|
|
387
|
-
* implementation.
|
|
388
|
-
*
|
|
389
|
-
* @typeParam M - The XRPC method type
|
|
390
|
-
*/
|
|
391
|
-
export class XrpcInternalError<
|
|
392
|
-
M extends Procedure | Query = Procedure | Query,
|
|
393
|
-
> extends XrpcError<M, 'InternalServerError', XrpcInternalError<M>> {
|
|
394
|
-
name = 'XrpcInternalError'
|
|
395
|
-
|
|
396
|
-
constructor(method: M, message?: string, options?: ErrorOptions) {
|
|
397
|
-
super(
|
|
398
|
-
method,
|
|
399
|
-
'InternalServerError',
|
|
400
|
-
message ?? 'Unable to fulfill XRPC request',
|
|
401
|
-
options,
|
|
402
|
-
)
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
override get reason(): this {
|
|
406
|
-
return this
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
override shouldRetry(): boolean {
|
|
410
|
-
return false
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
override toJSON(): LexErrorData {
|
|
414
|
-
// @NOTE Do not expose internal error details to downstream clients
|
|
415
|
-
return { error: this.error, message: 'Internal Server Error' }
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
override toDownstreamError(): DownstreamError {
|
|
419
|
-
return { status: 500, body: this.toJSON() }
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
|
|
423
|
-
/**
|
|
424
|
-
* Special case of XrpcInternalError that specifically represents errors thrown
|
|
425
|
-
* by {@link Agent.fetchHandler} during the XRPC request. This includes:
|
|
426
|
-
* - Network errors (connection refused, DNS failure)
|
|
427
|
-
* - Request timeouts
|
|
428
|
-
* - Request aborted via AbortSignal
|
|
429
|
-
*
|
|
430
|
-
* These errors are optimistically considered retryable, as many fetch errors
|
|
431
|
-
* are transient and may succeed on retry.
|
|
432
|
-
*/
|
|
433
|
-
export class XrpcFetchError<
|
|
434
|
-
M extends Procedure | Query = Procedure | Query,
|
|
435
|
-
> extends XrpcInternalError<M> {
|
|
436
|
-
name = 'XrpcFetchError'
|
|
437
|
-
|
|
438
|
-
constructor(method: M, cause: unknown) {
|
|
439
|
-
const message = cause instanceof Error ? cause.message : String(cause)
|
|
440
|
-
super(method, `Unexpected fetchHandler() error: ${message}`, { cause })
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
override shouldRetry(): boolean {
|
|
444
|
-
// Ideally, we would inspect the reason to determine if it's retryable (by
|
|
445
|
-
// detecting network errors, timeouts, etc.). Since these cases are highly
|
|
446
|
-
// platform-dependent, we optimistically assume all fetch errors are
|
|
447
|
-
// transient and retryable.
|
|
448
|
-
return true
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
override toJSON(): LexErrorData {
|
|
452
|
-
// @NOTE Do not expose internal error details to downstream clients
|
|
453
|
-
return { error: this.error, message: 'Failed to perform upstream request' }
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
override toDownstreamError(): DownstreamError {
|
|
457
|
-
// While it might technically be a 500 error, we use 502 Bad Gateway here to
|
|
458
|
-
// indicate that the error occurred while communicating with the upstream
|
|
459
|
-
// server, allowing downstream clients to distinguish between errors in our
|
|
460
|
-
// internal processing (500) and errors in the upstream server or network
|
|
461
|
-
// (502).
|
|
462
|
-
return { status: 502, body: this.toJSON() }
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
/**
|
|
467
|
-
* Union type of all possible XRPC failure types.
|
|
468
|
-
*
|
|
469
|
-
* Used as the return type for safe/non-throwing XRPC methods. Check the
|
|
470
|
-
* `success` property to distinguish between success and failure:
|
|
471
|
-
*
|
|
472
|
-
* @typeParam M - The XRPC method type
|
|
473
|
-
*
|
|
474
|
-
* @example
|
|
475
|
-
* ```typescript
|
|
476
|
-
* const result = await client.xrpcSafe(someMethod, options)
|
|
477
|
-
* if (result.success) {
|
|
478
|
-
* console.log(result.body) // XrpcResponse
|
|
479
|
-
* } else {
|
|
480
|
-
* // result is XrpcFailure (XrpcResponseError | XrpcInvalidResponseError | XrpcInternalError)
|
|
481
|
-
* console.error(result.error, result.message)
|
|
482
|
-
* }
|
|
483
|
-
* ```
|
|
484
|
-
*/
|
|
485
|
-
export type XrpcFailure<M extends Procedure | Query = Procedure | Query> =
|
|
486
|
-
// The server returned a valid XRPC error response
|
|
487
|
-
| XrpcResponseError<M>
|
|
488
|
-
// The response was not a valid XRPC response, or it does not match the schema
|
|
489
|
-
| XrpcInvalidResponseError<M>
|
|
490
|
-
// Something went wrong (network error, etc.)
|
|
491
|
-
| XrpcInternalError<M>
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Converts an unknown error into an appropriate {@link XrpcFailure} type.
|
|
495
|
-
*
|
|
496
|
-
* If the error is already an XrpcFailure for the given method, returns it as-is.
|
|
497
|
-
* Otherwise, wraps it in an {@link XrpcInternalError}.
|
|
498
|
-
*
|
|
499
|
-
* @param method - The XRPC method that was called
|
|
500
|
-
* @param cause - The error to convert
|
|
501
|
-
* @returns An XrpcFailure instance
|
|
502
|
-
*
|
|
503
|
-
* @example
|
|
504
|
-
* ```typescript
|
|
505
|
-
* try {
|
|
506
|
-
* const response = await fetch(...)
|
|
507
|
-
* // ... process response
|
|
508
|
-
* } catch (err) {
|
|
509
|
-
* return asXrpcFailure(method, err)
|
|
510
|
-
* }
|
|
511
|
-
* ```
|
|
512
|
-
*/
|
|
513
|
-
export function asXrpcFailure<M extends Procedure | Query>(
|
|
514
|
-
method: M,
|
|
515
|
-
cause: unknown,
|
|
516
|
-
): XrpcFailure<M> {
|
|
517
|
-
if (
|
|
518
|
-
cause instanceof XrpcResponseError ||
|
|
519
|
-
cause instanceof XrpcInvalidResponseError ||
|
|
520
|
-
cause instanceof XrpcInternalError
|
|
521
|
-
) {
|
|
522
|
-
if (cause.method === method) return cause
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
return new XrpcInternalError(method, undefined, { cause })
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
const HOP_BY_HOP_HEADERS = new Set([
|
|
529
|
-
'connection',
|
|
530
|
-
'keep-alive',
|
|
531
|
-
'proxy-authenticate',
|
|
532
|
-
'proxy-authorization',
|
|
533
|
-
'te',
|
|
534
|
-
'trailer',
|
|
535
|
-
'transfer-encoding',
|
|
536
|
-
'upgrade',
|
|
537
|
-
])
|
|
538
|
-
|
|
539
|
-
function stripHopByHopHeaders(headers: Headers): Headers {
|
|
540
|
-
const result = new Headers(headers)
|
|
541
|
-
|
|
542
|
-
// Remove statically known hop-by-hop headers
|
|
543
|
-
for (const name of HOP_BY_HOP_HEADERS) {
|
|
544
|
-
result.delete(name)
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
// Remove headers listed in the "Connection" header
|
|
548
|
-
const connection = headers.get('connection')
|
|
549
|
-
if (connection) {
|
|
550
|
-
for (const name of connection.split(',')) {
|
|
551
|
-
result.delete(name.trim())
|
|
552
|
-
}
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
// These are not actually hop-by-hop headers, but we remove them because the
|
|
556
|
-
// upstream payload gets parsed and re-serialized, so content length and
|
|
557
|
-
// encoding may no longer be accurate.
|
|
558
|
-
result.delete('content-length')
|
|
559
|
-
result.delete('content-encoding')
|
|
560
|
-
|
|
561
|
-
return result
|
|
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
|
-
}
|