@atproto/xrpc 0.5.0 → 0.6.0-rc.0
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 +35 -0
- package/README.md +56 -31
- package/dist/client.d.ts +12 -8
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +21 -80
- package/dist/client.js.map +1 -1
- package/dist/fetch-handler.d.ts +33 -0
- package/dist/fetch-handler.d.ts.map +1 -0
- package/dist/fetch-handler.js +21 -0
- package/dist/fetch-handler.js.map +1 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +16 -12
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +61 -12
- package/dist/types.js.map +1 -1
- package/dist/util.d.ts +8 -5
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +290 -77
- package/dist/util.js.map +1 -1
- package/dist/xrpc-client.d.ts +10 -0
- package/dist/xrpc-client.d.ts.map +1 -0
- package/dist/xrpc-client.js +79 -0
- package/dist/xrpc-client.js.map +1 -0
- package/package.json +3 -3
- package/src/client.ts +34 -121
- package/src/fetch-handler.ts +68 -0
- package/src/index.ts +5 -1
- package/src/types.ts +77 -24
- package/src/util.ts +353 -84
- package/src/xrpc-client.ts +108 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { LexiconDoc, Lexicons } from '@atproto/lexicon';
|
|
2
|
+
import { FetchHandler, FetchHandlerOptions } from './fetch-handler';
|
|
3
|
+
import { CallOptions, QueryParams, XRPCResponse } from './types';
|
|
4
|
+
export declare class XrpcClient {
|
|
5
|
+
readonly fetchHandler: FetchHandler;
|
|
6
|
+
readonly lex: Lexicons;
|
|
7
|
+
constructor(fetchHandler: FetchHandler | FetchHandlerOptions, lex: Lexicons | Iterable<LexiconDoc>);
|
|
8
|
+
call(methodNsid: string, params?: QueryParams, data?: unknown, opts?: CallOptions): Promise<XRPCResponse>;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=xrpc-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xrpc-client.d.ts","sourceRoot":"","sources":["../src/xrpc-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAmB,MAAM,kBAAkB,CAAA;AACxE,OAAO,EACL,YAAY,EACZ,mBAAmB,EAEpB,MAAM,iBAAiB,CAAA;AACxB,OAAO,EACL,WAAW,EACX,WAAW,EAIX,YAAY,EAEb,MAAM,SAAS,CAAA;AAUhB,qBAAa,UAAU;IACrB,QAAQ,CAAC,YAAY,EAAE,YAAY,CAAA;IACnC,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAA;gBAGpB,YAAY,EAAE,YAAY,GAAG,mBAAmB,EAChD,GAAG,EAAE,QAAQ,GAAG,QAAQ,CAAC,UAAU,CAAC;IAUhC,IAAI,CACR,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,WAAW,EACpB,IAAI,CAAC,EAAE,OAAO,EACd,IAAI,CAAC,EAAE,WAAW,GACjB,OAAO,CAAC,YAAY,CAAC;CA8DzB"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.XrpcClient = void 0;
|
|
4
|
+
const lexicon_1 = require("@atproto/lexicon");
|
|
5
|
+
const fetch_handler_1 = require("./fetch-handler");
|
|
6
|
+
const types_1 = require("./types");
|
|
7
|
+
const util_1 = require("./util");
|
|
8
|
+
class XrpcClient {
|
|
9
|
+
constructor(fetchHandler, lex) {
|
|
10
|
+
Object.defineProperty(this, "fetchHandler", {
|
|
11
|
+
enumerable: true,
|
|
12
|
+
configurable: true,
|
|
13
|
+
writable: true,
|
|
14
|
+
value: void 0
|
|
15
|
+
});
|
|
16
|
+
Object.defineProperty(this, "lex", {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true,
|
|
20
|
+
value: void 0
|
|
21
|
+
});
|
|
22
|
+
this.fetchHandler =
|
|
23
|
+
typeof fetchHandler === 'function'
|
|
24
|
+
? fetchHandler
|
|
25
|
+
: (0, fetch_handler_1.buildFetchHandler)(fetchHandler);
|
|
26
|
+
this.lex = lex instanceof lexicon_1.Lexicons ? lex : new lexicon_1.Lexicons(lex);
|
|
27
|
+
}
|
|
28
|
+
async call(methodNsid, params, data, opts) {
|
|
29
|
+
const def = this.lex.getDefOrThrow(methodNsid);
|
|
30
|
+
if (!def || (def.type !== 'query' && def.type !== 'procedure')) {
|
|
31
|
+
throw new TypeError(`Invalid lexicon: ${methodNsid}. Must be a query or procedure.`);
|
|
32
|
+
}
|
|
33
|
+
//@TODO: should we validate the params and data here?
|
|
34
|
+
// this.lex.assertValidXrpcParams(methodNsid, params)
|
|
35
|
+
// if (data !== undefined) {
|
|
36
|
+
// this.lex.assertValidXrpcInput(methodNsid, data)
|
|
37
|
+
// }
|
|
38
|
+
const reqUrl = (0, util_1.constructMethodCallUrl)(methodNsid, def, params);
|
|
39
|
+
const reqMethod = (0, util_1.getMethodSchemaHTTPMethod)(def);
|
|
40
|
+
const reqHeaders = (0, util_1.constructMethodCallHeaders)(def, data, opts);
|
|
41
|
+
const reqBody = (0, util_1.encodeMethodCallBody)(reqHeaders, data);
|
|
42
|
+
// The duplex field is required for streaming bodies, but not yet reflected
|
|
43
|
+
// anywhere in docs or types. See whatwg/fetch#1438, nodejs/node#46221.
|
|
44
|
+
const init = {
|
|
45
|
+
method: reqMethod,
|
|
46
|
+
headers: reqHeaders,
|
|
47
|
+
body: reqBody,
|
|
48
|
+
duplex: 'half',
|
|
49
|
+
signal: opts?.signal,
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
const response = await this.fetchHandler.call(undefined, reqUrl, init);
|
|
53
|
+
const resStatus = response.status;
|
|
54
|
+
const resHeaders = Object.fromEntries(response.headers.entries());
|
|
55
|
+
const resBodyBytes = await response.arrayBuffer();
|
|
56
|
+
const resBody = (0, util_1.httpResponseBodyParse)(response.headers.get('content-type'), resBodyBytes);
|
|
57
|
+
const resCode = (0, types_1.httpResponseCodeToEnum)(resStatus);
|
|
58
|
+
if (resCode !== types_1.ResponseType.Success) {
|
|
59
|
+
const { error = undefined, message = undefined } = resBody && (0, util_1.isErrorResponseBody)(resBody) ? resBody : {};
|
|
60
|
+
throw new types_1.XRPCError(resCode, error, message, resHeaders);
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
this.lex.assertValidXrpcOutput(methodNsid, resBody);
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
if (e instanceof lexicon_1.ValidationError) {
|
|
67
|
+
throw new types_1.XRPCInvalidResponseError(methodNsid, e, resBody);
|
|
68
|
+
}
|
|
69
|
+
throw e;
|
|
70
|
+
}
|
|
71
|
+
return new types_1.XRPCResponse(resBody, resHeaders);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
throw types_1.XRPCError.from(err);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.XrpcClient = XrpcClient;
|
|
79
|
+
//# sourceMappingURL=xrpc-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"xrpc-client.js","sourceRoot":"","sources":["../src/xrpc-client.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto/xrpc",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0-rc.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "atproto HTTP API (XRPC) client library",
|
|
6
6
|
"keywords": [
|
|
@@ -16,8 +16,8 @@
|
|
|
16
16
|
"main": "dist/index.js",
|
|
17
17
|
"types": "dist/index.d.ts",
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"zod": "^3.
|
|
20
|
-
"@atproto/lexicon": "^0.4.0"
|
|
19
|
+
"zod": "^3.23.8",
|
|
20
|
+
"@atproto/lexicon": "^0.4.1-rc.0"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"typescript": "^5.3.3"
|
package/src/client.ts
CHANGED
|
@@ -1,29 +1,24 @@
|
|
|
1
|
-
import { LexiconDoc, Lexicons
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
constructMethodCallHeaders,
|
|
6
|
-
encodeMethodCallBody,
|
|
7
|
-
httpResponseCodeToEnum,
|
|
8
|
-
httpResponseBodyParse,
|
|
9
|
-
normalizeHeaders,
|
|
10
|
-
} from './util'
|
|
11
|
-
import {
|
|
12
|
-
FetchHandler,
|
|
13
|
-
FetchHandlerResponse,
|
|
14
|
-
Headers,
|
|
15
|
-
CallOptions,
|
|
16
|
-
QueryParams,
|
|
17
|
-
ResponseType,
|
|
18
|
-
errorResponseBody,
|
|
19
|
-
ErrorResponseBody,
|
|
20
|
-
XRPCResponse,
|
|
21
|
-
XRPCError,
|
|
22
|
-
XRPCInvalidResponseError,
|
|
23
|
-
} from './types'
|
|
1
|
+
import { LexiconDoc, Lexicons } from '@atproto/lexicon'
|
|
2
|
+
import { CallOptions, Gettable, QueryParams } from './types'
|
|
3
|
+
import { XrpcClient } from './xrpc-client'
|
|
4
|
+
import { combineHeaders } from './util'
|
|
24
5
|
|
|
6
|
+
/** @deprecated Use {@link XrpcClient} instead */
|
|
25
7
|
export class Client {
|
|
26
|
-
|
|
8
|
+
/** @deprecated */
|
|
9
|
+
get fetch(): never {
|
|
10
|
+
throw new Error(
|
|
11
|
+
'Client.fetch is no longer supported. Use an XrpcClient instead.',
|
|
12
|
+
)
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** @deprecated */
|
|
16
|
+
set fetch(_: never) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
'Client.fetch is no longer supported. Use an XrpcClient instead.',
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
27
22
|
lex = new Lexicons()
|
|
28
23
|
|
|
29
24
|
// method calls
|
|
@@ -33,7 +28,7 @@ export class Client {
|
|
|
33
28
|
serviceUri: string | URL,
|
|
34
29
|
methodNsid: string,
|
|
35
30
|
params?: QueryParams,
|
|
36
|
-
data?:
|
|
31
|
+
data?: BodyInit | null,
|
|
37
32
|
opts?: CallOptions,
|
|
38
33
|
) {
|
|
39
34
|
return this.service(serviceUri).call(methodNsid, params, data, opts)
|
|
@@ -61,109 +56,27 @@ export class Client {
|
|
|
61
56
|
}
|
|
62
57
|
}
|
|
63
58
|
|
|
64
|
-
|
|
65
|
-
|
|
59
|
+
/** @deprecated Use {@link XrpcClient} instead */
|
|
60
|
+
export class ServiceClient extends XrpcClient {
|
|
66
61
|
uri: URL
|
|
67
|
-
headers
|
|
62
|
+
protected headers = new Map<string, Gettable<null | string>>()
|
|
68
63
|
|
|
69
|
-
constructor(
|
|
70
|
-
|
|
64
|
+
constructor(
|
|
65
|
+
public baseClient: Client,
|
|
66
|
+
serviceUri: string | URL,
|
|
67
|
+
) {
|
|
68
|
+
super(async (input, init) => {
|
|
69
|
+
const headers = combineHeaders(init.headers, this.headers)
|
|
70
|
+
return fetch(new URL(input, this.uri), { ...init, headers })
|
|
71
|
+
}, baseClient.lex)
|
|
71
72
|
this.uri = typeof serviceUri === 'string' ? new URL(serviceUri) : serviceUri
|
|
72
73
|
}
|
|
73
74
|
|
|
74
|
-
setHeader(key: string, value: string): void {
|
|
75
|
-
this.headers
|
|
75
|
+
setHeader(key: string, value: Gettable<null | string>): void {
|
|
76
|
+
this.headers.set(key.toLowerCase(), value)
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
unsetHeader(key: string): void {
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
async call(
|
|
83
|
-
methodNsid: string,
|
|
84
|
-
params?: QueryParams,
|
|
85
|
-
data?: unknown,
|
|
86
|
-
opts?: CallOptions,
|
|
87
|
-
) {
|
|
88
|
-
const def = this.baseClient.lex.getDefOrThrow(methodNsid)
|
|
89
|
-
if (!def || (def.type !== 'query' && def.type !== 'procedure')) {
|
|
90
|
-
throw new Error(
|
|
91
|
-
`Invalid lexicon: ${methodNsid}. Must be a query or procedure.`,
|
|
92
|
-
)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const httpMethod = getMethodSchemaHTTPMethod(def)
|
|
96
|
-
const httpUri = constructMethodCallUri(methodNsid, def, this.uri, params)
|
|
97
|
-
const httpHeaders = constructMethodCallHeaders(def, data, {
|
|
98
|
-
headers: {
|
|
99
|
-
...this.headers,
|
|
100
|
-
...opts?.headers,
|
|
101
|
-
},
|
|
102
|
-
encoding: opts?.encoding,
|
|
103
|
-
})
|
|
104
|
-
|
|
105
|
-
const res = await this.baseClient.fetch(
|
|
106
|
-
httpUri,
|
|
107
|
-
httpMethod,
|
|
108
|
-
httpHeaders,
|
|
109
|
-
data,
|
|
110
|
-
)
|
|
111
|
-
|
|
112
|
-
const resCode = httpResponseCodeToEnum(res.status)
|
|
113
|
-
if (resCode === ResponseType.Success) {
|
|
114
|
-
try {
|
|
115
|
-
this.baseClient.lex.assertValidXrpcOutput(methodNsid, res.body)
|
|
116
|
-
} catch (e: any) {
|
|
117
|
-
if (e instanceof ValidationError) {
|
|
118
|
-
throw new XRPCInvalidResponseError(methodNsid, e, res.body)
|
|
119
|
-
} else {
|
|
120
|
-
throw e
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
return new XRPCResponse(res.body, res.headers)
|
|
124
|
-
} else {
|
|
125
|
-
if (res.body && isErrorResponseBody(res.body)) {
|
|
126
|
-
throw new XRPCError(
|
|
127
|
-
resCode,
|
|
128
|
-
res.body.error,
|
|
129
|
-
res.body.message,
|
|
130
|
-
res.headers,
|
|
131
|
-
)
|
|
132
|
-
} else {
|
|
133
|
-
throw new XRPCError(resCode)
|
|
134
|
-
}
|
|
135
|
-
}
|
|
80
|
+
this.headers.delete(key.toLowerCase())
|
|
136
81
|
}
|
|
137
82
|
}
|
|
138
|
-
|
|
139
|
-
export async function defaultFetchHandler(
|
|
140
|
-
httpUri: string,
|
|
141
|
-
httpMethod: string,
|
|
142
|
-
httpHeaders: Headers,
|
|
143
|
-
httpReqBody: unknown,
|
|
144
|
-
): Promise<FetchHandlerResponse> {
|
|
145
|
-
try {
|
|
146
|
-
// The duplex field is now required for streaming bodies, but not yet reflected
|
|
147
|
-
// anywhere in docs or types. See whatwg/fetch#1438, nodejs/node#46221.
|
|
148
|
-
const headers = normalizeHeaders(httpHeaders)
|
|
149
|
-
const reqInit: RequestInit & { duplex: string } = {
|
|
150
|
-
method: httpMethod,
|
|
151
|
-
headers,
|
|
152
|
-
body: encodeMethodCallBody(headers, httpReqBody),
|
|
153
|
-
duplex: 'half',
|
|
154
|
-
}
|
|
155
|
-
const res = await fetch(httpUri, reqInit)
|
|
156
|
-
const resBody = await res.arrayBuffer()
|
|
157
|
-
return {
|
|
158
|
-
status: res.status,
|
|
159
|
-
headers: Object.fromEntries(res.headers.entries()),
|
|
160
|
-
body: httpResponseBodyParse(res.headers.get('content-type'), resBody),
|
|
161
|
-
}
|
|
162
|
-
} catch (e) {
|
|
163
|
-
throw new XRPCError(ResponseType.Unknown, String(e))
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function isErrorResponseBody(v: unknown): v is ErrorResponseBody {
|
|
168
|
-
return errorResponseBody.safeParse(v).success
|
|
169
|
-
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Gettable } from './types'
|
|
2
|
+
import { combineHeaders } from './util'
|
|
3
|
+
|
|
4
|
+
export type FetchHandler = (
|
|
5
|
+
this: void,
|
|
6
|
+
/**
|
|
7
|
+
* The URL (pathname + query parameters) to make the request to, without the
|
|
8
|
+
* origin. The origin (protocol, hostname, and port) must be added by this
|
|
9
|
+
* {@link FetchHandler}, typically based on authentication or other factors.
|
|
10
|
+
*/
|
|
11
|
+
url: string,
|
|
12
|
+
init: RequestInit,
|
|
13
|
+
) => Promise<Response>
|
|
14
|
+
|
|
15
|
+
export type FetchHandlerOptions = BuildFetchHandlerOptions | string | URL
|
|
16
|
+
|
|
17
|
+
export type BuildFetchHandlerOptions = {
|
|
18
|
+
/**
|
|
19
|
+
* The service URL to make requests to. This can be a string, URL, or a
|
|
20
|
+
* function that returns a string or URL. This is useful for dynamic URLs,
|
|
21
|
+
* such as a service URL that changes based on authentication.
|
|
22
|
+
*/
|
|
23
|
+
service: Gettable<string | URL>
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Headers to be added to every request. If a function is provided, it will be
|
|
27
|
+
* called on each request to get the headers. This is useful for dynamic
|
|
28
|
+
* headers, such as authentication tokens that may expire.
|
|
29
|
+
*/
|
|
30
|
+
headers?: {
|
|
31
|
+
[_ in string]?: Gettable<null | string>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Bring your own fetch implementation. Typically useful for testing, logging,
|
|
36
|
+
* mocking, or adding retries, session management, signatures, proof of
|
|
37
|
+
* possession (DPoP), etc. Defaults to the global `fetch` function.
|
|
38
|
+
*/
|
|
39
|
+
fetch?: typeof globalThis.fetch
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function buildFetchHandler(options: FetchHandlerOptions): FetchHandler {
|
|
43
|
+
const {
|
|
44
|
+
service,
|
|
45
|
+
headers: defaultHeaders = undefined,
|
|
46
|
+
fetch = globalThis.fetch,
|
|
47
|
+
} = typeof options === 'string' || options instanceof URL
|
|
48
|
+
? { service: options }
|
|
49
|
+
: options
|
|
50
|
+
|
|
51
|
+
if (typeof fetch !== 'function') {
|
|
52
|
+
throw new TypeError(
|
|
53
|
+
'XrpcDispatcher requires fetch() to be available in your environment.',
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const defaultHeadersEntries =
|
|
58
|
+
defaultHeaders != null ? Object.entries(defaultHeaders) : undefined
|
|
59
|
+
|
|
60
|
+
return async function (url, init) {
|
|
61
|
+
const base = typeof service === 'function' ? service() : service
|
|
62
|
+
const fullUrl = new URL(url, base)
|
|
63
|
+
|
|
64
|
+
const headers = combineHeaders(init.headers, defaultHeadersEntries)
|
|
65
|
+
|
|
66
|
+
return fetch(fullUrl, { ...init, headers })
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
-
export * from './types'
|
|
2
1
|
export * from './client'
|
|
2
|
+
export * from './fetch-handler'
|
|
3
|
+
export * from './types'
|
|
4
|
+
export * from './util'
|
|
5
|
+
export * from './xrpc-client'
|
|
3
6
|
|
|
4
7
|
import { Client } from './client'
|
|
8
|
+
/** @deprecated create a local {@link XrpcClient} instance instead */
|
|
5
9
|
const defaultInst = new Client()
|
|
6
10
|
export default defaultInst
|
package/src/types.ts
CHANGED
|
@@ -2,26 +2,19 @@ import { z } from 'zod'
|
|
|
2
2
|
import { ValidationError } from '@atproto/lexicon'
|
|
3
3
|
|
|
4
4
|
export type QueryParams = Record<string, any>
|
|
5
|
-
export type
|
|
5
|
+
export type HeadersMap = Record<string, string>
|
|
6
|
+
|
|
7
|
+
/** @deprecated not to be confused with the WHATWG Headers constructor */
|
|
8
|
+
export type Headers = HeadersMap
|
|
9
|
+
|
|
10
|
+
export type Gettable<T> = T | (() => T)
|
|
6
11
|
|
|
7
12
|
export interface CallOptions {
|
|
8
13
|
encoding?: string
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export interface FetchHandlerResponse {
|
|
13
|
-
status: number
|
|
14
|
-
headers: Headers
|
|
15
|
-
body: ArrayBuffer | undefined
|
|
14
|
+
signal?: AbortSignal
|
|
15
|
+
headers?: HeadersMap
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export type FetchHandler = (
|
|
19
|
-
httpUri: string,
|
|
20
|
-
httpMethod: string,
|
|
21
|
-
httpHeaders: Headers,
|
|
22
|
-
httpReqBody: any,
|
|
23
|
-
) => Promise<FetchHandlerResponse>
|
|
24
|
-
|
|
25
18
|
export const errorResponseBody = z.object({
|
|
26
19
|
error: z.string().optional(),
|
|
27
20
|
message: z.string().optional(),
|
|
@@ -45,7 +38,24 @@ export enum ResponseType {
|
|
|
45
38
|
UpstreamTimeout = 504,
|
|
46
39
|
}
|
|
47
40
|
|
|
41
|
+
export function httpResponseCodeToEnum(status: number): ResponseType {
|
|
42
|
+
if (status in ResponseType) {
|
|
43
|
+
return status
|
|
44
|
+
} else if (status >= 100 && status < 200) {
|
|
45
|
+
return ResponseType.XRPCNotSupported
|
|
46
|
+
} else if (status >= 200 && status < 300) {
|
|
47
|
+
return ResponseType.Success
|
|
48
|
+
} else if (status >= 300 && status < 400) {
|
|
49
|
+
return ResponseType.XRPCNotSupported
|
|
50
|
+
} else if (status >= 400 && status < 500) {
|
|
51
|
+
return ResponseType.InvalidRequest
|
|
52
|
+
} else {
|
|
53
|
+
return ResponseType.InternalServerError
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
48
57
|
export const ResponseTypeNames = {
|
|
58
|
+
[ResponseType.Unknown]: 'Unknown',
|
|
49
59
|
[ResponseType.InvalidResponse]: 'InvalidResponse',
|
|
50
60
|
[ResponseType.Success]: 'Success',
|
|
51
61
|
[ResponseType.InvalidRequest]: 'InvalidRequest',
|
|
@@ -61,7 +71,12 @@ export const ResponseTypeNames = {
|
|
|
61
71
|
[ResponseType.UpstreamTimeout]: 'UpstreamTimeout',
|
|
62
72
|
}
|
|
63
73
|
|
|
74
|
+
export function httpResponseCodeToName(status: number): string {
|
|
75
|
+
return ResponseTypeNames[httpResponseCodeToEnum(status)]
|
|
76
|
+
}
|
|
77
|
+
|
|
64
78
|
export const ResponseTypeStrings = {
|
|
79
|
+
[ResponseType.Unknown]: 'Unknown',
|
|
65
80
|
[ResponseType.InvalidResponse]: 'Invalid Response',
|
|
66
81
|
[ResponseType.Success]: 'Success',
|
|
67
82
|
[ResponseType.InvalidRequest]: 'Invalid Request',
|
|
@@ -77,27 +92,63 @@ export const ResponseTypeStrings = {
|
|
|
77
92
|
[ResponseType.UpstreamTimeout]: 'Upstream Timeout',
|
|
78
93
|
}
|
|
79
94
|
|
|
95
|
+
export function httpResponseCodeToString(status: number): string {
|
|
96
|
+
return ResponseTypeStrings[httpResponseCodeToEnum(status)]
|
|
97
|
+
}
|
|
98
|
+
|
|
80
99
|
export class XRPCResponse {
|
|
81
100
|
success = true
|
|
82
101
|
|
|
83
|
-
constructor(
|
|
102
|
+
constructor(
|
|
103
|
+
public data: any,
|
|
104
|
+
public headers: Headers,
|
|
105
|
+
) {}
|
|
84
106
|
}
|
|
85
107
|
|
|
86
108
|
export class XRPCError extends Error {
|
|
87
109
|
success = false
|
|
88
|
-
|
|
110
|
+
|
|
111
|
+
public status: ResponseType
|
|
89
112
|
|
|
90
113
|
constructor(
|
|
91
|
-
|
|
92
|
-
public error
|
|
114
|
+
statusCode: number,
|
|
115
|
+
public error: string = httpResponseCodeToName(statusCode),
|
|
93
116
|
message?: string,
|
|
94
|
-
headers?: Headers,
|
|
117
|
+
public headers?: Headers,
|
|
118
|
+
options?: ErrorOptions,
|
|
95
119
|
) {
|
|
96
|
-
super(message || error ||
|
|
97
|
-
|
|
98
|
-
|
|
120
|
+
super(message || error || httpResponseCodeToString(statusCode), options)
|
|
121
|
+
|
|
122
|
+
this.status = httpResponseCodeToEnum(statusCode)
|
|
123
|
+
|
|
124
|
+
// Pre 2022 runtimes won't handle the "options" constructor argument
|
|
125
|
+
const cause = options?.cause
|
|
126
|
+
if (this.cause === undefined && cause !== undefined) {
|
|
127
|
+
this.cause = cause
|
|
99
128
|
}
|
|
100
|
-
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
static from(cause: unknown, fallbackStatus?: ResponseType): XRPCError {
|
|
132
|
+
if (cause instanceof XRPCError) {
|
|
133
|
+
return cause
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Extract status code from "http-errors" like errors
|
|
137
|
+
const statusCode: unknown =
|
|
138
|
+
cause instanceof Error
|
|
139
|
+
? ('statusCode' in cause ? cause.statusCode : undefined) ??
|
|
140
|
+
('status' in cause ? cause.status : undefined)
|
|
141
|
+
: undefined
|
|
142
|
+
|
|
143
|
+
const status: ResponseType =
|
|
144
|
+
typeof statusCode === 'number'
|
|
145
|
+
? httpResponseCodeToEnum(statusCode)
|
|
146
|
+
: fallbackStatus ?? ResponseType.Unknown
|
|
147
|
+
|
|
148
|
+
const error = ResponseTypeNames[status]
|
|
149
|
+
const message = cause instanceof Error ? cause.message : String(cause)
|
|
150
|
+
|
|
151
|
+
return new XRPCError(status, error, message, undefined, { cause })
|
|
101
152
|
}
|
|
102
153
|
}
|
|
103
154
|
|
|
@@ -111,6 +162,8 @@ export class XRPCInvalidResponseError extends XRPCError {
|
|
|
111
162
|
ResponseType.InvalidResponse,
|
|
112
163
|
ResponseTypeStrings[ResponseType.InvalidResponse],
|
|
113
164
|
`The server gave an invalid response and may be out of date.`,
|
|
165
|
+
undefined,
|
|
166
|
+
{ cause: validationError },
|
|
114
167
|
)
|
|
115
168
|
}
|
|
116
169
|
}
|