@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/CHANGELOG.md +32 -0
- package/dist/client.d.ts +24 -21
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +14 -7
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +22 -22
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +62 -37
- package/dist/errors.js.map +1 -1
- package/dist/response.d.ts +66 -7
- package/dist/response.d.ts.map +1 -1
- package/dist/response.js +90 -69
- package/dist/response.js.map +1 -1
- package/dist/types.d.ts +8 -37
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -0
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +14 -27
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +15 -6
- package/dist/util.js.map +1 -1
- package/dist/xrpc.d.ts +40 -15
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +4 -2
- package/dist/xrpc.js.map +1 -1
- package/package.json +6 -6
- package/src/client.ts +83 -31
- package/src/errors.test.ts +243 -32
- package/src/errors.ts +91 -52
- package/src/response.ts +229 -102
- package/src/types.ts +17 -40
- package/src/util.test.ts +11 -11
- package/src/util.ts +33 -36
- package/src/xrpc.test.ts +691 -142
- package/src/xrpc.ts +73 -29
package/src/client.ts
CHANGED
|
@@ -20,10 +20,24 @@ import {
|
|
|
20
20
|
import { Agent, AgentOptions, buildAgent } from './agent.js'
|
|
21
21
|
import { XrpcFailure } from './errors.js'
|
|
22
22
|
import { com } from './lexicons/index.js'
|
|
23
|
-
import {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
import {
|
|
24
|
+
XrpcResponse,
|
|
25
|
+
XrpcResponseBody,
|
|
26
|
+
XrpcResponseOptions,
|
|
27
|
+
} from './response.js'
|
|
28
|
+
import { BinaryBodyInit, Service } from './types.js'
|
|
29
|
+
import {
|
|
30
|
+
XrpcRequestHeadersOptions,
|
|
31
|
+
applyDefaults,
|
|
32
|
+
buildXrpcRequestHeaders,
|
|
33
|
+
} from './util.js'
|
|
34
|
+
import {
|
|
35
|
+
XrpcOptions,
|
|
36
|
+
XrpcRequestParams,
|
|
37
|
+
XrpcRequestProcessingOptions,
|
|
38
|
+
xrpc,
|
|
39
|
+
xrpcSafe,
|
|
40
|
+
} from './xrpc.js'
|
|
27
41
|
|
|
28
42
|
export type {
|
|
29
43
|
AtIdentifierString,
|
|
@@ -58,13 +72,13 @@ export type {
|
|
|
58
72
|
* }
|
|
59
73
|
* ```
|
|
60
74
|
*/
|
|
61
|
-
export type ClientOptions =
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
|
|
75
|
+
export type ClientOptions = XrpcRequestHeadersOptions &
|
|
76
|
+
Pick<XrpcRequestProcessingOptions, 'validateRequest'> &
|
|
77
|
+
XrpcResponseOptions
|
|
78
|
+
|
|
79
|
+
export type ActionOptions = {
|
|
80
|
+
/** AbortSignal to cancel the request. */
|
|
81
|
+
signal?: AbortSignal
|
|
68
82
|
}
|
|
69
83
|
|
|
70
84
|
/**
|
|
@@ -87,7 +101,7 @@ export type ClientOptions = {
|
|
|
87
101
|
export type Action<I = any, O = any> = (
|
|
88
102
|
client: Client,
|
|
89
103
|
input: I,
|
|
90
|
-
options:
|
|
104
|
+
options: ActionOptions,
|
|
91
105
|
) => O | Promise<O>
|
|
92
106
|
|
|
93
107
|
/**
|
|
@@ -109,7 +123,10 @@ export type InferActionOutput<A extends Action> =
|
|
|
109
123
|
*
|
|
110
124
|
* @see {@link Client.createRecord}
|
|
111
125
|
*/
|
|
112
|
-
export type CreateRecordOptions =
|
|
126
|
+
export type CreateRecordOptions = Omit<
|
|
127
|
+
XrpcOptions<typeof com.atproto.repo.createRecord.main>,
|
|
128
|
+
'body'
|
|
129
|
+
> & {
|
|
113
130
|
/** Repository identifier (DID or handle). Defaults to authenticated user's DID. */
|
|
114
131
|
repo?: AtIdentifierString
|
|
115
132
|
/** Compare-and-swap on the repo commit. If specified, must match current commit. */
|
|
@@ -123,7 +140,10 @@ export type CreateRecordOptions = CallOptions & {
|
|
|
123
140
|
*
|
|
124
141
|
* @see {@link Client.deleteRecord}
|
|
125
142
|
*/
|
|
126
|
-
export type DeleteRecordOptions =
|
|
143
|
+
export type DeleteRecordOptions = Omit<
|
|
144
|
+
XrpcOptions<typeof com.atproto.repo.deleteRecord.main>,
|
|
145
|
+
'params'
|
|
146
|
+
> & {
|
|
127
147
|
/** Repository identifier (DID or handle). Defaults to authenticated user's DID. */
|
|
128
148
|
repo?: AtIdentifierString
|
|
129
149
|
/** Compare-and-swap on the repo commit. If specified, must match current commit. */
|
|
@@ -137,7 +157,10 @@ export type DeleteRecordOptions = CallOptions & {
|
|
|
137
157
|
*
|
|
138
158
|
* @see {@link Client.getRecord}
|
|
139
159
|
*/
|
|
140
|
-
export type GetRecordOptions =
|
|
160
|
+
export type GetRecordOptions = Omit<
|
|
161
|
+
XrpcOptions<typeof com.atproto.repo.getRecord.main>,
|
|
162
|
+
'params'
|
|
163
|
+
> & {
|
|
141
164
|
/** Repository identifier (DID or handle). Defaults to authenticated user's DID. */
|
|
142
165
|
repo?: AtIdentifierString
|
|
143
166
|
}
|
|
@@ -147,7 +170,10 @@ export type GetRecordOptions = CallOptions & {
|
|
|
147
170
|
*
|
|
148
171
|
* @see {@link Client.putRecord}
|
|
149
172
|
*/
|
|
150
|
-
export type PutRecordOptions =
|
|
173
|
+
export type PutRecordOptions = Omit<
|
|
174
|
+
XrpcOptions<typeof com.atproto.repo.putRecord.main>,
|
|
175
|
+
'body'
|
|
176
|
+
> & {
|
|
151
177
|
/** Repository identifier (DID or handle). Defaults to authenticated user's DID. */
|
|
152
178
|
repo?: AtIdentifierString
|
|
153
179
|
/** Compare-and-swap on the repo commit. If specified, must match current commit. */
|
|
@@ -163,7 +189,10 @@ export type PutRecordOptions = CallOptions & {
|
|
|
163
189
|
*
|
|
164
190
|
* @see {@link Client.listRecords}
|
|
165
191
|
*/
|
|
166
|
-
export type ListRecordsOptions =
|
|
192
|
+
export type ListRecordsOptions = Omit<
|
|
193
|
+
XrpcOptions<typeof com.atproto.repo.listRecords.main>,
|
|
194
|
+
'params'
|
|
195
|
+
> & {
|
|
167
196
|
/** Repository identifier (DID or handle). Defaults to authenticated user's DID. */
|
|
168
197
|
repo?: AtIdentifierString
|
|
169
198
|
/** Maximum number of records to return. */
|
|
@@ -174,6 +203,16 @@ export type ListRecordsOptions = CallOptions & {
|
|
|
174
203
|
reverse?: boolean
|
|
175
204
|
}
|
|
176
205
|
|
|
206
|
+
export type UploadBlobOptions = Omit<
|
|
207
|
+
XrpcOptions<typeof com.atproto.repo.uploadBlob.main>,
|
|
208
|
+
'body'
|
|
209
|
+
>
|
|
210
|
+
|
|
211
|
+
export type GetBlobOptions = Omit<
|
|
212
|
+
XrpcOptions<typeof com.atproto.sync.getBlob.main>,
|
|
213
|
+
'params'
|
|
214
|
+
>
|
|
215
|
+
|
|
177
216
|
export type RecordKeyOptions<
|
|
178
217
|
T extends RecordSchema,
|
|
179
218
|
AlsoOptionalWhenRecordKeyIs extends LexiconRecordKey = never,
|
|
@@ -315,11 +354,22 @@ export class Client implements Agent {
|
|
|
315
354
|
/** Set of labeler DIDs specific to this client instance. */
|
|
316
355
|
public readonly labelers: Set<DidString>
|
|
317
356
|
|
|
357
|
+
public readonly xrpcDefaults: {
|
|
358
|
+
readonly validateRequest: boolean
|
|
359
|
+
readonly validateResponse: boolean
|
|
360
|
+
readonly strictResponseProcessing: boolean
|
|
361
|
+
}
|
|
362
|
+
|
|
318
363
|
constructor(agent: Agent | AgentOptions, options: ClientOptions = {}) {
|
|
319
364
|
this.agent = buildAgent(agent)
|
|
320
365
|
this.service = options.service
|
|
321
366
|
this.labelers = new Set(options.labelers)
|
|
322
367
|
this.headers = new Headers(options.headers)
|
|
368
|
+
this.xrpcDefaults = Object.freeze({
|
|
369
|
+
validateRequest: options.validateRequest ?? false,
|
|
370
|
+
validateResponse: options.validateResponse ?? true,
|
|
371
|
+
strictResponseProcessing: options.strictResponseProcessing ?? true,
|
|
372
|
+
})
|
|
323
373
|
}
|
|
324
374
|
|
|
325
375
|
/**
|
|
@@ -392,7 +442,7 @@ export class Client implements Agent {
|
|
|
392
442
|
path: `/${string}`,
|
|
393
443
|
init: RequestInit,
|
|
394
444
|
): Promise<Response> {
|
|
395
|
-
const headers =
|
|
445
|
+
const headers = buildXrpcRequestHeaders({
|
|
396
446
|
headers: init.headers,
|
|
397
447
|
service: this.service,
|
|
398
448
|
labelers: [
|
|
@@ -454,7 +504,7 @@ export class Client implements Agent {
|
|
|
454
504
|
ns: Main<M>,
|
|
455
505
|
options: XrpcOptions<M> = {} as XrpcOptions<M>,
|
|
456
506
|
): Promise<XrpcResponse<M>> {
|
|
457
|
-
return xrpc(this, ns, options)
|
|
507
|
+
return xrpc(this, ns, applyDefaults(options, this.xrpcDefaults))
|
|
458
508
|
}
|
|
459
509
|
|
|
460
510
|
/**
|
|
@@ -493,7 +543,7 @@ export class Client implements Agent {
|
|
|
493
543
|
ns: Main<M>,
|
|
494
544
|
options: XrpcOptions<M> = {} as XrpcOptions<M>,
|
|
495
545
|
): Promise<XrpcResponse<M> | XrpcFailure<M>> {
|
|
496
|
-
return xrpcSafe(this, ns, options)
|
|
546
|
+
return xrpcSafe(this, ns, applyDefaults(options, this.xrpcDefaults))
|
|
497
547
|
}
|
|
498
548
|
|
|
499
549
|
/**
|
|
@@ -649,14 +699,8 @@ export class Client implements Agent {
|
|
|
649
699
|
* console.log(response.body.blob) // Use this ref in records
|
|
650
700
|
* ```
|
|
651
701
|
*/
|
|
652
|
-
async uploadBlob(
|
|
653
|
-
body
|
|
654
|
-
options?: CallOptions & { encoding?: `${string}/${string}` },
|
|
655
|
-
) {
|
|
656
|
-
return this.xrpc(com.atproto.repo.uploadBlob.main, {
|
|
657
|
-
...options,
|
|
658
|
-
body,
|
|
659
|
-
})
|
|
702
|
+
async uploadBlob(body: BinaryBodyInit, options?: UploadBlobOptions) {
|
|
703
|
+
return this.xrpc(com.atproto.repo.uploadBlob.main, { ...options, body })
|
|
660
704
|
}
|
|
661
705
|
|
|
662
706
|
/**
|
|
@@ -666,7 +710,7 @@ export class Client implements Agent {
|
|
|
666
710
|
* @param cid - The CID of the blob
|
|
667
711
|
* @param options - Call options
|
|
668
712
|
*/
|
|
669
|
-
async getBlob(did: DidString, cid: CidString, options?:
|
|
713
|
+
async getBlob(did: DidString, cid: CidString, options?: GetBlobOptions) {
|
|
670
714
|
return this.xrpc(com.atproto.sync.getBlob.main, {
|
|
671
715
|
...options,
|
|
672
716
|
params: { did, cid },
|
|
@@ -723,7 +767,13 @@ export class Client implements Agent {
|
|
|
723
767
|
: T extends Query
|
|
724
768
|
? XrpcRequestParams<T>
|
|
725
769
|
: never,
|
|
726
|
-
options?:
|
|
770
|
+
options?: T extends Action
|
|
771
|
+
? ActionOptions
|
|
772
|
+
: T extends Procedure
|
|
773
|
+
? Omit<XrpcOptions<T>, 'body'>
|
|
774
|
+
: T extends Query
|
|
775
|
+
? Omit<XrpcOptions<T>, 'params'>
|
|
776
|
+
: never,
|
|
727
777
|
): Promise<
|
|
728
778
|
T extends Action
|
|
729
779
|
? InferActionOutput<T>
|
|
@@ -736,7 +786,7 @@ export class Client implements Agent {
|
|
|
736
786
|
public async call(
|
|
737
787
|
ns: Main<Action> | Main<Procedure> | Main<Query>,
|
|
738
788
|
arg?: LexValue | Params,
|
|
739
|
-
options:
|
|
789
|
+
options: ActionOptions = {},
|
|
740
790
|
): Promise<unknown> {
|
|
741
791
|
const method = getMain(ns)
|
|
742
792
|
|
|
@@ -797,6 +847,7 @@ export class Client implements Agent {
|
|
|
797
847
|
): Promise<CreateOutput> {
|
|
798
848
|
const schema: T = getMain(ns)
|
|
799
849
|
const record = schema.build(input) as TypedLexMap<NsidString>
|
|
850
|
+
if (options?.validateRequest) schema.validate(record)
|
|
800
851
|
const rkey = options.rkey ?? getDefaultRecordKey(schema)
|
|
801
852
|
if (rkey !== undefined) schema.keySchema.assert(rkey)
|
|
802
853
|
const response = await this.createRecord(record, rkey, options)
|
|
@@ -893,6 +944,7 @@ export class Client implements Agent {
|
|
|
893
944
|
): Promise<PutOutput> {
|
|
894
945
|
const schema: T = getMain(ns)
|
|
895
946
|
const record = schema.build(input) as TypedLexMap<NsidString>
|
|
947
|
+
if (options?.validateRequest) schema.validate(record)
|
|
896
948
|
const rkey = options.rkey ?? getLiteralRecordKey(schema)
|
|
897
949
|
const response = await this.putRecord(record, rkey, options)
|
|
898
950
|
return response.body
|
package/src/errors.test.ts
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
XrpcInternalError,
|
|
7
7
|
XrpcInvalidResponseError,
|
|
8
8
|
XrpcResponseError,
|
|
9
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
//
|
|
411
|
+
// XrpcInvalidResponseError
|
|
207
412
|
// ============================================================================
|
|
208
413
|
|
|
209
|
-
describe(
|
|
210
|
-
it('has error code
|
|
211
|
-
const response = new Response(null, { status:
|
|
212
|
-
const err = new
|
|
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('
|
|
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:
|
|
219
|
-
const err = new
|
|
220
|
-
|
|
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
|
|
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:
|
|
232
|
-
const err = new
|
|
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
|
-
//
|
|
449
|
+
// XrpcResponseValidationError
|
|
239
450
|
// ============================================================================
|
|
240
451
|
|
|
241
|
-
describe(
|
|
242
|
-
it('extends
|
|
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
|
|
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(
|
|
465
|
+
expect(err).toBeInstanceOf(XrpcInvalidResponseError)
|
|
255
466
|
expect(err.reason).toBe(err)
|
|
256
|
-
expect(err.error).toBe('
|
|
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
|
|
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
|
|
490
|
+
const err = new XrpcResponseValidationError(
|
|
280
491
|
testQuery,
|
|
281
492
|
new Response(null, { status: 200 }),
|
|
282
493
|
{ encoding: 'application/json', body: { value: 42 } },
|