@atproto/lex-client 0.0.9 → 0.0.11
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 +30 -0
- package/LICENSE.txt +1 -1
- package/dist/agent.d.ts +5 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +15 -1
- package/dist/agent.js.map +1 -1
- package/dist/client.d.ts +59 -40
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -6
- package/dist/client.js.map +1 -1
- package/dist/errors.d.ts +52 -51
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +90 -71
- package/dist/errors.js.map +1 -1
- package/dist/lexicons/com/atproto/repo/createRecord.defs.d.ts +20 -10
- package/dist/lexicons/com/atproto/repo/createRecord.defs.d.ts.map +1 -1
- package/dist/lexicons/com/atproto/repo/defs.defs.d.ts +1 -1
- package/dist/lexicons/com/atproto/repo/defs.defs.d.ts.map +1 -1
- package/dist/lexicons/com/atproto/repo/deleteRecord.defs.d.ts +14 -6
- package/dist/lexicons/com/atproto/repo/deleteRecord.defs.d.ts.map +1 -1
- package/dist/lexicons/com/atproto/repo/getRecord.defs.d.ts +12 -4
- package/dist/lexicons/com/atproto/repo/getRecord.defs.d.ts.map +1 -1
- package/dist/lexicons/com/atproto/repo/listRecords.defs.d.ts +11 -11
- package/dist/lexicons/com/atproto/repo/listRecords.defs.d.ts.map +1 -1
- package/dist/lexicons/com/atproto/repo/listRecords.defs.js +2 -1
- package/dist/lexicons/com/atproto/repo/listRecords.defs.js.map +1 -1
- package/dist/lexicons/com/atproto/repo/putRecord.defs.d.ts +18 -10
- package/dist/lexicons/com/atproto/repo/putRecord.defs.d.ts.map +1 -1
- package/dist/response.d.ts +14 -13
- package/dist/response.d.ts.map +1 -1
- package/dist/response.js +36 -35
- package/dist/response.js.map +1 -1
- package/dist/util.d.ts +1 -1
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js.map +1 -1
- package/dist/www-authenticate.d.ts +12 -0
- package/dist/www-authenticate.d.ts.map +1 -0
- package/dist/www-authenticate.js +57 -0
- package/dist/www-authenticate.js.map +1 -0
- package/dist/xrpc.d.ts +14 -21
- package/dist/xrpc.d.ts.map +1 -1
- package/dist/xrpc.js +18 -35
- package/dist/xrpc.js.map +1 -1
- package/package.json +6 -6
- package/src/agent.ts +34 -1
- package/src/client.ts +34 -33
- package/src/errors.ts +161 -128
- package/src/lexicons/com/atproto/repo/listRecords.defs.ts +4 -1
- package/src/response.ts +71 -71
- package/src/util.ts +1 -1
- package/src/www-authenticate.test.ts +227 -0
- package/src/www-authenticate.ts +77 -0
- package/src/xrpc.ts +53 -95
package/src/errors.ts
CHANGED
|
@@ -1,28 +1,26 @@
|
|
|
1
1
|
import { LexError, LexErrorCode, LexErrorData } from '@atproto/lex-data'
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
InferMethodError,
|
|
4
|
+
Procedure,
|
|
5
|
+
Query,
|
|
6
|
+
ResultFailure,
|
|
7
|
+
lexErrorDataSchema,
|
|
8
|
+
} from '@atproto/lex-schema'
|
|
9
|
+
import { XrpcPayload } from './util.js'
|
|
10
|
+
import {
|
|
11
|
+
WWWAuthenticate,
|
|
12
|
+
parseWWWAuthenticateHeader,
|
|
13
|
+
} from './www-authenticate.js'
|
|
14
|
+
|
|
15
|
+
export const RETRYABLE_HTTP_STATUS_CODES: ReadonlySet<number> = new Set([
|
|
16
|
+
408, 425, 429, 500, 502, 503, 504, 522, 524,
|
|
17
|
+
])
|
|
4
18
|
|
|
5
19
|
export { LexError }
|
|
6
20
|
export type { LexErrorCode, LexErrorData }
|
|
7
21
|
|
|
8
|
-
export type
|
|
9
|
-
LexErrorData<N>,
|
|
10
|
-
'application/json'
|
|
11
|
-
>
|
|
12
|
-
|
|
13
|
-
export class LexRpcError<
|
|
14
|
-
N extends LexErrorCode = LexErrorCode,
|
|
15
|
-
> extends LexError<N> {
|
|
16
|
-
name = 'LexRpcError'
|
|
17
|
-
|
|
18
|
-
constructor(
|
|
19
|
-
error: N,
|
|
20
|
-
message: string = `${error} Lexicon RPC error`,
|
|
21
|
-
options?: ErrorOptions,
|
|
22
|
-
) {
|
|
23
|
-
super(error, message, options)
|
|
24
|
-
}
|
|
25
|
-
}
|
|
22
|
+
export type XrpcErrorPayload<N extends LexErrorCode = LexErrorCode> =
|
|
23
|
+
XrpcPayload<LexErrorData<N>, 'application/json'>
|
|
26
24
|
|
|
27
25
|
/**
|
|
28
26
|
* All unsuccessful responses should follow a standard error response
|
|
@@ -36,174 +34,209 @@ export class LexRpcError<
|
|
|
36
34
|
*
|
|
37
35
|
* This function checks whether a given payload matches this schema.
|
|
38
36
|
*/
|
|
39
|
-
export function
|
|
40
|
-
payload:
|
|
41
|
-
): payload is
|
|
37
|
+
export function isXrpcErrorPayload(
|
|
38
|
+
payload: XrpcPayload | null,
|
|
39
|
+
): payload is XrpcErrorPayload {
|
|
42
40
|
return (
|
|
43
41
|
payload !== null &&
|
|
44
42
|
payload.encoding === 'application/json' &&
|
|
45
|
-
|
|
43
|
+
lexErrorDataSchema.matches(payload.body)
|
|
46
44
|
)
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
*/
|
|
52
|
-
type LexRpcFailureResult<N extends LexErrorCode, E> = l.ResultFailure<E> & {
|
|
53
|
-
readonly error: N
|
|
54
|
-
shouldRetry(): boolean
|
|
55
|
-
matchesSchema(): boolean
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Class used to represent an HTTP request that resulted in an XRPC method error
|
|
60
|
-
* That is, a non-2xx response with a valid XRPC error payload.
|
|
61
|
-
*/
|
|
62
|
-
export class LexRpcResponseError<
|
|
63
|
-
M extends l.Procedure | l.Query = l.Procedure | l.Query,
|
|
47
|
+
export abstract class XrpcError<
|
|
48
|
+
M extends Procedure | Query = Procedure | Query,
|
|
64
49
|
N extends LexErrorCode = LexErrorCode,
|
|
50
|
+
TReason = unknown,
|
|
65
51
|
>
|
|
66
|
-
extends
|
|
67
|
-
implements
|
|
52
|
+
extends LexError<N>
|
|
53
|
+
implements ResultFailure<TReason>
|
|
68
54
|
{
|
|
69
|
-
name = '
|
|
55
|
+
name = 'XrpcError'
|
|
70
56
|
|
|
71
57
|
constructor(
|
|
72
58
|
readonly method: M,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
readonly payload: LexRpcErrorPayload<N>,
|
|
59
|
+
error: N,
|
|
60
|
+
message: string = `${error} Lexicon RPC error`,
|
|
76
61
|
options?: ErrorOptions,
|
|
77
62
|
) {
|
|
78
|
-
const { error, message } = payload.body
|
|
79
63
|
super(error, message, options)
|
|
80
64
|
}
|
|
81
65
|
|
|
82
|
-
|
|
66
|
+
/**
|
|
67
|
+
* @see {@link ResultFailure.success}
|
|
68
|
+
*/
|
|
69
|
+
readonly success = false as const
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @see {@link ResultFailure.reason}
|
|
73
|
+
*/
|
|
74
|
+
abstract readonly reason: TReason
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Indicates whether the error is transient and can be retried.
|
|
78
|
+
*/
|
|
79
|
+
abstract shouldRetry(): boolean
|
|
83
80
|
|
|
84
|
-
|
|
85
|
-
return this
|
|
81
|
+
matchesSchema(): this is XrpcError<M, InferMethodError<M>> {
|
|
82
|
+
return this.method.errors?.includes(this.error) ?? false
|
|
86
83
|
}
|
|
84
|
+
}
|
|
87
85
|
|
|
88
|
-
|
|
89
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Class used to represent an HTTP request that resulted in an XRPC method
|
|
88
|
+
* error. That is, a non-2xx response with a valid XRPC error payload.
|
|
89
|
+
*/
|
|
90
|
+
export class XrpcResponseError<
|
|
91
|
+
M extends Procedure | Query = Procedure | Query,
|
|
92
|
+
N extends LexErrorCode = InferMethodError<M> | LexErrorCode,
|
|
93
|
+
> extends XrpcError<M, N, XrpcResponseError<M, N>> {
|
|
94
|
+
name = 'XrpcResponseError'
|
|
95
|
+
|
|
96
|
+
constructor(
|
|
97
|
+
method: M,
|
|
98
|
+
readonly response: Response,
|
|
99
|
+
readonly payload: XrpcErrorPayload<N>,
|
|
100
|
+
options?: ErrorOptions,
|
|
101
|
+
) {
|
|
102
|
+
const { error, message } = payload.body
|
|
103
|
+
super(method, error, message, options)
|
|
90
104
|
}
|
|
91
105
|
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
override get reason(): this {
|
|
107
|
+
return this
|
|
94
108
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return
|
|
109
|
+
|
|
110
|
+
override shouldRetry(): boolean {
|
|
111
|
+
return RETRYABLE_HTTP_STATUS_CODES.has(this.response.status)
|
|
98
112
|
}
|
|
99
113
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
114
|
+
override toJSON() {
|
|
115
|
+
return this.payload.body
|
|
116
|
+
}
|
|
103
117
|
|
|
104
|
-
|
|
118
|
+
override toResponse(): Response {
|
|
119
|
+
// Re-expose schema-valid errors as-is to downstream clients
|
|
120
|
+
if (this.matchesSchema()) {
|
|
121
|
+
const status = this.response.status >= 500 ? 502 : this.response.status
|
|
122
|
+
return Response.json(this.toJSON(), { status })
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return this.response.status >= 500
|
|
126
|
+
? // The upstream server had an error, return a generic upstream failure
|
|
127
|
+
Response.json({ error: 'UpstreamFailure' }, { status: 502 })
|
|
128
|
+
: // If the error is on our side, return a generic internal server error
|
|
129
|
+
Response.json({ error: 'InternalServerError' }, { status: 500 })
|
|
105
130
|
}
|
|
106
131
|
|
|
107
|
-
|
|
132
|
+
get body(): LexErrorData {
|
|
108
133
|
return this.payload.body
|
|
109
134
|
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export type { WWWAuthenticate }
|
|
138
|
+
export class XrpcAuthenticationError<
|
|
139
|
+
M extends Procedure | Query = Procedure | Query,
|
|
140
|
+
N extends LexErrorCode = LexErrorCode,
|
|
141
|
+
> extends XrpcResponseError<M, N> {
|
|
142
|
+
name = 'XrpcAuthenticationError'
|
|
143
|
+
|
|
144
|
+
override shouldRetry(): boolean {
|
|
145
|
+
return false
|
|
146
|
+
}
|
|
110
147
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return
|
|
148
|
+
#wwwAuthenticate?: WWWAuthenticate
|
|
149
|
+
get wwwAuthenticate(): WWWAuthenticate {
|
|
150
|
+
return (this.#wwwAuthenticate ??=
|
|
151
|
+
parseWWWAuthenticateHeader(
|
|
152
|
+
this.response.headers.get('www-authenticate'),
|
|
153
|
+
) ?? {})
|
|
114
154
|
}
|
|
115
155
|
}
|
|
116
156
|
|
|
117
157
|
/**
|
|
118
|
-
* This class represents
|
|
158
|
+
* This class represents invalid or unprocessable XRPC response from the
|
|
159
|
+
* upstream server.
|
|
119
160
|
*/
|
|
120
|
-
export class
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
>
|
|
125
|
-
extends LexRpcError<N>
|
|
126
|
-
implements LexRpcFailureResult<N, LexRpcUpstreamError<N>>
|
|
127
|
-
{
|
|
128
|
-
name = 'LexRpcUpstreamError' as const
|
|
129
|
-
|
|
130
|
-
// For debugging purposes, we keep the response details here
|
|
131
|
-
readonly response: {
|
|
132
|
-
status: number
|
|
133
|
-
headers: Headers
|
|
134
|
-
payload: Payload | null
|
|
135
|
-
}
|
|
161
|
+
export class XrpcUpstreamError<
|
|
162
|
+
M extends Procedure | Query = Procedure | Query,
|
|
163
|
+
> extends XrpcError<M, 'UpstreamFailure', XrpcUpstreamError<M>> {
|
|
164
|
+
name = 'XrpcUpstreamError'
|
|
136
165
|
|
|
137
166
|
constructor(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
167
|
+
method: M,
|
|
168
|
+
readonly response: Response,
|
|
169
|
+
readonly payload: XrpcPayload | null,
|
|
170
|
+
message: string = `Unexpected upstream XRPC response`,
|
|
142
171
|
options?: ErrorOptions,
|
|
143
172
|
) {
|
|
144
|
-
super(
|
|
145
|
-
this.response = {
|
|
146
|
-
status: response.status,
|
|
147
|
-
headers: response.headers,
|
|
148
|
-
payload,
|
|
149
|
-
}
|
|
173
|
+
super(method, 'UpstreamFailure', message, options)
|
|
150
174
|
}
|
|
151
175
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
get reason(): this {
|
|
176
|
+
override get reason(): this {
|
|
155
177
|
return this
|
|
156
178
|
}
|
|
157
179
|
|
|
158
|
-
|
|
159
|
-
return
|
|
180
|
+
override shouldRetry(): boolean {
|
|
181
|
+
return RETRYABLE_HTTP_STATUS_CODES.has(this.response.status)
|
|
160
182
|
}
|
|
161
183
|
|
|
162
|
-
|
|
163
|
-
// Do not retry client errors
|
|
164
|
-
return this.response.status >= 500
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
toResponse(): Response {
|
|
184
|
+
override toResponse(): Response {
|
|
168
185
|
return Response.json(this.toJSON(), { status: 502 })
|
|
169
186
|
}
|
|
170
187
|
}
|
|
171
188
|
|
|
172
|
-
export class
|
|
173
|
-
extends
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
name = 'LexRpcUnexpectedError' as const
|
|
189
|
+
export class XrpcInternalError<
|
|
190
|
+
M extends Procedure | Query = Procedure | Query,
|
|
191
|
+
> extends XrpcError<M, 'InternalServerError', XrpcInternalError<M>> {
|
|
192
|
+
name = 'XrpcInternalError'
|
|
177
193
|
|
|
178
|
-
|
|
179
|
-
super(
|
|
194
|
+
constructor(method: M, message?: string, options?: ErrorOptions) {
|
|
195
|
+
super(
|
|
196
|
+
method,
|
|
197
|
+
'InternalServerError',
|
|
198
|
+
message ?? 'Unable to fulfill XRPC request',
|
|
199
|
+
options,
|
|
200
|
+
)
|
|
180
201
|
}
|
|
181
202
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
get reason() {
|
|
185
|
-
return this.cause
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
matchesSchema(): false {
|
|
189
|
-
return false
|
|
203
|
+
override get reason(): this {
|
|
204
|
+
return this
|
|
190
205
|
}
|
|
191
206
|
|
|
192
|
-
shouldRetry():
|
|
207
|
+
override shouldRetry(): true {
|
|
208
|
+
// Ideally, we would inspect the reason to determine if it's retryable
|
|
209
|
+
// (by detecting network errors, timeouts, etc.). Since these cases are
|
|
210
|
+
// highly platform-dependent, we optimistically assume all internal
|
|
211
|
+
// errors are retryable.
|
|
193
212
|
return true
|
|
194
213
|
}
|
|
195
214
|
|
|
196
|
-
toResponse(): Response {
|
|
197
|
-
|
|
215
|
+
override toResponse(): Response {
|
|
216
|
+
// Do not expose internal error details to downstream clients
|
|
217
|
+
return Response.json({ error: this.error }, { status: 500 })
|
|
198
218
|
}
|
|
219
|
+
}
|
|
199
220
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
221
|
+
export type XrpcFailure<M extends Procedure | Query = Procedure | Query> =
|
|
222
|
+
// The server returned a valid XRPC error response
|
|
223
|
+
| XrpcResponseError<M>
|
|
224
|
+
// The response was not a valid XRPC response, or it does not match the schema
|
|
225
|
+
| XrpcUpstreamError<M>
|
|
226
|
+
// Something went wrong (network error, etc.)
|
|
227
|
+
| XrpcInternalError<M>
|
|
228
|
+
|
|
229
|
+
export function asXrpcFailure<M extends Procedure | Query>(
|
|
230
|
+
method: M,
|
|
231
|
+
cause: unknown,
|
|
232
|
+
): XrpcFailure<M> {
|
|
233
|
+
if (
|
|
234
|
+
cause instanceof XrpcResponseError ||
|
|
235
|
+
cause instanceof XrpcUpstreamError ||
|
|
236
|
+
cause instanceof XrpcInternalError
|
|
237
|
+
) {
|
|
238
|
+
if (cause.method === method) return cause
|
|
208
239
|
}
|
|
240
|
+
|
|
241
|
+
return new XrpcInternalError(method, undefined, { cause })
|
|
209
242
|
}
|
|
@@ -17,7 +17,10 @@ const main =
|
|
|
17
17
|
repo: /*#__PURE__*/ l.string({ format: 'at-identifier' }),
|
|
18
18
|
collection: /*#__PURE__*/ l.string({ format: 'nsid' }),
|
|
19
19
|
limit: /*#__PURE__*/ l.optional(
|
|
20
|
-
/*#__PURE__*/ l.
|
|
20
|
+
/*#__PURE__*/ l.withDefault(
|
|
21
|
+
/*#__PURE__*/ l.integer({ minimum: 1, maximum: 100 }),
|
|
22
|
+
50,
|
|
23
|
+
),
|
|
21
24
|
),
|
|
22
25
|
cursor: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.string()),
|
|
23
26
|
reverse: /*#__PURE__*/ l.optional(/*#__PURE__*/ l.boolean()),
|
package/src/response.ts
CHANGED
|
@@ -7,27 +7,28 @@ import {
|
|
|
7
7
|
ResultSuccess,
|
|
8
8
|
} from '@atproto/lex-schema'
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
XrpcAuthenticationError,
|
|
11
|
+
XrpcResponseError,
|
|
12
|
+
XrpcUpstreamError,
|
|
13
|
+
isXrpcErrorPayload,
|
|
13
14
|
} from './errors.js'
|
|
14
|
-
import {
|
|
15
|
+
import { XrpcPayload } from './util.js'
|
|
15
16
|
|
|
16
|
-
export type
|
|
17
|
+
export type XrpcResponseBody<M extends Procedure | Query> =
|
|
17
18
|
InferMethodOutputBody<M, Uint8Array>
|
|
18
19
|
|
|
19
|
-
export type
|
|
20
|
+
export type XrpcResponsePayload<M extends Procedure | Query> =
|
|
20
21
|
InferMethodOutputEncoding<M> extends infer E extends string
|
|
21
|
-
?
|
|
22
|
+
? XrpcPayload<XrpcResponseBody<M>, E>
|
|
22
23
|
: null
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Small container for XRPC response data.
|
|
26
27
|
*
|
|
27
|
-
* @implements {ResultSuccess<
|
|
28
|
+
* @implements {ResultSuccess<XrpcResponse<M>>} for convenience in result handling contexts.
|
|
28
29
|
*/
|
|
29
|
-
export class
|
|
30
|
-
implements ResultSuccess<
|
|
30
|
+
export class XrpcResponse<M extends Procedure | Query>
|
|
31
|
+
implements ResultSuccess<XrpcResponse<M>>
|
|
31
32
|
{
|
|
32
33
|
/** @see {@link ResultSuccess.success} */
|
|
33
34
|
readonly success = true as const
|
|
@@ -41,7 +42,7 @@ export class LexRpcResponse<const M extends Procedure | Query>
|
|
|
41
42
|
readonly method: M,
|
|
42
43
|
readonly status: number,
|
|
43
44
|
readonly headers: Headers,
|
|
44
|
-
readonly payload:
|
|
45
|
+
readonly payload: XrpcResponsePayload<M>,
|
|
45
46
|
) {}
|
|
46
47
|
|
|
47
48
|
/**
|
|
@@ -57,21 +58,22 @@ export class LexRpcResponse<const M extends Procedure | Query>
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
get body() {
|
|
60
|
-
return this.payload?.body as
|
|
61
|
+
return this.payload?.body as XrpcResponseBody<M>
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/**
|
|
64
|
-
* @throws {
|
|
65
|
-
* {@link
|
|
66
|
-
* the method's declared error schema.
|
|
67
|
-
* @
|
|
65
|
+
* @throws {XrpcResponseError} in case of (valid) XRPC error responses. Use
|
|
66
|
+
* {@link XrpcResponseError.matchesSchema} to narrow the error type based on
|
|
67
|
+
* the method's declared error schema. This can be narrowed further as a
|
|
68
|
+
* {@link XrpcAuthenticationError} if the error is an authentication error.
|
|
69
|
+
* @throws {XrpcUpstreamError} when the response is not a valid XRPC
|
|
68
70
|
* response, or if the response does not conform to the method's schema.
|
|
69
71
|
*/
|
|
70
72
|
static async fromFetchResponse<const M extends Procedure | Query>(
|
|
71
73
|
method: M,
|
|
72
74
|
response: Response,
|
|
73
75
|
options?: { validateResponse?: boolean },
|
|
74
|
-
): Promise<
|
|
76
|
+
): Promise<XrpcResponse<M>> {
|
|
75
77
|
// @NOTE The body MUST either be read or canceled to avoid resource leaks.
|
|
76
78
|
// Since nothing should cause an exception before "readPayload" is
|
|
77
79
|
// called, we can safely not use a try/finally here.
|
|
@@ -79,88 +81,96 @@ export class LexRpcResponse<const M extends Procedure | Query>
|
|
|
79
81
|
// @NOTE redirect is set to 'follow', so we shouldn't get 3xx responses here
|
|
80
82
|
if (response.status < 200 || response.status >= 300) {
|
|
81
83
|
// Always parse json for error responses
|
|
82
|
-
const payload = await readPayload(response, { parse: true })
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
84
|
+
const payload = await readPayload(response, { parse: true }).catch(
|
|
85
|
+
(cause) => {
|
|
86
|
+
throw new XrpcUpstreamError(
|
|
87
|
+
method,
|
|
88
|
+
response,
|
|
89
|
+
null,
|
|
90
|
+
'Unable to parse response payload',
|
|
91
|
+
{ cause },
|
|
92
|
+
)
|
|
93
|
+
},
|
|
94
|
+
)
|
|
92
95
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
response,
|
|
98
|
-
payload,
|
|
99
|
-
)
|
|
96
|
+
// Properly formatted XRPC error response ?
|
|
97
|
+
if (response.status >= 400 && isXrpcErrorPayload(payload)) {
|
|
98
|
+
throw response.status === 401
|
|
99
|
+
? new XrpcAuthenticationError<M>(method, response, payload)
|
|
100
|
+
: new XrpcResponseError<M>(method, response, payload)
|
|
100
101
|
}
|
|
101
102
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
? `Upstream server returned an invalid response payload`
|
|
106
|
-
: `Upstream server returned an invalid status code`,
|
|
103
|
+
// Invalid XRPC response (we probably did not hit an XRPC implementation)
|
|
104
|
+
throw new XrpcUpstreamError(
|
|
105
|
+
method,
|
|
107
106
|
response,
|
|
108
107
|
payload,
|
|
108
|
+
response.status >= 500
|
|
109
|
+
? 'Upstream server encountered an error'
|
|
110
|
+
: response.status >= 400
|
|
111
|
+
? 'Invalid response payload'
|
|
112
|
+
: 'Invalid response status code',
|
|
109
113
|
)
|
|
110
114
|
}
|
|
111
115
|
|
|
112
116
|
// Only parse json if the schema expects it
|
|
113
117
|
const payload = await readPayload(response, {
|
|
114
118
|
parse: shouldParse(method),
|
|
119
|
+
}).catch((cause) => {
|
|
120
|
+
throw new XrpcUpstreamError(
|
|
121
|
+
method,
|
|
122
|
+
response,
|
|
123
|
+
null,
|
|
124
|
+
'Unable to parse response payload',
|
|
125
|
+
{ cause },
|
|
126
|
+
)
|
|
115
127
|
})
|
|
116
128
|
|
|
117
129
|
// Response is successful (2xx). Validate payload (data and encoding) against schema.
|
|
118
130
|
if (method.output.encoding == null) {
|
|
119
131
|
// Schema expects no payload
|
|
120
132
|
if (payload) {
|
|
121
|
-
throw new
|
|
122
|
-
|
|
123
|
-
`Expected response with no body, got ${payload.encoding}`,
|
|
133
|
+
throw new XrpcUpstreamError(
|
|
134
|
+
method,
|
|
124
135
|
response,
|
|
125
136
|
payload,
|
|
137
|
+
`Expected response with no body, got ${payload.encoding}`,
|
|
126
138
|
)
|
|
127
139
|
}
|
|
128
140
|
} else {
|
|
129
141
|
// Schema expects a payload
|
|
130
142
|
if (!payload || !method.output.matchesEncoding(payload.encoding)) {
|
|
131
|
-
throw new
|
|
132
|
-
|
|
143
|
+
throw new XrpcUpstreamError(
|
|
144
|
+
method,
|
|
145
|
+
response,
|
|
146
|
+
payload,
|
|
133
147
|
payload
|
|
134
148
|
? `Expected ${method.output.encoding} response, got ${payload.encoding}`
|
|
135
149
|
: `Expected non-empty response with content-type ${method.output.encoding}`,
|
|
136
|
-
response,
|
|
137
|
-
payload,
|
|
138
150
|
)
|
|
139
151
|
}
|
|
140
152
|
|
|
141
153
|
// Assert valid response body.
|
|
142
154
|
if (method.output.schema && options?.validateResponse !== false) {
|
|
143
|
-
const result = method.output.schema.safeParse(payload.body
|
|
144
|
-
allowTransform: false,
|
|
145
|
-
})
|
|
155
|
+
const result = method.output.schema.safeParse(payload.body)
|
|
146
156
|
|
|
147
157
|
if (!result.success) {
|
|
148
|
-
throw new
|
|
149
|
-
|
|
150
|
-
`Response validation failed: ${result.reason.message}`,
|
|
158
|
+
throw new XrpcUpstreamError(
|
|
159
|
+
method,
|
|
151
160
|
response,
|
|
152
161
|
payload,
|
|
162
|
+
`Response validation failed: ${result.reason.message}`,
|
|
153
163
|
{ cause: result.reason },
|
|
154
164
|
)
|
|
155
165
|
}
|
|
156
166
|
}
|
|
157
167
|
}
|
|
158
168
|
|
|
159
|
-
return new
|
|
169
|
+
return new XrpcResponse<M>(
|
|
160
170
|
method,
|
|
161
171
|
response.status,
|
|
162
172
|
response.headers,
|
|
163
|
-
payload as
|
|
173
|
+
payload as XrpcResponsePayload<M>,
|
|
164
174
|
)
|
|
165
175
|
}
|
|
166
176
|
}
|
|
@@ -175,7 +185,7 @@ function shouldParse(method: Procedure | Query) {
|
|
|
175
185
|
async function readPayload(
|
|
176
186
|
response: Response,
|
|
177
187
|
options?: { parse?: boolean },
|
|
178
|
-
): Promise<
|
|
188
|
+
): Promise<XrpcPayload | null> {
|
|
179
189
|
// @TODO Should we limit the maximum response size here (this could also be
|
|
180
190
|
// done by the FetchHandler)?
|
|
181
191
|
|
|
@@ -206,22 +216,12 @@ async function readPayload(
|
|
|
206
216
|
// to @ipld/dag-json)
|
|
207
217
|
const text = await response.text()
|
|
208
218
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
return { encoding, body: lexParse(text) }
|
|
216
|
-
} catch (cause) {
|
|
217
|
-
throw new LexRpcUpstreamError(
|
|
218
|
-
'InvalidResponse',
|
|
219
|
-
'Invalid JSON response body',
|
|
220
|
-
response,
|
|
221
|
-
null,
|
|
222
|
-
{ cause },
|
|
223
|
-
)
|
|
224
|
-
}
|
|
219
|
+
// @NOTE Using `lexParse(text)` (instead of `jsonToLex(json)`) here as
|
|
220
|
+
// using a reviver function during JSON.parse should be faster than
|
|
221
|
+
// parsing to JSON then converting to Lex (?)
|
|
222
|
+
|
|
223
|
+
// @TODO verify statement above
|
|
224
|
+
return { encoding, body: lexParse(text) }
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
return { encoding, body: new Uint8Array(await response.arrayBuffer()) }
|