@atproto-labs/fetch 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -4
- package/dist/fetch-error.d.ts +1 -12
- package/dist/fetch-error.d.ts.map +1 -1
- package/dist/fetch-error.js +24 -39
- package/dist/fetch-error.js.map +1 -1
- package/dist/fetch-request.d.ts +22 -5
- package/dist/fetch-request.d.ts.map +1 -1
- package/dist/fetch-request.js +55 -18
- package/dist/fetch-request.js.map +1 -1
- package/dist/fetch-response.d.ts +30 -12
- package/dist/fetch-response.d.ts.map +1 -1
- package/dist/fetch-response.js +134 -81
- package/dist/fetch-response.js.map +1 -1
- package/dist/fetch-wrap.d.ts +47 -9
- package/dist/fetch-wrap.d.ts.map +1 -1
- package/dist/fetch-wrap.js +112 -61
- package/dist/fetch-wrap.js.map +1 -1
- package/dist/fetch.d.ts +8 -1
- package/dist/fetch.d.ts.map +1 -1
- package/dist/fetch.js +13 -0
- package/dist/fetch.js.map +1 -1
- package/dist/transformed-response.d.ts.map +1 -1
- package/dist/transformed-response.js +5 -2
- package/dist/transformed-response.js.map +1 -1
- package/dist/util.d.ts +46 -14
- package/dist/util.d.ts.map +1 -1
- package/dist/util.js +121 -24
- package/dist/util.js.map +1 -1
- package/package.json +6 -5
- package/src/fetch-error.ts +26 -44
- package/src/fetch-request.ts +81 -24
- package/src/fetch-response.ts +177 -111
- package/src/fetch-wrap.ts +139 -90
- package/src/fetch.ts +38 -3
- package/src/transformed-response.ts +5 -2
- package/src/util.ts +142 -25
- package/tsconfig.build.json +8 -0
- package/tsconfig.json +2 -6
|
@@ -15,10 +15,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
15
15
|
exports.TransformedResponse = void 0;
|
|
16
16
|
class TransformedResponse extends Response {
|
|
17
17
|
constructor(response, transform) {
|
|
18
|
-
if (response.body
|
|
18
|
+
if (!response.body) {
|
|
19
|
+
throw new TypeError('Response body is not available');
|
|
20
|
+
}
|
|
21
|
+
if (response.bodyUsed) {
|
|
19
22
|
throw new TypeError('Response body is already used');
|
|
20
23
|
}
|
|
21
|
-
super(response.body
|
|
24
|
+
super(response.body.pipeThrough(transform), {
|
|
22
25
|
status: response.status,
|
|
23
26
|
statusText: response.statusText,
|
|
24
27
|
headers: response.headers,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transformed-response.js","sourceRoot":"","sources":["../src/transformed-response.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,MAAa,mBAAoB,SAAQ,QAAQ;IAG/C,YAAY,QAAkB,EAAE,SAA0B;QACxD,IAAI,QAAQ,CAAC,IAAI,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"transformed-response.js","sourceRoot":"","sources":["../src/transformed-response.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;AAAA,MAAa,mBAAoB,SAAQ,QAAQ;IAG/C,YAAY,QAAkB,EAAE,SAA0B;QACxD,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;YACnB,MAAM,IAAI,SAAS,CAAC,gCAAgC,CAAC,CAAA;QACvD,CAAC;QACD,IAAI,QAAQ,CAAC,QAAQ,EAAE,CAAC;YACtB,MAAM,IAAI,SAAS,CAAC,+BAA+B,CAAC,CAAA;QACtD,CAAC;QAED,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,WAAW,CAAC,SAAS,CAAC,EAAE;YAC1C,MAAM,EAAE,QAAQ,CAAC,MAAM;YACvB,UAAU,EAAE,QAAQ,CAAC,UAAU;YAC/B,OAAO,EAAE,QAAQ,CAAC,OAAO;SAC1B,CAAC,CAAA;QAdJ,gDAAmB;QAgBjB,uBAAA,IAAI,iCAAa,QAAQ,MAAA,CAAA;IAC3B,CAAC;IAED;;OAEG;IACH,IAAI,GAAG;QACL,OAAO,uBAAA,IAAI,qCAAU,CAAC,GAAG,CAAA;IAC3B,CAAC;IACD,IAAI,UAAU;QACZ,OAAO,uBAAA,IAAI,qCAAU,CAAC,UAAU,CAAA;IAClC,CAAC;IACD,IAAI,IAAI;QACN,OAAO,uBAAA,IAAI,qCAAU,CAAC,IAAI,CAAA;IAC5B,CAAC;IACD,IAAI,UAAU;QACZ,OAAO,uBAAA,IAAI,qCAAU,CAAC,UAAU,CAAA;IAClC,CAAC;CACF;AAnCD,kDAmCC"}
|
package/dist/util.d.ts
CHANGED
|
@@ -6,19 +6,51 @@ export type JsonObject = {
|
|
|
6
6
|
[key: string]: Json;
|
|
7
7
|
};
|
|
8
8
|
export type JsonArray = Json[];
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}
|
|
13
|
-
}
|
|
9
|
+
export type ThisParameterOverride<C, Fn extends (...a: any) => any> = Fn extends (...args: infer P) => infer R ? ((this: C, ...args: P) => R) & {
|
|
10
|
+
bind(context: C): (...args: P) => R;
|
|
11
|
+
} : never;
|
|
14
12
|
export declare function isIp(hostname: string): boolean;
|
|
15
|
-
export declare const ifObject: <V>(v
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export declare
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
export declare const ifObject: <V>(v: V) => (V extends symbol | Function | JsonScalar | JsonArray ? never : V extends Json ? V : Record<string, unknown>) | undefined;
|
|
14
|
+
export declare const ifString: <V>(v: V) => (V & string) | undefined;
|
|
15
|
+
export declare class MaxBytesTransformStream extends TransformStream<Uint8Array, Uint8Array> {
|
|
16
|
+
constructor(maxBytes: number);
|
|
17
|
+
}
|
|
18
|
+
export declare function padLines(input: string, pad: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* @param [onCancellationError] - Callback that will trigger to asynchronously
|
|
21
|
+
* handle any error that occurs while cancelling the response body. Providing
|
|
22
|
+
* this will speed up the process and avoid potential deadlocks. Defaults to
|
|
23
|
+
* awaiting the cancellation operation. use `"log"` to log the error.
|
|
24
|
+
* @see {@link https://undici.nodejs.org/#/?id=garbage-collection}
|
|
25
|
+
* @note awaiting this function's result, when no `onCancellationError` is
|
|
26
|
+
* provided, might result in a dead lock. Indeed, if the response was cloned(),
|
|
27
|
+
* the response.body.cancel() method will not resolve until the other response's
|
|
28
|
+
* body is consumed/cancelled.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* // Make sure response was not cloned, or that every cloned response was
|
|
33
|
+
* // consumed/cancelled before awaiting this function's result.
|
|
34
|
+
* await cancelBody(response)
|
|
35
|
+
* ```
|
|
36
|
+
* @example
|
|
37
|
+
* ```ts
|
|
38
|
+
* await cancelBody(response, (err) => {
|
|
39
|
+
* // No biggie, let's just log the error
|
|
40
|
+
* console.warn('Failed to cancel response body', err)
|
|
41
|
+
* })
|
|
42
|
+
* ```
|
|
43
|
+
* @example
|
|
44
|
+
* ```ts
|
|
45
|
+
* // Will generate an "unhandledRejection" if an error occurs while cancelling
|
|
46
|
+
* // the response body. This will likely crash the process.
|
|
47
|
+
* await cancelBody(response, (err) => { throw err })
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function cancelBody(body: Body, onCancellationError?: 'log' | ((err: unknown) => void)): Promise<void>;
|
|
51
|
+
export declare function logCancellationError(err: unknown): void;
|
|
52
|
+
export declare function stringifyMessage(input: Body & {
|
|
53
|
+
headers: Headers;
|
|
54
|
+
}): Promise<string>;
|
|
55
|
+
export declare const extractUrl: (input: Request | string | URL) => URL;
|
|
24
56
|
//# sourceMappingURL=util.d.ts.map
|
package/dist/util.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;AACzD,MAAM,MAAM,IAAI,GAAG,UAAU,GAAG,IAAI,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;CAAE,CAAA;AAC5E,MAAM,MAAM,UAAU,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAA;AAChD,MAAM,MAAM,SAAS,GAAG,IAAI,EAAE,CAAA;AAE9B,
|
|
1
|
+
{"version":3,"file":"util.d.ts","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAA;AACzD,MAAM,MAAM,IAAI,GAAG,UAAU,GAAG,IAAI,EAAE,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAA;CAAE,CAAA;AAC5E,MAAM,MAAM,UAAU,GAAG;IAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAA;AAChD,MAAM,MAAM,SAAS,GAAG,IAAI,EAAE,CAAA;AAE9B,MAAM,MAAM,qBAAqB,CAC/B,CAAC,EACD,EAAE,SAAS,CAAC,GAAG,CAAC,EAAE,GAAG,KAAK,GAAG,IAC3B,EAAE,SAAS,CAAC,GAAG,IAAI,EAAE,MAAM,CAAC,KAAK,MAAM,CAAC,GACxC,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,GAAG,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG;IAC7B,IAAI,CAAC,OAAO,EAAE,CAAC,GAAG,CAAC,GAAG,IAAI,EAAE,CAAC,KAAK,CAAC,CAAA;CACpC,GACD,KAAK,CAAA;AAET,wBAAgB,IAAI,CAAC,QAAQ,EAAE,MAAM,WAQpC;AAGD,eAAO,MAAM,QAAQ,SAAU,CAAC,8HAe/B,CAAA;AAED,eAAO,MAAM,QAAQ,SAAU,CAAC,6BAA4C,CAAA;AAE5E,qBAAa,uBAAwB,SAAQ,eAAe,CAC1D,UAAU,EACV,UAAU,CACX;gBACa,QAAQ,EAAE,MAAM;CAqB7B;AAGD,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,UAGlD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,IAAI,EACV,mBAAmB,CAAC,EAAE,KAAK,GAAG,CAAC,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAC,GACrD,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,GAAG,IAAI,CAEvD;AAED,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,IAAI,GAAG;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,mBAQxE;AA2BD,eAAO,MAAM,UAAU,UAAW,OAAO,GAAG,MAAM,GAAG,GAAG,QAK9B,CAAA"}
|
package/dist/util.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
// TODO: Move to a shared package ?
|
|
2
|
+
// @TODO: Move some of these to a shared package ?
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
-
exports.
|
|
4
|
+
exports.extractUrl = exports.stringifyMessage = exports.logCancellationError = exports.cancelBody = exports.padLines = exports.MaxBytesTransformStream = exports.ifString = exports.ifObject = exports.isIp = void 0;
|
|
5
5
|
function isIp(hostname) {
|
|
6
6
|
// IPv4
|
|
7
7
|
if (hostname.match(/^\d+\.\d+\.\d+\.\d+$/))
|
|
@@ -12,7 +12,6 @@ function isIp(hostname) {
|
|
|
12
12
|
return false;
|
|
13
13
|
}
|
|
14
14
|
exports.isIp = isIp;
|
|
15
|
-
// TODO: Move to a shared package ?
|
|
16
15
|
const plainObjectProto = Object.prototype;
|
|
17
16
|
const ifObject = (v) => {
|
|
18
17
|
if (typeof v === 'object' && v != null && !Array.isArray(v)) {
|
|
@@ -25,27 +24,125 @@ const ifObject = (v) => {
|
|
|
25
24
|
return undefined;
|
|
26
25
|
};
|
|
27
26
|
exports.ifObject = ifObject;
|
|
28
|
-
const ifArray = (v) => (Array.isArray(v) ? v : undefined);
|
|
29
|
-
exports.ifArray = ifArray;
|
|
30
|
-
const ifScalar = (v) => {
|
|
31
|
-
switch (typeof v) {
|
|
32
|
-
case 'string':
|
|
33
|
-
case 'number':
|
|
34
|
-
case 'boolean':
|
|
35
|
-
return v;
|
|
36
|
-
default:
|
|
37
|
-
if (v === null)
|
|
38
|
-
return null;
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
exports.ifScalar = ifScalar;
|
|
43
|
-
const ifBoolean = (v) => (typeof v === 'boolean' ? v : undefined);
|
|
44
|
-
exports.ifBoolean = ifBoolean;
|
|
45
27
|
const ifString = (v) => (typeof v === 'string' ? v : undefined);
|
|
46
28
|
exports.ifString = ifString;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
29
|
+
class MaxBytesTransformStream extends TransformStream {
|
|
30
|
+
constructor(maxBytes) {
|
|
31
|
+
// Note: negation accounts for invalid value types (NaN, non numbers)
|
|
32
|
+
if (!(maxBytes >= 0)) {
|
|
33
|
+
throw new TypeError('maxBytes must be a non-negative number');
|
|
34
|
+
}
|
|
35
|
+
let bytesRead = 0;
|
|
36
|
+
super({
|
|
37
|
+
transform: (chunk, ctrl) => {
|
|
38
|
+
if ((bytesRead += chunk.length) <= maxBytes) {
|
|
39
|
+
ctrl.enqueue(chunk);
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
ctrl.error(new Error('Response too large'));
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.MaxBytesTransformStream = MaxBytesTransformStream;
|
|
49
|
+
const LINE_BREAK = /\r?\n/g;
|
|
50
|
+
function padLines(input, pad) {
|
|
51
|
+
if (!input)
|
|
52
|
+
return input;
|
|
53
|
+
return pad + input.replace(LINE_BREAK, `$&${pad}`);
|
|
54
|
+
}
|
|
55
|
+
exports.padLines = padLines;
|
|
56
|
+
/**
|
|
57
|
+
* @param [onCancellationError] - Callback that will trigger to asynchronously
|
|
58
|
+
* handle any error that occurs while cancelling the response body. Providing
|
|
59
|
+
* this will speed up the process and avoid potential deadlocks. Defaults to
|
|
60
|
+
* awaiting the cancellation operation. use `"log"` to log the error.
|
|
61
|
+
* @see {@link https://undici.nodejs.org/#/?id=garbage-collection}
|
|
62
|
+
* @note awaiting this function's result, when no `onCancellationError` is
|
|
63
|
+
* provided, might result in a dead lock. Indeed, if the response was cloned(),
|
|
64
|
+
* the response.body.cancel() method will not resolve until the other response's
|
|
65
|
+
* body is consumed/cancelled.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* // Make sure response was not cloned, or that every cloned response was
|
|
70
|
+
* // consumed/cancelled before awaiting this function's result.
|
|
71
|
+
* await cancelBody(response)
|
|
72
|
+
* ```
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* await cancelBody(response, (err) => {
|
|
76
|
+
* // No biggie, let's just log the error
|
|
77
|
+
* console.warn('Failed to cancel response body', err)
|
|
78
|
+
* })
|
|
79
|
+
* ```
|
|
80
|
+
* @example
|
|
81
|
+
* ```ts
|
|
82
|
+
* // Will generate an "unhandledRejection" if an error occurs while cancelling
|
|
83
|
+
* // the response body. This will likely crash the process.
|
|
84
|
+
* await cancelBody(response, (err) => { throw err })
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
async function cancelBody(body, onCancellationError) {
|
|
88
|
+
if (body.body &&
|
|
89
|
+
!body.bodyUsed &&
|
|
90
|
+
!body.body.locked &&
|
|
91
|
+
// Support for alternative fetch implementations
|
|
92
|
+
typeof body.body.cancel === 'function') {
|
|
93
|
+
if (typeof onCancellationError === 'function') {
|
|
94
|
+
void body.body.cancel().catch(onCancellationError);
|
|
95
|
+
}
|
|
96
|
+
else if (onCancellationError === 'log') {
|
|
97
|
+
void body.body.cancel().catch(logCancellationError);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
await body.body.cancel();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
exports.cancelBody = cancelBody;
|
|
105
|
+
function logCancellationError(err) {
|
|
106
|
+
console.warn('Failed to cancel response body', err);
|
|
107
|
+
}
|
|
108
|
+
exports.logCancellationError = logCancellationError;
|
|
109
|
+
async function stringifyMessage(input) {
|
|
110
|
+
try {
|
|
111
|
+
const headers = stringifyHeaders(input.headers);
|
|
112
|
+
const payload = await stringifyBody(input);
|
|
113
|
+
return headers && payload ? `${headers}\n${payload}` : headers || payload;
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
void cancelBody(input, 'log');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
exports.stringifyMessage = stringifyMessage;
|
|
120
|
+
function stringifyHeaders(headers) {
|
|
121
|
+
return Array.from(headers)
|
|
122
|
+
.map(([name, value]) => `${name}: ${value}`)
|
|
123
|
+
.join('\n');
|
|
124
|
+
}
|
|
125
|
+
async function stringifyBody(body) {
|
|
126
|
+
try {
|
|
127
|
+
const blob = await body.blob();
|
|
128
|
+
if (blob.type?.startsWith('text/')) {
|
|
129
|
+
const text = await blob.text();
|
|
130
|
+
return JSON.stringify(text);
|
|
131
|
+
}
|
|
132
|
+
if (/application\/(?:\w+\+)?json/.test(blob.type)) {
|
|
133
|
+
const text = await blob.text();
|
|
134
|
+
return text.includes('\n') ? JSON.stringify(JSON.parse(text)) : text;
|
|
135
|
+
}
|
|
136
|
+
return `[Body size: ${blob.size}, type: ${JSON.stringify(blob.type)} ]`;
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
return '[Body could not be read]';
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const extractUrl = (input) => typeof input === 'string'
|
|
143
|
+
? new URL(input)
|
|
144
|
+
: input instanceof URL
|
|
145
|
+
? input
|
|
146
|
+
: new URL(input.url);
|
|
147
|
+
exports.extractUrl = extractUrl;
|
|
51
148
|
//# sourceMappingURL=util.js.map
|
package/dist/util.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":";AAAA,
|
|
1
|
+
{"version":3,"file":"util.js","sourceRoot":"","sources":["../src/util.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atproto-labs/fetch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Isomorphic wrapper utilities for fetch API",
|
|
6
6
|
"keywords": [
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"repository": {
|
|
12
12
|
"type": "git",
|
|
13
13
|
"url": "https://github.com/bluesky-social/atproto",
|
|
14
|
-
"directory": "packages/fetch"
|
|
14
|
+
"directory": "packages/internal/fetch"
|
|
15
15
|
},
|
|
16
16
|
"type": "commonjs",
|
|
17
17
|
"main": "dist/index.js",
|
|
@@ -23,13 +23,14 @@
|
|
|
23
23
|
}
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"
|
|
27
|
-
"zod": "^3.22.4",
|
|
28
|
-
"@atproto-labs/transformer": "0.0.1"
|
|
26
|
+
"@atproto-labs/pipe": "0.1.0"
|
|
29
27
|
},
|
|
30
28
|
"devDependencies": {
|
|
31
29
|
"typescript": "^5.3.3"
|
|
32
30
|
},
|
|
31
|
+
"optionalDependencies": {
|
|
32
|
+
"zod": "^3.23.8"
|
|
33
|
+
},
|
|
33
34
|
"scripts": {
|
|
34
35
|
"build": "tsc --build tsconfig.json"
|
|
35
36
|
}
|
package/src/fetch-error.ts
CHANGED
|
@@ -1,44 +1,26 @@
|
|
|
1
|
-
import { Transformer } from '@atproto-labs/transformer'
|
|
2
|
-
|
|
3
|
-
export type FetchErrorOptions = {
|
|
4
|
-
cause?: unknown
|
|
5
|
-
request?: Request
|
|
6
|
-
response?: Response
|
|
7
|
-
}
|
|
8
|
-
|
|
9
1
|
export class FetchError extends Error {
|
|
10
|
-
public readonly
|
|
11
|
-
public readonly response?: Response
|
|
2
|
+
public readonly statusCode: number
|
|
12
3
|
|
|
13
|
-
constructor(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
this.request = request
|
|
20
|
-
this.response = response
|
|
21
|
-
}
|
|
4
|
+
constructor(statusCode?: number, message?: string, options?: ErrorOptions) {
|
|
5
|
+
if (statusCode == null || !message) {
|
|
6
|
+
const info = extractInfo(extractRootCause(options?.cause))
|
|
7
|
+
statusCode = statusCode ?? info[0]
|
|
8
|
+
message = message || info[1]
|
|
9
|
+
}
|
|
22
10
|
|
|
23
|
-
|
|
24
|
-
const cause = extractCause(err)
|
|
25
|
-
return new FetchError(...extractInfo(cause), { cause })
|
|
26
|
-
}
|
|
27
|
-
}
|
|
11
|
+
super(message, options)
|
|
28
12
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
) => {
|
|
32
|
-
throw await FetchError.from(err)
|
|
13
|
+
this.statusCode = statusCode
|
|
14
|
+
}
|
|
33
15
|
}
|
|
34
16
|
|
|
35
|
-
function
|
|
17
|
+
function extractRootCause(err: unknown): unknown {
|
|
36
18
|
// Unwrap the Network error from undici (i.e. Node's internal fetch() implementation)
|
|
37
19
|
// https://github.com/nodejs/undici/blob/3274c975947ce11a08508743df026f73598bfead/lib/web/fetch/index.js#L223-L228
|
|
38
20
|
if (
|
|
39
21
|
err instanceof TypeError &&
|
|
40
22
|
err.message === 'fetch failed' &&
|
|
41
|
-
err.cause
|
|
23
|
+
err.cause !== undefined
|
|
42
24
|
) {
|
|
43
25
|
return err.cause
|
|
44
26
|
}
|
|
@@ -46,32 +28,32 @@ function extractCause(err: unknown): unknown {
|
|
|
46
28
|
return err
|
|
47
29
|
}
|
|
48
30
|
|
|
49
|
-
|
|
50
|
-
err: unknown,
|
|
51
|
-
): [statusCode: number, message: string] {
|
|
31
|
+
function extractInfo(err: unknown): [statusCode: number, message: string] {
|
|
52
32
|
if (typeof err === 'string' && err.length > 0) {
|
|
53
|
-
return [
|
|
33
|
+
return [500, err]
|
|
54
34
|
}
|
|
55
35
|
|
|
56
36
|
if (!(err instanceof Error)) {
|
|
57
|
-
return [
|
|
37
|
+
return [500, 'Failed to fetch']
|
|
58
38
|
}
|
|
59
39
|
|
|
60
|
-
|
|
40
|
+
const code = err['code']
|
|
41
|
+
if (typeof code === 'string') {
|
|
61
42
|
switch (true) {
|
|
62
|
-
case
|
|
63
|
-
return [
|
|
64
|
-
case
|
|
43
|
+
case code === 'ENOTFOUND':
|
|
44
|
+
return [400, 'Invalid hostname']
|
|
45
|
+
case code === 'ECONNREFUSED':
|
|
65
46
|
return [502, 'Connection refused']
|
|
66
|
-
case
|
|
47
|
+
case code === 'DEPTH_ZERO_SELF_SIGNED_CERT':
|
|
67
48
|
return [502, 'Self-signed certificate']
|
|
68
|
-
case
|
|
49
|
+
case code.startsWith('ERR_TLS'):
|
|
69
50
|
return [502, 'TLS error']
|
|
70
|
-
case
|
|
51
|
+
case code.startsWith('ECONN'):
|
|
71
52
|
return [502, 'Connection error']
|
|
53
|
+
default:
|
|
54
|
+
return [500, `${code} error`]
|
|
72
55
|
}
|
|
73
56
|
}
|
|
74
57
|
|
|
75
|
-
|
|
76
|
-
return [502, err.message]
|
|
58
|
+
return [500, err.message]
|
|
77
59
|
}
|
package/src/fetch-request.ts
CHANGED
|
@@ -1,40 +1,95 @@
|
|
|
1
|
-
import { Transformer } from '@atproto-labs/transformer'
|
|
2
|
-
|
|
3
1
|
import { FetchError } from './fetch-error.js'
|
|
4
|
-
import {
|
|
2
|
+
import { asRequest } from './fetch.js'
|
|
3
|
+
import { extractUrl, isIp } from './util.js'
|
|
4
|
+
|
|
5
|
+
export class FetchRequestError extends FetchError {
|
|
6
|
+
constructor(
|
|
7
|
+
public readonly request: Request,
|
|
8
|
+
statusCode?: number,
|
|
9
|
+
message?: string,
|
|
10
|
+
options?: ErrorOptions,
|
|
11
|
+
) {
|
|
12
|
+
super(statusCode, message, options)
|
|
13
|
+
}
|
|
5
14
|
|
|
6
|
-
|
|
15
|
+
static from(request: Request, cause: unknown): FetchRequestError {
|
|
16
|
+
if (cause instanceof FetchRequestError) return cause
|
|
17
|
+
return new FetchRequestError(request, undefined, undefined, { cause })
|
|
18
|
+
}
|
|
19
|
+
}
|
|
7
20
|
|
|
8
|
-
export function protocolCheckRequestTransform(
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
21
|
+
export function protocolCheckRequestTransform(protocols: {
|
|
22
|
+
'about:'?: boolean
|
|
23
|
+
'blob:'?: boolean
|
|
24
|
+
'data:'?: boolean
|
|
25
|
+
'file:'?: boolean
|
|
26
|
+
'http:'?: boolean | { allowCustomPort: boolean }
|
|
27
|
+
'https:'?: boolean | { allowCustomPort: boolean }
|
|
28
|
+
}) {
|
|
29
|
+
return (input: Request | string | URL, init?: RequestInit) => {
|
|
30
|
+
const { protocol, port } = extractUrl(input)
|
|
31
|
+
|
|
32
|
+
const request = asRequest(input, init)
|
|
33
|
+
|
|
34
|
+
const config: undefined | boolean | { allowCustomPort?: boolean } =
|
|
35
|
+
Object.hasOwn(protocols, protocol) ? protocols[protocol] : undefined
|
|
36
|
+
|
|
37
|
+
if (!config) {
|
|
38
|
+
throw new FetchRequestError(
|
|
39
|
+
request,
|
|
40
|
+
400,
|
|
41
|
+
`Forbidden protocol "${protocol}"`,
|
|
42
|
+
)
|
|
43
|
+
} else if (config === true) {
|
|
44
|
+
// Safe to proceed
|
|
45
|
+
} else if (!config['allowCustomPort'] && port !== '') {
|
|
46
|
+
throw new FetchRequestError(
|
|
47
|
+
request,
|
|
48
|
+
400,
|
|
49
|
+
`Custom ${protocol} ports not allowed`,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
12
52
|
|
|
13
|
-
|
|
14
|
-
|
|
53
|
+
return request
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function redirectCheckRequestTransform() {
|
|
58
|
+
return (input: Request | string | URL, init?: RequestInit) => {
|
|
59
|
+
const request = asRequest(input, init)
|
|
15
60
|
|
|
16
|
-
if (
|
|
17
|
-
throw new
|
|
61
|
+
if (request.redirect === 'follow') {
|
|
62
|
+
throw new FetchRequestError(
|
|
63
|
+
request,
|
|
64
|
+
500,
|
|
65
|
+
'Request redirect must be "error" or "manual"',
|
|
66
|
+
)
|
|
18
67
|
}
|
|
19
68
|
|
|
20
69
|
return request
|
|
21
70
|
}
|
|
22
71
|
}
|
|
23
72
|
|
|
24
|
-
export function
|
|
25
|
-
return
|
|
73
|
+
export function requireHostHeaderTransform() {
|
|
74
|
+
return (input: Request | string | URL, init?: RequestInit) => {
|
|
26
75
|
// Note that fetch() will automatically add the Host header from the URL and
|
|
27
76
|
// discard any Host header manually set in the request.
|
|
28
77
|
|
|
29
|
-
const { protocol, hostname } =
|
|
78
|
+
const { protocol, hostname } = extractUrl(input)
|
|
79
|
+
|
|
80
|
+
const request = asRequest(input, init)
|
|
30
81
|
|
|
31
82
|
// "Host" header only makes sense in the context of an HTTP request
|
|
32
|
-
if (protocol !== 'http:' && protocol !== 'https') {
|
|
33
|
-
throw new
|
|
83
|
+
if (protocol !== 'http:' && protocol !== 'https:') {
|
|
84
|
+
throw new FetchRequestError(
|
|
85
|
+
request,
|
|
86
|
+
400,
|
|
87
|
+
`"${protocol}" requests are not allowed`,
|
|
88
|
+
)
|
|
34
89
|
}
|
|
35
90
|
|
|
36
91
|
if (!hostname || isIp(hostname)) {
|
|
37
|
-
throw new
|
|
92
|
+
throw new FetchRequestError(request, 400, 'Invalid hostname')
|
|
38
93
|
}
|
|
39
94
|
|
|
40
95
|
return request
|
|
@@ -54,21 +109,23 @@ export const DEFAULT_FORBIDDEN_DOMAIN_NAMES = [
|
|
|
54
109
|
|
|
55
110
|
export function forbiddenDomainNameRequestTransform(
|
|
56
111
|
denyList: Iterable<string> = DEFAULT_FORBIDDEN_DOMAIN_NAMES,
|
|
57
|
-
)
|
|
112
|
+
) {
|
|
58
113
|
const denySet = new Set<string>(denyList)
|
|
59
114
|
|
|
60
115
|
// Optimization: if no forbidden domain names are provided, we can skip the
|
|
61
116
|
// check entirely.
|
|
62
117
|
if (denySet.size === 0) {
|
|
63
|
-
return
|
|
118
|
+
return asRequest
|
|
64
119
|
}
|
|
65
120
|
|
|
66
|
-
return async (
|
|
67
|
-
const { hostname } =
|
|
121
|
+
return async (input: Request | string | URL, init?: RequestInit) => {
|
|
122
|
+
const { hostname } = extractUrl(input)
|
|
123
|
+
|
|
124
|
+
const request = asRequest(input, init)
|
|
68
125
|
|
|
69
126
|
// Full domain name check
|
|
70
127
|
if (denySet.has(hostname)) {
|
|
71
|
-
throw new
|
|
128
|
+
throw new FetchRequestError(request, 403, 'Forbidden hostname')
|
|
72
129
|
}
|
|
73
130
|
|
|
74
131
|
// Sub domain name check
|
|
@@ -76,7 +133,7 @@ export function forbiddenDomainNameRequestTransform(
|
|
|
76
133
|
while (curDot !== -1) {
|
|
77
134
|
const subdomain = hostname.slice(curDot + 1)
|
|
78
135
|
if (denySet.has(`*.${subdomain}`)) {
|
|
79
|
-
throw new
|
|
136
|
+
throw new FetchRequestError(request, 403, 'Forbidden hostname')
|
|
80
137
|
}
|
|
81
138
|
curDot = hostname.indexOf('.', curDot + 1)
|
|
82
139
|
}
|