@atproto/lex-client 0.0.15 → 0.0.16
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 +9 -0
- package/dist/agent.d.ts +1 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +7 -5
- package/dist/agent.js.map +1 -1
- package/dist/errors.d.ts +23 -12
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +43 -16
- package/dist/errors.js.map +1 -1
- package/dist/util.d.ts +1 -0
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +6 -0
- package/dist/util.js.map +1 -1
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +3 -1
- package/dist/xrpc.js.map +1 -1
- package/package.json +3 -3
- package/src/agent.test.ts +216 -0
- package/src/agent.ts +8 -6
- package/src/errors.test.ts +77 -8
- package/src/errors.ts +52 -17
- package/src/util.test.ts +333 -0
- package/src/util.ts +9 -0
- package/src/xrpc.test.ts +904 -0
- package/src/xrpc.ts +4 -2
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
type Agent,
|
|
4
|
+
type AgentConfig,
|
|
5
|
+
type FetchHandler,
|
|
6
|
+
buildAgent,
|
|
7
|
+
isAgent,
|
|
8
|
+
} from './agent.js'
|
|
9
|
+
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// isAgent
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
14
|
+
describe(isAgent, () => {
|
|
15
|
+
it('returns true for a valid agent with did', () => {
|
|
16
|
+
const agent: Agent = {
|
|
17
|
+
did: 'did:plc:example',
|
|
18
|
+
fetchHandler: async () => new Response(),
|
|
19
|
+
}
|
|
20
|
+
expect(isAgent(agent)).toBe(true)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('returns true for an agent without did', () => {
|
|
24
|
+
const agent = { fetchHandler: async () => new Response() }
|
|
25
|
+
expect(isAgent(agent)).toBe(true)
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('returns true when did is undefined', () => {
|
|
29
|
+
const agent = { did: undefined, fetchHandler: async () => new Response() }
|
|
30
|
+
expect(isAgent(agent)).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('returns false for null', () => {
|
|
34
|
+
expect(isAgent(null)).toBe(false)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('returns false for non-objects', () => {
|
|
38
|
+
expect(isAgent('string')).toBe(false)
|
|
39
|
+
expect(isAgent(42)).toBe(false)
|
|
40
|
+
expect(isAgent(undefined)).toBe(false)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('returns false when fetchHandler is not a function', () => {
|
|
44
|
+
expect(isAgent({ fetchHandler: 'not-a-function' })).toBe(false)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns false when did is not a string', () => {
|
|
48
|
+
expect(isAgent({ did: 42, fetchHandler: async () => new Response() })).toBe(
|
|
49
|
+
false,
|
|
50
|
+
)
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// buildAgent
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
describe(buildAgent, () => {
|
|
59
|
+
describe('from Agent', () => {
|
|
60
|
+
it('returns the same agent instance', () => {
|
|
61
|
+
const agent: Agent = {
|
|
62
|
+
did: 'did:plc:example',
|
|
63
|
+
fetchHandler: async () => new Response(),
|
|
64
|
+
}
|
|
65
|
+
expect(buildAgent(agent)).toBe(agent)
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
describe('from FetchHandler', () => {
|
|
70
|
+
it('wraps a function as an agent', () => {
|
|
71
|
+
const handler: FetchHandler = async () => new Response()
|
|
72
|
+
const agent = buildAgent(handler)
|
|
73
|
+
|
|
74
|
+
expect(agent.did).toBeUndefined()
|
|
75
|
+
expect(typeof agent.fetchHandler).toBe('function')
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('from string URL', () => {
|
|
80
|
+
it('creates an agent that prepends the service URL', async () => {
|
|
81
|
+
const fetchFn = vi.fn<typeof globalThis.fetch>(async () =>
|
|
82
|
+
Response.json({ ok: true }),
|
|
83
|
+
)
|
|
84
|
+
const agent = buildAgent({
|
|
85
|
+
service: 'https://example.com',
|
|
86
|
+
fetch: fetchFn,
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
await agent.fetchHandler('/xrpc/io.example.test', { method: 'GET' })
|
|
90
|
+
|
|
91
|
+
expect(fetchFn).toHaveBeenCalledOnce()
|
|
92
|
+
const [url, init] = fetchFn.mock.calls[0]
|
|
93
|
+
expect(url).toEqual(
|
|
94
|
+
new URL('/xrpc/io.example.test', 'https://example.com'),
|
|
95
|
+
)
|
|
96
|
+
expect(init?.method).toBe('GET')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('has undefined did', () => {
|
|
100
|
+
const agent = buildAgent('https://example.com')
|
|
101
|
+
expect(agent.did).toBeUndefined()
|
|
102
|
+
})
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
describe('from URL instance', () => {
|
|
106
|
+
it('creates an agent with the URL as service', async () => {
|
|
107
|
+
const fetchFn = vi.fn<typeof globalThis.fetch>(async () =>
|
|
108
|
+
Response.json({ ok: true }),
|
|
109
|
+
)
|
|
110
|
+
const agent = buildAgent({
|
|
111
|
+
service: new URL('https://example.com'),
|
|
112
|
+
fetch: fetchFn,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
await agent.fetchHandler('/xrpc/io.example.test', { method: 'GET' })
|
|
116
|
+
|
|
117
|
+
expect(fetchFn).toHaveBeenCalledOnce()
|
|
118
|
+
const [url] = fetchFn.mock.calls[0]
|
|
119
|
+
expect(url).toEqual(
|
|
120
|
+
new URL('/xrpc/io.example.test', 'https://example.com'),
|
|
121
|
+
)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
describe('from AgentConfig', () => {
|
|
126
|
+
it('exposes did from config', () => {
|
|
127
|
+
const agent = buildAgent({
|
|
128
|
+
did: 'did:plc:test123',
|
|
129
|
+
service: 'https://example.com',
|
|
130
|
+
})
|
|
131
|
+
expect(agent.did).toBe('did:plc:test123')
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('reflects did changes on the config object', () => {
|
|
135
|
+
const config: AgentConfig = {
|
|
136
|
+
did: 'did:plc:original',
|
|
137
|
+
service: 'https://example.com',
|
|
138
|
+
}
|
|
139
|
+
const agent = buildAgent(config)
|
|
140
|
+
expect(agent.did).toBe('did:plc:original')
|
|
141
|
+
|
|
142
|
+
config.did = 'did:plc:updated'
|
|
143
|
+
expect(agent.did).toBe('did:plc:updated')
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('throws TypeError when fetch is not available', () => {
|
|
147
|
+
const originalFetch = globalThis.fetch
|
|
148
|
+
try {
|
|
149
|
+
// @ts-expect-error removing fetch to simulate missing environment
|
|
150
|
+
globalThis.fetch = undefined
|
|
151
|
+
expect(() => buildAgent({ service: 'https://example.com' })).toThrow(
|
|
152
|
+
TypeError,
|
|
153
|
+
)
|
|
154
|
+
} finally {
|
|
155
|
+
globalThis.fetch = originalFetch
|
|
156
|
+
}
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('headers', () => {
|
|
161
|
+
it('sends config headers when no request headers', async () => {
|
|
162
|
+
const fetchFn = vi.fn<typeof globalThis.fetch>(async () =>
|
|
163
|
+
Response.json({}),
|
|
164
|
+
)
|
|
165
|
+
const agent = buildAgent({
|
|
166
|
+
service: 'https://example.com',
|
|
167
|
+
headers: { Authorization: 'Bearer token123' },
|
|
168
|
+
fetch: fetchFn,
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
await agent.fetchHandler('/xrpc/test', { method: 'GET' })
|
|
172
|
+
|
|
173
|
+
const [, init] = fetchFn.mock.calls[0]
|
|
174
|
+
expect(init?.headers).toEqual({ Authorization: 'Bearer token123' })
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('sends request headers when no config headers', async () => {
|
|
178
|
+
const fetchFn = vi.fn<typeof globalThis.fetch>(async () =>
|
|
179
|
+
Response.json({}),
|
|
180
|
+
)
|
|
181
|
+
const agent = buildAgent({
|
|
182
|
+
service: 'https://example.com',
|
|
183
|
+
fetch: fetchFn,
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
await agent.fetchHandler('/xrpc/test', {
|
|
187
|
+
method: 'GET',
|
|
188
|
+
headers: { 'X-Custom': 'value' },
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const [, init] = fetchFn.mock.calls[0]
|
|
192
|
+
expect(init?.headers).toEqual({ 'X-Custom': 'value' })
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('merges config and request headers, with request taking priority', async () => {
|
|
196
|
+
const fetchFn = vi.fn<typeof globalThis.fetch>(async () =>
|
|
197
|
+
Response.json({}),
|
|
198
|
+
)
|
|
199
|
+
const agent = buildAgent({
|
|
200
|
+
service: 'https://example.com',
|
|
201
|
+
headers: { Authorization: 'Bearer default', 'X-Default': 'yes' },
|
|
202
|
+
fetch: fetchFn,
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
await agent.fetchHandler('/xrpc/test', {
|
|
206
|
+
method: 'GET',
|
|
207
|
+
headers: { Authorization: 'Bearer override' },
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
const [, init] = fetchFn.mock.calls[0]
|
|
211
|
+
const headers = new Headers(init?.headers)
|
|
212
|
+
expect(headers.get('Authorization')).toBe('Bearer override')
|
|
213
|
+
expect(headers.get('X-Default')).toBe('yes')
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
})
|
package/src/agent.ts
CHANGED
|
@@ -101,7 +101,7 @@ export type AgentConfig = {
|
|
|
101
101
|
*
|
|
102
102
|
* Can be a full {@link AgentConfig} object, or a simple service URL string/{@link URL}.
|
|
103
103
|
*/
|
|
104
|
-
export type AgentOptions = AgentConfig | string | URL
|
|
104
|
+
export type AgentOptions = AgentConfig | FetchHandler | string | URL
|
|
105
105
|
|
|
106
106
|
/**
|
|
107
107
|
* Creates an {@link Agent} from various input types.
|
|
@@ -132,12 +132,14 @@ export function buildAgent<O extends Agent | AgentOptions>(
|
|
|
132
132
|
options: O,
|
|
133
133
|
): O extends Agent ? O : Agent
|
|
134
134
|
export function buildAgent(options: Agent | AgentOptions): Agent {
|
|
135
|
-
if (isAgent(options)) return options
|
|
136
|
-
|
|
137
135
|
const config: Agent | AgentConfig =
|
|
138
|
-
typeof options === '
|
|
139
|
-
? { did: undefined,
|
|
140
|
-
: options
|
|
136
|
+
typeof options === 'function'
|
|
137
|
+
? { did: undefined, fetchHandler: options }
|
|
138
|
+
: typeof options === 'string' || options instanceof URL
|
|
139
|
+
? { did: undefined, service: options }
|
|
140
|
+
: options
|
|
141
|
+
|
|
142
|
+
if (isAgent(config)) return config
|
|
141
143
|
|
|
142
144
|
const { service, fetch = globalThis.fetch } = config
|
|
143
145
|
|
package/src/errors.test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
|
|
|
2
2
|
import { IssueInvalidType, LexValidationError, l } from '@atproto/lex-schema'
|
|
3
3
|
import {
|
|
4
4
|
XrpcAuthenticationError,
|
|
5
|
+
XrpcFetchError,
|
|
5
6
|
XrpcInternalError,
|
|
6
7
|
XrpcInvalidResponseError,
|
|
7
8
|
XrpcResponseError,
|
|
@@ -13,14 +14,14 @@ import {
|
|
|
13
14
|
const testQuery = l.query(
|
|
14
15
|
'io.example.test',
|
|
15
16
|
l.params(),
|
|
16
|
-
l.
|
|
17
|
+
l.jsonPayload({ value: l.string() }),
|
|
17
18
|
['TestError', 'AnotherError'],
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
const testQueryNoErrors = l.query(
|
|
21
22
|
'io.example.noErrors',
|
|
22
23
|
l.params(),
|
|
23
|
-
l.
|
|
24
|
+
l.jsonPayload({ value: l.string() }),
|
|
24
25
|
)
|
|
25
26
|
|
|
26
27
|
// ============================================================================
|
|
@@ -42,6 +43,7 @@ describe(XrpcResponseError, () => {
|
|
|
42
43
|
|
|
43
44
|
it('exposes status from the response', () => {
|
|
44
45
|
const err = createResponseError(404, 'NotFound')
|
|
46
|
+
expect(err.reason).toBe(err)
|
|
45
47
|
expect(err.status).toBe(404)
|
|
46
48
|
})
|
|
47
49
|
|
|
@@ -54,6 +56,7 @@ describe(XrpcResponseError, () => {
|
|
|
54
56
|
encoding: 'application/json',
|
|
55
57
|
body: { error: 'TestError' },
|
|
56
58
|
})
|
|
59
|
+
expect(err.reason).toBe(err)
|
|
57
60
|
expect(err.headers.get('X-Test')).toBe('value')
|
|
58
61
|
})
|
|
59
62
|
|
|
@@ -63,7 +66,7 @@ describe(XrpcResponseError, () => {
|
|
|
63
66
|
})
|
|
64
67
|
|
|
65
68
|
describe('toDownstreamError', () => {
|
|
66
|
-
it('returns 502 for
|
|
69
|
+
it('returns 502 for upstream 500 errors', () => {
|
|
67
70
|
const err = createResponseError(
|
|
68
71
|
500,
|
|
69
72
|
'InternalServerError',
|
|
@@ -78,6 +81,17 @@ describe(XrpcResponseError, () => {
|
|
|
78
81
|
})
|
|
79
82
|
})
|
|
80
83
|
|
|
84
|
+
it('preserves original status for non-500 5xx errors', () => {
|
|
85
|
+
const err = createResponseError(503, 'ServiceUnavailable', 'Try later')
|
|
86
|
+
const downstream = err.toDownstreamError()
|
|
87
|
+
|
|
88
|
+
expect(downstream.status).toBe(503)
|
|
89
|
+
expect(downstream.body).toEqual({
|
|
90
|
+
error: 'ServiceUnavailable',
|
|
91
|
+
message: 'Try later',
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
|
|
81
95
|
it('preserves original status for 4xx errors', () => {
|
|
82
96
|
const err = createResponseError(404, 'NotFound', 'Record not found')
|
|
83
97
|
const downstream = err.toDownstreamError()
|
|
@@ -159,6 +173,7 @@ describe(XrpcAuthenticationError, () => {
|
|
|
159
173
|
encoding: 'application/json',
|
|
160
174
|
body: { error: 'AuthenticationRequired' },
|
|
161
175
|
})
|
|
176
|
+
expect(err.reason).toBe(err)
|
|
162
177
|
expect(err.wwwAuthenticate).toHaveProperty('Bearer')
|
|
163
178
|
})
|
|
164
179
|
|
|
@@ -195,6 +210,7 @@ describe(XrpcUpstreamError, () => {
|
|
|
195
210
|
it('has error code UpstreamFailure', () => {
|
|
196
211
|
const response = new Response(null, { status: 200 })
|
|
197
212
|
const err = new XrpcUpstreamError(testQuery, response)
|
|
213
|
+
expect(err.reason).toBe(err)
|
|
198
214
|
expect(err.error).toBe('UpstreamFailure')
|
|
199
215
|
})
|
|
200
216
|
|
|
@@ -236,6 +252,7 @@ describe(XrpcInvalidResponseError, () => {
|
|
|
236
252
|
)
|
|
237
253
|
|
|
238
254
|
expect(err).toBeInstanceOf(XrpcUpstreamError)
|
|
255
|
+
expect(err.reason).toBe(err)
|
|
239
256
|
expect(err.error).toBe('UpstreamFailure')
|
|
240
257
|
expect(err.cause).toBe(validationError)
|
|
241
258
|
})
|
|
@@ -277,14 +294,10 @@ describe(XrpcInvalidResponseError, () => {
|
|
|
277
294
|
describe(XrpcInternalError, () => {
|
|
278
295
|
it('has error code InternalServerError', () => {
|
|
279
296
|
const err = new XrpcInternalError(testQuery)
|
|
297
|
+
expect(err.reason).toBe(err)
|
|
280
298
|
expect(err.error).toBe('InternalServerError')
|
|
281
299
|
})
|
|
282
300
|
|
|
283
|
-
it('is always retryable', () => {
|
|
284
|
-
const err = new XrpcInternalError(testQuery)
|
|
285
|
-
expect(err.shouldRetry()).toBe(true)
|
|
286
|
-
})
|
|
287
|
-
|
|
288
301
|
it('toJSON does not expose internal details', () => {
|
|
289
302
|
const err = new XrpcInternalError(
|
|
290
303
|
testQuery,
|
|
@@ -305,6 +318,62 @@ describe(XrpcInternalError, () => {
|
|
|
305
318
|
expect(downstream.body.error).toBe('InternalServerError')
|
|
306
319
|
expect(downstream.body.message).toBe('Internal Server Error')
|
|
307
320
|
})
|
|
321
|
+
|
|
322
|
+
it('is not retryable', () => {
|
|
323
|
+
const err = new XrpcInternalError(testQuery, 'something broke')
|
|
324
|
+
expect(err.shouldRetry()).toBe(false)
|
|
325
|
+
})
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
// ============================================================================
|
|
329
|
+
// XrpcFetchError
|
|
330
|
+
// ============================================================================
|
|
331
|
+
|
|
332
|
+
describe(XrpcFetchError, () => {
|
|
333
|
+
it('extends XrpcInternalError', () => {
|
|
334
|
+
const err = new XrpcFetchError(testQuery, new TypeError('fetch failed'))
|
|
335
|
+
expect(err).toBeInstanceOf(XrpcInternalError)
|
|
336
|
+
expect(err.error).toBe('InternalServerError')
|
|
337
|
+
})
|
|
338
|
+
|
|
339
|
+
it('uses cause message when cause is an Error', () => {
|
|
340
|
+
const cause = new TypeError('Failed to fetch')
|
|
341
|
+
const err = new XrpcFetchError(testQuery, cause)
|
|
342
|
+
expect(err.message).toBe('Unexpected fetchHandler() error: Failed to fetch')
|
|
343
|
+
expect(err.cause).toBe(cause)
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
it('uses fallback message when cause is not an Error', () => {
|
|
347
|
+
const err = new XrpcFetchError(testQuery, 'string cause')
|
|
348
|
+
expect(err.message).toBe('Unexpected fetchHandler() error: string cause')
|
|
349
|
+
expect(err.cause).toBe('string cause')
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
it('is retryable', () => {
|
|
353
|
+
const err = new XrpcFetchError(testQuery, new Error('network timeout'))
|
|
354
|
+
expect(err.shouldRetry()).toBe(true)
|
|
355
|
+
})
|
|
356
|
+
|
|
357
|
+
it('toJSON does not expose internal details', () => {
|
|
358
|
+
const err = new XrpcFetchError(
|
|
359
|
+
testQuery,
|
|
360
|
+
new Error('ECONNREFUSED 10.0.0.1:443'),
|
|
361
|
+
)
|
|
362
|
+
const json = err.toJSON()
|
|
363
|
+
|
|
364
|
+
expect(json.error).toBe('InternalServerError')
|
|
365
|
+
expect(json.message).toBe('Failed to perform upstream request')
|
|
366
|
+
expect(json.message).not.toContain('ECONNREFUSED')
|
|
367
|
+
})
|
|
368
|
+
|
|
369
|
+
it('toDownstreamError returns 502', () => {
|
|
370
|
+
const err = new XrpcFetchError(testQuery, new Error('DNS lookup failed'))
|
|
371
|
+
const downstream = err.toDownstreamError()
|
|
372
|
+
|
|
373
|
+
expect(downstream.status).toBe(502)
|
|
374
|
+
expect(downstream.body.error).toBe('InternalServerError')
|
|
375
|
+
expect(downstream.body.message).toBe('Failed to perform upstream request')
|
|
376
|
+
})
|
|
308
377
|
})
|
|
309
378
|
|
|
310
379
|
// ============================================================================
|
package/src/errors.ts
CHANGED
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
ResultFailure,
|
|
8
8
|
lexErrorDataSchema,
|
|
9
9
|
} from '@atproto/lex-schema'
|
|
10
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
11
|
+
import { Agent } from './agent.js'
|
|
10
12
|
import { XrpcResponsePayload } from './util.js'
|
|
11
13
|
import {
|
|
12
14
|
WWWAuthenticate,
|
|
@@ -347,17 +349,11 @@ export class XrpcInvalidResponseError<
|
|
|
347
349
|
}
|
|
348
350
|
|
|
349
351
|
/**
|
|
350
|
-
* Error class for internal/client-side errors during XRPC requests.
|
|
352
|
+
* Error class for unexpected internal/client-side errors during XRPC requests.
|
|
351
353
|
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
* - Request timeouts
|
|
356
|
-
* - Request aborted via AbortSignal
|
|
357
|
-
* - Invalid request construction
|
|
358
|
-
*
|
|
359
|
-
* The error code is always 'InternalServerError' and these errors are
|
|
360
|
-
* optimistically considered retryable.
|
|
354
|
+
* The error code is always 'InternalServerError' and these errors not
|
|
355
|
+
* considered retryable as they stem from unforeseen issues in the
|
|
356
|
+
* implementation.
|
|
361
357
|
*
|
|
362
358
|
* @typeParam M - The XRPC method type
|
|
363
359
|
*/
|
|
@@ -379,15 +375,11 @@ export class XrpcInternalError<
|
|
|
379
375
|
return this
|
|
380
376
|
}
|
|
381
377
|
|
|
382
|
-
override shouldRetry():
|
|
383
|
-
|
|
384
|
-
// (by detecting network errors, timeouts, etc.). Since these cases are
|
|
385
|
-
// highly platform-dependent, we optimistically assume all internal
|
|
386
|
-
// errors are retryable.
|
|
387
|
-
return true
|
|
378
|
+
override shouldRetry(): boolean {
|
|
379
|
+
return false
|
|
388
380
|
}
|
|
389
381
|
|
|
390
|
-
override toJSON(): LexErrorData
|
|
382
|
+
override toJSON(): LexErrorData {
|
|
391
383
|
// @NOTE Do not expose internal error details to downstream clients
|
|
392
384
|
return { error: this.error, message: 'Internal Server Error' }
|
|
393
385
|
}
|
|
@@ -397,6 +389,49 @@ export class XrpcInternalError<
|
|
|
397
389
|
}
|
|
398
390
|
}
|
|
399
391
|
|
|
392
|
+
/**
|
|
393
|
+
* Special case of XrpcInternalError that specifically represents errors thrown
|
|
394
|
+
* by {@link Agent.fetchHandler} during the XRPC request. This includes:
|
|
395
|
+
* - Network errors (connection refused, DNS failure)
|
|
396
|
+
* - Request timeouts
|
|
397
|
+
* - Request aborted via AbortSignal
|
|
398
|
+
*
|
|
399
|
+
* These errors are optimistically considered retryable, as many fetch errors
|
|
400
|
+
* are transient and may succeed on retry.
|
|
401
|
+
*/
|
|
402
|
+
export class XrpcFetchError<
|
|
403
|
+
M extends Procedure | Query = Procedure | Query,
|
|
404
|
+
> extends XrpcInternalError<M> {
|
|
405
|
+
name = 'XrpcFetchError'
|
|
406
|
+
|
|
407
|
+
constructor(method: M, cause: unknown) {
|
|
408
|
+
const message = cause instanceof Error ? cause.message : String(cause)
|
|
409
|
+
super(method, `Unexpected fetchHandler() error: ${message}`, { cause })
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
override shouldRetry(): boolean {
|
|
413
|
+
// Ideally, we would inspect the reason to determine if it's retryable (by
|
|
414
|
+
// detecting network errors, timeouts, etc.). Since these cases are highly
|
|
415
|
+
// platform-dependent, we optimistically assume all fetch errors are
|
|
416
|
+
// transient and retryable.
|
|
417
|
+
return true
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
override toJSON(): LexErrorData {
|
|
421
|
+
// @NOTE Do not expose internal error details to downstream clients
|
|
422
|
+
return { error: this.error, message: 'Failed to perform upstream request' }
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
override toDownstreamError(): DownstreamError {
|
|
426
|
+
// While it might technically be a 500 error, we use 502 Bad Gateway here to
|
|
427
|
+
// indicate that the error occurred while communicating with the upstream
|
|
428
|
+
// server, allowing downstream clients to distinguish between errors in our
|
|
429
|
+
// internal processing (500) and errors in the upstream server or network
|
|
430
|
+
// (502).
|
|
431
|
+
return { status: 502, body: this.toJSON() }
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
400
435
|
/**
|
|
401
436
|
* Union type of all possible XRPC failure types.
|
|
402
437
|
*
|