@gjsify/fetch 0.0.4 → 0.1.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/README.md +27 -2
- package/globals.mjs +12 -0
- package/lib/body.d.ts +69 -0
- package/lib/body.js +375 -0
- package/lib/errors/abort-error.d.ts +7 -0
- package/lib/errors/abort-error.js +9 -0
- package/lib/errors/base.d.ts +6 -0
- package/lib/errors/base.js +17 -0
- package/lib/errors/fetch-error.d.ts +16 -0
- package/lib/errors/fetch-error.js +23 -0
- package/lib/esm/body.js +104 -56
- package/lib/esm/errors/base.js +3 -1
- package/lib/esm/headers.js +116 -131
- package/lib/esm/index.js +145 -190
- package/lib/esm/request.js +42 -41
- package/lib/esm/response.js +19 -4
- package/lib/esm/utils/blob-from.js +2 -98
- package/lib/esm/utils/data-uri.js +23 -0
- package/lib/esm/utils/is.js +7 -3
- package/lib/esm/utils/multipart-parser.js +5 -2
- package/lib/esm/utils/referrer.js +10 -10
- package/lib/esm/utils/soup-helpers.js +22 -0
- package/lib/headers.d.ts +33 -0
- package/lib/headers.js +195 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +205 -0
- package/lib/request.d.ts +101 -0
- package/lib/request.js +308 -0
- package/lib/response.d.ts +73 -0
- package/lib/response.js +158 -0
- package/lib/types/index.d.ts +1 -0
- package/lib/types/index.js +1 -0
- package/lib/types/system-error.d.ts +11 -0
- package/lib/types/system-error.js +2 -0
- package/lib/utils/blob-from.d.ts +2 -0
- package/lib/utils/blob-from.js +4 -0
- package/lib/utils/data-uri.d.ts +10 -0
- package/lib/utils/data-uri.js +27 -0
- package/lib/utils/get-search.d.ts +1 -0
- package/lib/utils/get-search.js +8 -0
- package/lib/utils/is-redirect.d.ts +7 -0
- package/lib/utils/is-redirect.js +10 -0
- package/lib/utils/is.d.ts +35 -0
- package/lib/utils/is.js +74 -0
- package/lib/utils/multipart-parser.d.ts +2 -0
- package/lib/utils/multipart-parser.js +396 -0
- package/lib/utils/referrer.d.ts +76 -0
- package/lib/utils/referrer.js +283 -0
- package/lib/utils/soup-helpers.d.ts +12 -0
- package/lib/utils/soup-helpers.js +25 -0
- package/package.json +23 -27
- package/src/body.ts +181 -169
- package/src/errors/base.ts +3 -1
- package/src/headers.ts +155 -202
- package/src/index.spec.ts +268 -3
- package/src/index.ts +199 -312
- package/src/request.ts +84 -75
- package/src/response.ts +48 -18
- package/src/test.mts +1 -1
- package/src/utils/blob-from.ts +4 -164
- package/src/utils/data-uri.ts +29 -0
- package/src/utils/is.ts +15 -15
- package/src/utils/multipart-parser.ts +3 -3
- package/src/utils/referrer.ts +11 -11
- package/src/utils/soup-helpers.ts +37 -0
- package/tsconfig.json +4 -4
- package/tsconfig.tsbuildinfo +1 -0
- package/lib/cjs/body.js +0 -255
- package/lib/cjs/errors/abort-error.js +0 -9
- package/lib/cjs/errors/base.js +0 -17
- package/lib/cjs/errors/fetch-error.js +0 -21
- package/lib/cjs/headers.js +0 -202
- package/lib/cjs/index.js +0 -224
- package/lib/cjs/request.js +0 -281
- package/lib/cjs/response.js +0 -133
- package/lib/cjs/types/index.js +0 -1
- package/lib/cjs/types/system-error.js +0 -1
- package/lib/cjs/utils/blob-from.js +0 -101
- package/lib/cjs/utils/get-search.js +0 -11
- package/lib/cjs/utils/is-redirect.js +0 -7
- package/lib/cjs/utils/is.js +0 -28
- package/lib/cjs/utils/multipart-parser.js +0 -353
- package/lib/cjs/utils/referrer.js +0 -153
- package/test.gjs.js +0 -34758
- package/test.gjs.mjs +0 -53172
- package/test.node.js +0 -1226
- package/test.node.mjs +0 -6273
- package/tsconfig.types.json +0 -8
package/README.md
CHANGED
|
@@ -1,9 +1,34 @@
|
|
|
1
1
|
# @gjsify/fetch
|
|
2
2
|
|
|
3
|
-
Web and
|
|
3
|
+
GJS implementation of the Web Fetch API using Soup 3.0 and Gio. Provides fetch(), Request, Response, and Headers.
|
|
4
|
+
|
|
5
|
+
Part of the [gjsify](https://github.com/gjsify/gjsify) project — Node.js and Web APIs for GJS (GNOME JavaScript).
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @gjsify/fetch
|
|
11
|
+
# or
|
|
12
|
+
yarn add @gjsify/fetch
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { fetch, Request, Response, Headers } from '@gjsify/fetch';
|
|
19
|
+
|
|
20
|
+
const response = await fetch('https://example.com');
|
|
21
|
+
const text = await response.text();
|
|
22
|
+
console.log(text);
|
|
23
|
+
```
|
|
4
24
|
|
|
5
25
|
## Inspirations and credits
|
|
26
|
+
|
|
6
27
|
- https://github.com/sonnyp/troll/blob/main/src/std/fetch.js
|
|
7
28
|
- https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/node-fetch
|
|
8
29
|
- https://github.com/node-fetch/node-fetch
|
|
9
|
-
- https://github.com/denoland/deno/tree/main/ext/fetch
|
|
30
|
+
- https://github.com/denoland/deno/tree/main/ext/fetch
|
|
31
|
+
|
|
32
|
+
## License
|
|
33
|
+
|
|
34
|
+
MIT
|
package/globals.mjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-exports native Web API globals for use in Node.js builds.
|
|
3
|
+
*
|
|
4
|
+
* When tests import from the bare 'fetch' specifier, the build system
|
|
5
|
+
* aliases it to this module on Node.js (where fetch/Headers/Request/
|
|
6
|
+
* Response/FormData are native globals).
|
|
7
|
+
*/
|
|
8
|
+
export const Headers = globalThis.Headers;
|
|
9
|
+
export const Request = globalThis.Request;
|
|
10
|
+
export const Response = globalThis.Response;
|
|
11
|
+
export const FormData = globalThis.FormData;
|
|
12
|
+
export default globalThis.fetch;
|
package/lib/body.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Blob } from './utils/blob-from.js';
|
|
2
|
+
import { Readable, Writable } from 'node:stream';
|
|
3
|
+
import { Buffer } from 'node:buffer';
|
|
4
|
+
import { FormData } from '@gjsify/formdata';
|
|
5
|
+
import { FetchBaseError } from './errors/base.js';
|
|
6
|
+
import type { Request } from './request.js';
|
|
7
|
+
import type { Response } from './response.js';
|
|
8
|
+
declare const INTERNALS: unique symbol;
|
|
9
|
+
/**
|
|
10
|
+
* Body mixin
|
|
11
|
+
*
|
|
12
|
+
* Ref: https://fetch.spec.whatwg.org/#body
|
|
13
|
+
*
|
|
14
|
+
* @param body Readable stream
|
|
15
|
+
* @param opts Response options
|
|
16
|
+
*/
|
|
17
|
+
export default class Body {
|
|
18
|
+
[INTERNALS]: {
|
|
19
|
+
body: null | Buffer | Readable | Blob;
|
|
20
|
+
stream: Readable | null;
|
|
21
|
+
boundary: string;
|
|
22
|
+
disturbed: boolean;
|
|
23
|
+
error: null | FetchBaseError;
|
|
24
|
+
};
|
|
25
|
+
size: number;
|
|
26
|
+
constructor(body: BodyInit | Readable | Blob | Buffer | null, options?: {
|
|
27
|
+
size?: number;
|
|
28
|
+
headers?: unknown;
|
|
29
|
+
});
|
|
30
|
+
get body(): ReadableStream<Uint8Array> | null;
|
|
31
|
+
get _stream(): Readable;
|
|
32
|
+
get bodyUsed(): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Decode response as ArrayBuffer
|
|
35
|
+
*/
|
|
36
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
37
|
+
formData(): Promise<FormData>;
|
|
38
|
+
/**
|
|
39
|
+
* Return raw response as Blob
|
|
40
|
+
*/
|
|
41
|
+
blob(): Promise<Blob>;
|
|
42
|
+
/**
|
|
43
|
+
* Decode response as json
|
|
44
|
+
*/
|
|
45
|
+
json(): Promise<unknown>;
|
|
46
|
+
/**
|
|
47
|
+
* Decode response as text
|
|
48
|
+
*/
|
|
49
|
+
text(): Promise<string>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Clone body given Res/Req instance
|
|
53
|
+
*/
|
|
54
|
+
export declare const clone: <T extends Request | Response>(instance: T, highWaterMark?: number) => Blob | Readable | Buffer<ArrayBufferLike>;
|
|
55
|
+
/**
|
|
56
|
+
* Extract a Content-Type value from a body.
|
|
57
|
+
*/
|
|
58
|
+
export declare const extractContentType: (body: BodyInit | Readable | Blob | Buffer | null, request: Request | Response) => string | null;
|
|
59
|
+
/**
|
|
60
|
+
* Get total bytes of a body.
|
|
61
|
+
*/
|
|
62
|
+
export declare const getTotalBytes: (request: Request) => number | null;
|
|
63
|
+
/**
|
|
64
|
+
* Write a Body to a Node.js WritableStream.
|
|
65
|
+
*/
|
|
66
|
+
export declare const writeToStream: (dest: Writable, { body }: {
|
|
67
|
+
body: Readable | null;
|
|
68
|
+
}) => Promise<void>;
|
|
69
|
+
export {};
|
package/lib/body.js
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Adapted from node-fetch (https://github.com/node-fetch/node-fetch/blob/main/src/body.js)
|
|
3
|
+
// Copyright (c) node-fetch contributors. MIT license.
|
|
4
|
+
// Modifications: Rewritten for GJS using libsoup 3.0 and @gjsify/url
|
|
5
|
+
import { URLSearchParams } from '@gjsify/url';
|
|
6
|
+
import { Blob } from './utils/blob-from.js';
|
|
7
|
+
import { PassThrough, pipeline as pipelineCb, Readable, Stream } from 'node:stream';
|
|
8
|
+
import { Buffer } from 'node:buffer';
|
|
9
|
+
import { FormData, formDataToBlob } from '@gjsify/formdata';
|
|
10
|
+
import { FetchError } from './errors/fetch-error.js';
|
|
11
|
+
import { FetchBaseError } from './errors/base.js';
|
|
12
|
+
import { isBlob, isURLSearchParameters } from './utils/is.js';
|
|
13
|
+
const pipeline = (source, dest) => new Promise((resolve, reject) => {
|
|
14
|
+
pipelineCb(source, dest, (err) => {
|
|
15
|
+
if (err)
|
|
16
|
+
reject(err);
|
|
17
|
+
else
|
|
18
|
+
resolve();
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
const INTERNALS = Symbol('Body internals');
|
|
22
|
+
function isAnyArrayBuffer(val) {
|
|
23
|
+
return val instanceof ArrayBuffer ||
|
|
24
|
+
(typeof SharedArrayBuffer !== 'undefined' && val instanceof SharedArrayBuffer);
|
|
25
|
+
}
|
|
26
|
+
function isBoxedPrimitive(val) {
|
|
27
|
+
return (val instanceof String ||
|
|
28
|
+
val instanceof Number ||
|
|
29
|
+
val instanceof Boolean ||
|
|
30
|
+
(typeof Symbol !== 'undefined' && val instanceof Symbol) ||
|
|
31
|
+
(typeof BigInt !== 'undefined' && val instanceof BigInt));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Body mixin
|
|
35
|
+
*
|
|
36
|
+
* Ref: https://fetch.spec.whatwg.org/#body
|
|
37
|
+
*
|
|
38
|
+
* @param body Readable stream
|
|
39
|
+
* @param opts Response options
|
|
40
|
+
*/
|
|
41
|
+
export default class Body {
|
|
42
|
+
[INTERNALS] = {
|
|
43
|
+
body: null,
|
|
44
|
+
stream: null,
|
|
45
|
+
boundary: '',
|
|
46
|
+
disturbed: false,
|
|
47
|
+
error: null,
|
|
48
|
+
};
|
|
49
|
+
size = 0;
|
|
50
|
+
constructor(body, options = { size: 0 }) {
|
|
51
|
+
this.size = options.size || 0;
|
|
52
|
+
if (body === null || body === undefined) {
|
|
53
|
+
// Body is undefined or null
|
|
54
|
+
this[INTERNALS].body = null;
|
|
55
|
+
}
|
|
56
|
+
else if (isURLSearchParameters(body)) {
|
|
57
|
+
// Body is a URLSearchParams
|
|
58
|
+
this[INTERNALS].body = Buffer.from(body.toString());
|
|
59
|
+
}
|
|
60
|
+
else if (isBlob(body)) {
|
|
61
|
+
// Body is blob
|
|
62
|
+
this[INTERNALS].body = body;
|
|
63
|
+
}
|
|
64
|
+
else if (Buffer.isBuffer(body)) {
|
|
65
|
+
// Body is Buffer
|
|
66
|
+
this[INTERNALS].body = body;
|
|
67
|
+
}
|
|
68
|
+
else if (isAnyArrayBuffer(body)) {
|
|
69
|
+
// Body is ArrayBuffer
|
|
70
|
+
this[INTERNALS].body = Buffer.from(body);
|
|
71
|
+
}
|
|
72
|
+
else if (ArrayBuffer.isView(body)) {
|
|
73
|
+
// Body is ArrayBufferView
|
|
74
|
+
this[INTERNALS].body = Buffer.from(body.buffer, body.byteOffset, body.byteLength);
|
|
75
|
+
}
|
|
76
|
+
else if (body instanceof Readable) {
|
|
77
|
+
// Body is Node.js stream
|
|
78
|
+
this[INTERNALS].body = body;
|
|
79
|
+
}
|
|
80
|
+
else if (typeof ReadableStream !== 'undefined' && body instanceof ReadableStream) {
|
|
81
|
+
// Body is Web ReadableStream — convert to Node.js Readable
|
|
82
|
+
this[INTERNALS].body = readableStreamToReadable(body);
|
|
83
|
+
}
|
|
84
|
+
else if (body instanceof FormData) {
|
|
85
|
+
// Body is FormData
|
|
86
|
+
const blob = formDataToBlob(body);
|
|
87
|
+
this[INTERNALS].body = blob;
|
|
88
|
+
this[INTERNALS].boundary = blob.type?.split('boundary=')?.[1] ?? '';
|
|
89
|
+
}
|
|
90
|
+
else if (typeof body === 'string') {
|
|
91
|
+
// String body
|
|
92
|
+
this[INTERNALS].body = Buffer.from(body);
|
|
93
|
+
}
|
|
94
|
+
else if (body instanceof URLSearchParams) {
|
|
95
|
+
this[INTERNALS].body = Buffer.from(body.toString());
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
console.warn(`Unknown body type "${typeof body}", try to parse the body to string!`);
|
|
99
|
+
this[INTERNALS].body = Buffer.from(String(body));
|
|
100
|
+
}
|
|
101
|
+
// Set up the internal stream
|
|
102
|
+
const b = this[INTERNALS].body;
|
|
103
|
+
if (Buffer.isBuffer(b)) {
|
|
104
|
+
this[INTERNALS].stream = Readable.from(b);
|
|
105
|
+
}
|
|
106
|
+
else if (isBlob(b)) {
|
|
107
|
+
this[INTERNALS].stream = Readable.from(blobToAsyncIterable(b));
|
|
108
|
+
}
|
|
109
|
+
else if (b instanceof Readable) {
|
|
110
|
+
this[INTERNALS].stream = b;
|
|
111
|
+
}
|
|
112
|
+
if (b instanceof Stream) {
|
|
113
|
+
b.on('error', (error_) => {
|
|
114
|
+
const error = error_ instanceof FetchBaseError
|
|
115
|
+
? error_
|
|
116
|
+
: new FetchError(`Invalid response body while trying to fetch ${this.url}: ${error_.message}`, 'system', error_);
|
|
117
|
+
this[INTERNALS].error = error;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
get body() {
|
|
122
|
+
const stream = this[INTERNALS].stream;
|
|
123
|
+
if (!stream)
|
|
124
|
+
return null;
|
|
125
|
+
// If ReadableStream is available, wrap the Readable into one
|
|
126
|
+
if (typeof ReadableStream !== 'undefined') {
|
|
127
|
+
return new ReadableStream({
|
|
128
|
+
start(controller) {
|
|
129
|
+
stream.on('data', (chunk) => {
|
|
130
|
+
controller.enqueue(chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk));
|
|
131
|
+
});
|
|
132
|
+
stream.on('end', () => {
|
|
133
|
+
controller.close();
|
|
134
|
+
});
|
|
135
|
+
stream.on('error', (err) => {
|
|
136
|
+
controller.error(err);
|
|
137
|
+
});
|
|
138
|
+
},
|
|
139
|
+
cancel() {
|
|
140
|
+
stream.destroy();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
get _stream() {
|
|
147
|
+
return this[INTERNALS].stream;
|
|
148
|
+
}
|
|
149
|
+
get bodyUsed() {
|
|
150
|
+
return this[INTERNALS].disturbed;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Decode response as ArrayBuffer
|
|
154
|
+
*/
|
|
155
|
+
async arrayBuffer() {
|
|
156
|
+
const { buffer, byteOffset, byteLength } = await consumeBody(this);
|
|
157
|
+
return buffer.slice(byteOffset, byteOffset + byteLength);
|
|
158
|
+
}
|
|
159
|
+
async formData() {
|
|
160
|
+
const ct = this.headers?.get('content-type');
|
|
161
|
+
if (ct?.startsWith('application/x-www-form-urlencoded')) {
|
|
162
|
+
const formData = new FormData();
|
|
163
|
+
const parameters = new URLSearchParams(await this.text());
|
|
164
|
+
for (const [name, value] of parameters) {
|
|
165
|
+
formData.append(name, value);
|
|
166
|
+
}
|
|
167
|
+
return formData;
|
|
168
|
+
}
|
|
169
|
+
const { toFormData } = await import('./utils/multipart-parser.js');
|
|
170
|
+
return toFormData(this.body, ct);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Return raw response as Blob
|
|
174
|
+
*/
|
|
175
|
+
async blob() {
|
|
176
|
+
const ct = (this.headers?.get('content-type')) || (this[INTERNALS].body && this[INTERNALS].body.type) || '';
|
|
177
|
+
const buf = await this.arrayBuffer();
|
|
178
|
+
return new Blob([buf], {
|
|
179
|
+
type: ct
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Decode response as json
|
|
184
|
+
*/
|
|
185
|
+
async json() {
|
|
186
|
+
const text = await this.text();
|
|
187
|
+
return JSON.parse(text);
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Decode response as text
|
|
191
|
+
*/
|
|
192
|
+
async text() {
|
|
193
|
+
const buffer = await consumeBody(this);
|
|
194
|
+
return new TextDecoder().decode(buffer);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
// In browsers, all properties are enumerable.
|
|
198
|
+
Object.defineProperties(Body.prototype, {
|
|
199
|
+
body: { enumerable: true },
|
|
200
|
+
bodyUsed: { enumerable: true },
|
|
201
|
+
arrayBuffer: { enumerable: true },
|
|
202
|
+
blob: { enumerable: true },
|
|
203
|
+
json: { enumerable: true },
|
|
204
|
+
text: { enumerable: true },
|
|
205
|
+
});
|
|
206
|
+
/**
|
|
207
|
+
* Consume and convert an entire Body to a Buffer.
|
|
208
|
+
*/
|
|
209
|
+
async function consumeBody(data) {
|
|
210
|
+
if (data[INTERNALS].disturbed) {
|
|
211
|
+
throw new TypeError(`body used already for: ${data.url}`);
|
|
212
|
+
}
|
|
213
|
+
data[INTERNALS].disturbed = true;
|
|
214
|
+
if (data[INTERNALS].error) {
|
|
215
|
+
throw data[INTERNALS].error;
|
|
216
|
+
}
|
|
217
|
+
const { _stream: body } = data;
|
|
218
|
+
// Body is null
|
|
219
|
+
if (body === null) {
|
|
220
|
+
return Buffer.alloc(0);
|
|
221
|
+
}
|
|
222
|
+
if (!(body instanceof Stream)) {
|
|
223
|
+
return Buffer.alloc(0);
|
|
224
|
+
}
|
|
225
|
+
// Body is stream — consume it
|
|
226
|
+
const accum = [];
|
|
227
|
+
let accumBytes = 0;
|
|
228
|
+
try {
|
|
229
|
+
for await (const chunk of body) {
|
|
230
|
+
if (data.size > 0 && accumBytes + chunk.length > data.size) {
|
|
231
|
+
const error = new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size');
|
|
232
|
+
body.destroy(error);
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
accumBytes += chunk.length;
|
|
236
|
+
accum.push(chunk);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
241
|
+
const error_ = error instanceof FetchBaseError ? error : new FetchError(`Invalid response body while trying to fetch ${data.url}: ${err.message}`, 'system', err);
|
|
242
|
+
throw error_;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
if (accum.every(c => typeof c === 'string')) {
|
|
246
|
+
return Buffer.from(accum.join(''));
|
|
247
|
+
}
|
|
248
|
+
return Buffer.concat(accum, accumBytes);
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
252
|
+
throw new FetchError(`Could not create Buffer from response body for ${data.url}: ${err.message}`, 'system', err);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Clone body given Res/Req instance
|
|
257
|
+
*/
|
|
258
|
+
export const clone = (instance, highWaterMark) => {
|
|
259
|
+
let p1;
|
|
260
|
+
let p2;
|
|
261
|
+
let { body } = instance[INTERNALS];
|
|
262
|
+
if (instance.bodyUsed) {
|
|
263
|
+
throw new Error('cannot clone body after it is used');
|
|
264
|
+
}
|
|
265
|
+
if ((body instanceof Stream) && (typeof body.getBoundary !== 'function')) {
|
|
266
|
+
p1 = new PassThrough({ highWaterMark });
|
|
267
|
+
p2 = new PassThrough({ highWaterMark });
|
|
268
|
+
body.pipe(p1);
|
|
269
|
+
body.pipe(p2);
|
|
270
|
+
instance[INTERNALS].stream = p1;
|
|
271
|
+
body = p2;
|
|
272
|
+
}
|
|
273
|
+
return body;
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* Extract a Content-Type value from a body.
|
|
277
|
+
*/
|
|
278
|
+
export const extractContentType = (body, request) => {
|
|
279
|
+
if (body === null) {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
if (typeof body === 'string') {
|
|
283
|
+
return 'text/plain;charset=UTF-8';
|
|
284
|
+
}
|
|
285
|
+
if (isURLSearchParameters(body)) {
|
|
286
|
+
return 'application/x-www-form-urlencoded;charset=UTF-8';
|
|
287
|
+
}
|
|
288
|
+
if (isBlob(body)) {
|
|
289
|
+
return body.type || null;
|
|
290
|
+
}
|
|
291
|
+
if (Buffer.isBuffer(body) || isAnyArrayBuffer(body) || ArrayBuffer.isView(body)) {
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
if (body instanceof FormData) {
|
|
295
|
+
return `multipart/form-data; boundary=${request[INTERNALS].boundary}`;
|
|
296
|
+
}
|
|
297
|
+
if (body instanceof Stream) {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
return 'text/plain;charset=UTF-8';
|
|
301
|
+
};
|
|
302
|
+
/**
|
|
303
|
+
* Get total bytes of a body.
|
|
304
|
+
*/
|
|
305
|
+
export const getTotalBytes = (request) => {
|
|
306
|
+
const { body } = request[INTERNALS];
|
|
307
|
+
if (body === null) {
|
|
308
|
+
return 0;
|
|
309
|
+
}
|
|
310
|
+
if (isBlob(body)) {
|
|
311
|
+
return body.size;
|
|
312
|
+
}
|
|
313
|
+
if (Buffer.isBuffer(body)) {
|
|
314
|
+
return body.length;
|
|
315
|
+
}
|
|
316
|
+
if (body && typeof body.getLengthSync === 'function') {
|
|
317
|
+
const streamBody = body;
|
|
318
|
+
return streamBody.hasKnownLength && streamBody.hasKnownLength() ? streamBody.getLengthSync() : null;
|
|
319
|
+
}
|
|
320
|
+
return null;
|
|
321
|
+
};
|
|
322
|
+
/**
|
|
323
|
+
* Write a Body to a Node.js WritableStream.
|
|
324
|
+
*/
|
|
325
|
+
export const writeToStream = async (dest, { body }) => {
|
|
326
|
+
if (body === null) {
|
|
327
|
+
dest.end();
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
await pipeline(body, dest);
|
|
331
|
+
}
|
|
332
|
+
};
|
|
333
|
+
/**
|
|
334
|
+
* Convert a Web ReadableStream to a Node.js Readable.
|
|
335
|
+
*/
|
|
336
|
+
function readableStreamToReadable(webStream) {
|
|
337
|
+
const reader = webStream.getReader();
|
|
338
|
+
return new Readable({
|
|
339
|
+
async read() {
|
|
340
|
+
try {
|
|
341
|
+
const { done, value } = await reader.read();
|
|
342
|
+
if (done) {
|
|
343
|
+
this.push(null);
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
this.push(Buffer.from(value));
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch (err) {
|
|
350
|
+
this.destroy(err);
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
destroy(_err, callback) {
|
|
354
|
+
reader.cancel().then(() => callback(null), callback);
|
|
355
|
+
}
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Convert a Blob to an async iterable for Readable.from().
|
|
360
|
+
*/
|
|
361
|
+
async function* blobToAsyncIterable(blob) {
|
|
362
|
+
if (typeof blob.stream === 'function') {
|
|
363
|
+
const reader = blob.stream().getReader();
|
|
364
|
+
while (true) {
|
|
365
|
+
const { done, value } = await reader.read();
|
|
366
|
+
if (done)
|
|
367
|
+
break;
|
|
368
|
+
yield value;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
// Fallback: read the entire blob at once
|
|
373
|
+
yield new Uint8Array(await blob.arrayBuffer());
|
|
374
|
+
}
|
|
375
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export class FetchBaseError extends Error {
|
|
2
|
+
type;
|
|
3
|
+
constructor(message, type) {
|
|
4
|
+
super(message);
|
|
5
|
+
// Hide custom error implementation details from end-users
|
|
6
|
+
if (typeof Error.captureStackTrace === 'function') {
|
|
7
|
+
Error.captureStackTrace(this, this.constructor);
|
|
8
|
+
}
|
|
9
|
+
this.type = type;
|
|
10
|
+
}
|
|
11
|
+
get name() {
|
|
12
|
+
return this.constructor.name;
|
|
13
|
+
}
|
|
14
|
+
get [Symbol.toStringTag]() {
|
|
15
|
+
return this.constructor.name;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FetchBaseError } from './base.js';
|
|
2
|
+
import type { SystemError } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* FetchError interface for operational errors
|
|
5
|
+
*/
|
|
6
|
+
export declare class FetchError extends FetchBaseError {
|
|
7
|
+
code: string;
|
|
8
|
+
errno: string;
|
|
9
|
+
erroredSysCall: string;
|
|
10
|
+
/**
|
|
11
|
+
* @param message Error message for human
|
|
12
|
+
* @param type Error type for machine
|
|
13
|
+
* @param systemError For Node.js system error
|
|
14
|
+
*/
|
|
15
|
+
constructor(message: string, type?: string, systemError?: SystemError);
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { FetchBaseError } from './base.js';
|
|
2
|
+
/**
|
|
3
|
+
* FetchError interface for operational errors
|
|
4
|
+
*/
|
|
5
|
+
export class FetchError extends FetchBaseError {
|
|
6
|
+
code;
|
|
7
|
+
errno;
|
|
8
|
+
erroredSysCall;
|
|
9
|
+
/**
|
|
10
|
+
* @param message Error message for human
|
|
11
|
+
* @param type Error type for machine
|
|
12
|
+
* @param systemError For Node.js system error
|
|
13
|
+
*/
|
|
14
|
+
constructor(message, type, systemError) {
|
|
15
|
+
super(message, type);
|
|
16
|
+
// When err.type is `system`, err.erroredSysCall contains system error and err.code contains system error code
|
|
17
|
+
if (systemError) {
|
|
18
|
+
// eslint-disable-next-line no-multi-assign
|
|
19
|
+
this.code = this.errno = systemError.code;
|
|
20
|
+
this.erroredSysCall = systemError.syscall;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|