@gjsify/fetch 0.0.3 → 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 +5 -5
- 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 -53177
- package/test.node.js +0 -1226
- package/test.node.mjs +0 -6294
- package/tsconfig.types.json +0 -8
package/src/index.ts
CHANGED
|
@@ -1,43 +1,32 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
// SPDX-License-Identifier: MIT
|
|
2
|
+
// Adapted from node-fetch (https://github.com/node-fetch/node-fetch) and the Fetch API spec (https://fetch.spec.whatwg.org/)
|
|
3
|
+
// Copyright (c) node-fetch contributors. MIT license.
|
|
4
|
+
// Modifications: Rewritten for GJS using libsoup 3.0 (Soup.Session)
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
*
|
|
8
|
-
* a request API compatible with window.fetch
|
|
9
|
-
*
|
|
10
|
-
* All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import zlib from 'zlib';
|
|
14
|
-
import Stream, { PassThrough, pipeline as pump } from 'stream';
|
|
6
|
+
import type Gio from '@girs/gio-2.0';
|
|
7
|
+
import Stream from 'node:stream';
|
|
15
8
|
|
|
16
|
-
import
|
|
9
|
+
import { parseDataUri } from './utils/data-uri.js';
|
|
17
10
|
|
|
18
11
|
import { writeToStream, clone } from './body.js';
|
|
19
12
|
import Response from './response.js';
|
|
20
13
|
import Headers from './headers.js';
|
|
21
|
-
import { getSoupRequestOptions } from './request.js';
|
|
14
|
+
import Request, { getSoupRequestOptions } from './request.js';
|
|
22
15
|
import { FetchError } from './errors/fetch-error.js';
|
|
23
16
|
import { AbortError } from './errors/abort-error.js';
|
|
24
17
|
import { isRedirect } from './utils/is-redirect.js';
|
|
25
|
-
import { FormData } from 'formdata
|
|
18
|
+
import { FormData } from '@gjsify/formdata';
|
|
26
19
|
import { isDomainOrSubdomain, isSameProtocol } from './utils/is.js';
|
|
27
20
|
import { parseReferrerPolicyFromHeader } from './utils/referrer.js';
|
|
28
21
|
import {
|
|
29
22
|
Blob,
|
|
30
23
|
File,
|
|
31
|
-
fileFromSync,
|
|
32
|
-
fileFrom,
|
|
33
|
-
blobFromSync,
|
|
34
|
-
blobFrom
|
|
35
24
|
} from './utils/blob-from.js';
|
|
36
25
|
|
|
37
|
-
import { URL } from '@gjsify/
|
|
26
|
+
import { URL } from '@gjsify/url';
|
|
38
27
|
|
|
39
28
|
export { FormData, Headers, Request, Response, FetchError, AbortError, isRedirect };
|
|
40
|
-
export { Blob, File
|
|
29
|
+
export { Blob, File };
|
|
41
30
|
|
|
42
31
|
import type { SystemError } from './types/index.js';
|
|
43
32
|
|
|
@@ -49,319 +38,217 @@ const supportedSchemas = new Set(['data:', 'http:', 'https:']);
|
|
|
49
38
|
* @param url Absolute url or Request instance
|
|
50
39
|
* @param init Fetch options
|
|
51
40
|
*/
|
|
52
|
-
export default async function fetch(url: RequestInfo | URL, init: RequestInit = {}): Promise<Response> {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
41
|
+
export default async function fetch(url: RequestInfo | URL | Request, init: RequestInit = {}): Promise<Response> {
|
|
42
|
+
// Build request object
|
|
43
|
+
const request = new Request(url, init);
|
|
44
|
+
const { parsedURL, options } = getSoupRequestOptions(request);
|
|
45
|
+
if (!supportedSchemas.has(parsedURL.protocol)) {
|
|
46
|
+
throw new TypeError(`@gjsify/fetch cannot load ${url}. URL scheme "${parsedURL.protocol.replace(/:$/, '')}" is not supported.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Handle data: URIs
|
|
50
|
+
if (parsedURL.protocol === 'data:') {
|
|
51
|
+
const { buffer, typeFull } = parseDataUri(request.url);
|
|
52
|
+
const response = new Response(Buffer.from(buffer), { headers: { 'Content-Type': typeFull } });
|
|
53
|
+
return response;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const { signal } = request;
|
|
57
|
+
|
|
58
|
+
// Check if already aborted
|
|
59
|
+
if (signal && signal.aborted) {
|
|
60
|
+
throw new AbortError('The operation was aborted.');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Send HTTP request via Soup
|
|
64
|
+
let readable: Stream.Readable;
|
|
65
|
+
let cancellable: Gio.Cancellable;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const sendRes = await request._send(options);
|
|
69
|
+
readable = sendRes.readable;
|
|
70
|
+
cancellable = sendRes.cancellable;
|
|
71
|
+
} catch (error: unknown) {
|
|
72
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
73
|
+
throw new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err as unknown as SystemError);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Wire up abort signal to cancellable
|
|
77
|
+
const abortHandler = () => {
|
|
78
|
+
cancellable.cancel();
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
if (signal) {
|
|
82
|
+
signal.addEventListener('abort', abortHandler, { once: true });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const finalize = () => {
|
|
86
|
+
if (signal) {
|
|
87
|
+
signal.removeEventListener('abort', abortHandler);
|
|
66
88
|
}
|
|
89
|
+
};
|
|
67
90
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const error = new AbortError('The operation was aborted.');
|
|
73
|
-
reject(error);
|
|
74
|
-
if (request.body && request.body instanceof Stream.Readable) {
|
|
75
|
-
request.body.destroy(error);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (!response || !response.body) {
|
|
79
|
-
return;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
response.body.emit('error', error);
|
|
83
|
-
};
|
|
91
|
+
// Listen for cancellation
|
|
92
|
+
cancellable.connect('cancelled', () => {
|
|
93
|
+
readable.destroy(new AbortError('The operation was aborted.'));
|
|
94
|
+
});
|
|
84
95
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
96
|
+
// Handle stream errors
|
|
97
|
+
readable.on('error', (error: SystemError) => {
|
|
98
|
+
finalize();
|
|
99
|
+
// Error is consumed by the body when read
|
|
100
|
+
});
|
|
89
101
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
102
|
+
const message = request._message;
|
|
103
|
+
const headers = Headers._newFromSoupMessage(message);
|
|
104
|
+
const statusCode = message.status_code;
|
|
105
|
+
const statusMessage = message.get_reason_phrase();
|
|
94
106
|
|
|
95
|
-
|
|
96
|
-
|
|
107
|
+
// HTTP fetch step 5 — handle redirects
|
|
108
|
+
if (isRedirect(statusCode)) {
|
|
109
|
+
const location = headers.get('Location');
|
|
97
110
|
|
|
98
|
-
|
|
111
|
+
let locationURL: URL | null = null;
|
|
99
112
|
try {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
113
|
+
locationURL = location === null ? null : new URL(location, request.url);
|
|
114
|
+
} catch {
|
|
115
|
+
if (request.redirect !== 'manual') {
|
|
116
|
+
finalize();
|
|
117
|
+
throw new FetchError(`uri requested responds with an invalid redirect URL: ${location}`, 'invalid-redirect');
|
|
118
|
+
}
|
|
105
119
|
}
|
|
106
120
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
121
|
+
switch (request.redirect) {
|
|
122
|
+
case 'error':
|
|
123
|
+
finalize();
|
|
124
|
+
throw new FetchError(`uri requested responds with a redirect, redirect mode is set to error: ${request.url}`, 'no-redirect');
|
|
110
125
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
126
|
+
case 'manual':
|
|
127
|
+
// Nothing to do — return opaque redirect response
|
|
128
|
+
break;
|
|
114
129
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const message = request._message;
|
|
124
|
-
|
|
125
|
-
readable.on('error', (error: SystemError) => {
|
|
126
|
-
reject(new FetchError(`request to ${request.url} failed, reason: ${error.message}`, 'system', error));
|
|
127
|
-
finalize();
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
message.connect('finished', (message) => {
|
|
131
|
-
|
|
132
|
-
const headers = Headers._newFromSoupMessage( request._message, Soup.MessageHeadersType.RESPONSE);
|
|
133
|
-
const statusCode = message.status_code;
|
|
134
|
-
const statusMessage = message.get_reason_phrase() ;
|
|
135
|
-
|
|
136
|
-
// HTTP fetch step 5
|
|
137
|
-
if (isRedirect(statusCode)) {
|
|
138
|
-
// HTTP fetch step 5.2
|
|
139
|
-
const location = headers.get('Location');
|
|
140
|
-
|
|
141
|
-
// HTTP fetch step 5.3
|
|
142
|
-
let locationURL = null;
|
|
143
|
-
try {
|
|
144
|
-
locationURL = location === null ? null : new URL(location, request.url);
|
|
145
|
-
} catch {
|
|
146
|
-
// error here can only be invalid URL in Location: header
|
|
147
|
-
// do not throw when options.redirect == manual
|
|
148
|
-
// let the user extract the errorneous redirect URL
|
|
149
|
-
if (request.redirect !== 'manual') {
|
|
150
|
-
reject(new FetchError(`uri requested responds with an invalid redirect URL: ${location}`, 'invalid-redirect'));
|
|
151
|
-
finalize();
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
130
|
+
case 'follow': {
|
|
131
|
+
if (locationURL === null) {
|
|
132
|
+
break;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (request.counter >= request.follow) {
|
|
136
|
+
finalize();
|
|
137
|
+
throw new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect');
|
|
154
138
|
}
|
|
155
139
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
headers: new Headers(request.headers),
|
|
182
|
-
follow: request.follow,
|
|
183
|
-
counter: request.counter + 1,
|
|
184
|
-
agent: request.agent,
|
|
185
|
-
compress: request.compress,
|
|
186
|
-
method: request.method,
|
|
187
|
-
body: clone(request),
|
|
188
|
-
signal: request.signal,
|
|
189
|
-
size: request.size,
|
|
190
|
-
referrer: request.referrer,
|
|
191
|
-
referrerPolicy: request.referrerPolicy
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
// when forwarding sensitive headers like "Authorization",
|
|
195
|
-
// "WWW-Authenticate", and "Cookie" to untrusted targets,
|
|
196
|
-
// headers will be ignored when following a redirect to a domain
|
|
197
|
-
// that is not a subdomain match or exact match of the initial domain.
|
|
198
|
-
// For example, a redirect from "foo.com" to either "foo.com" or "sub.foo.com"
|
|
199
|
-
// will forward the sensitive headers, but a redirect to "bar.com" will not.
|
|
200
|
-
// headers will also be ignored when following a redirect to a domain using
|
|
201
|
-
// a different protocol. For example, a redirect from "https://foo.com" to "http://foo.com"
|
|
202
|
-
// will not forward the sensitive headers
|
|
203
|
-
if (!isDomainOrSubdomain(request.url, locationURL) || !isSameProtocol(request.url, locationURL)) {
|
|
204
|
-
for (const name of ['authorization', 'www-authenticate', 'cookie', 'cookie2']) {
|
|
205
|
-
requestOptions.headers.delete(name);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// HTTP-redirect fetch step 9
|
|
210
|
-
if (statusCode !== 303 && request.body && init.body instanceof Stream.Readable) {
|
|
211
|
-
reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect'));
|
|
212
|
-
finalize();
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// HTTP-redirect fetch step 11
|
|
217
|
-
if (statusCode === 303 || ((statusCode === 301 || statusCode === 302) && request.method === 'POST')) {
|
|
218
|
-
requestOptions.method = 'GET';
|
|
219
|
-
requestOptions.body = undefined;
|
|
220
|
-
requestOptions.headers.delete('content-length');
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// HTTP-redirect fetch step 14
|
|
224
|
-
const responseReferrerPolicy = parseReferrerPolicyFromHeader(headers);
|
|
225
|
-
if (responseReferrerPolicy) {
|
|
226
|
-
requestOptions.referrerPolicy = responseReferrerPolicy;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// HTTP-redirect fetch step 15
|
|
230
|
-
resolve(fetch(new Request(locationURL, requestOptions)));
|
|
231
|
-
finalize();
|
|
232
|
-
return;
|
|
140
|
+
const requestOptions: Omit<RequestInit, 'headers'> & {
|
|
141
|
+
headers: Headers;
|
|
142
|
+
follow: number;
|
|
143
|
+
counter: number;
|
|
144
|
+
agent: string | ((url: URL) => string);
|
|
145
|
+
compress: boolean;
|
|
146
|
+
size: number;
|
|
147
|
+
} = {
|
|
148
|
+
headers: new Headers(request.headers),
|
|
149
|
+
follow: request.follow,
|
|
150
|
+
counter: request.counter + 1,
|
|
151
|
+
agent: request.agent,
|
|
152
|
+
compress: request.compress,
|
|
153
|
+
method: request.method,
|
|
154
|
+
body: clone(request) as unknown as BodyInit | null,
|
|
155
|
+
signal: request.signal,
|
|
156
|
+
size: request.size,
|
|
157
|
+
referrer: request.referrer,
|
|
158
|
+
referrerPolicy: request.referrerPolicy
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// Don't forward sensitive headers to different domains/protocols
|
|
162
|
+
if (!isDomainOrSubdomain(request.url, locationURL) || !isSameProtocol(request.url, locationURL)) {
|
|
163
|
+
for (const name of ['authorization', 'www-authenticate', 'cookie', 'cookie2']) {
|
|
164
|
+
requestOptions.headers.delete(name);
|
|
233
165
|
}
|
|
166
|
+
}
|
|
234
167
|
|
|
235
|
-
|
|
236
|
-
|
|
168
|
+
// Cannot follow redirect with body being a readable stream
|
|
169
|
+
if (statusCode !== 303 && request.body && init.body instanceof Stream.Readable) {
|
|
170
|
+
finalize();
|
|
171
|
+
throw new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect');
|
|
237
172
|
}
|
|
238
|
-
}
|
|
239
173
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
174
|
+
// 303 or POST→GET conversion
|
|
175
|
+
if (statusCode === 303 || ((statusCode === 301 || statusCode === 302) && request.method === 'POST')) {
|
|
176
|
+
requestOptions.method = 'GET';
|
|
177
|
+
requestOptions.body = undefined;
|
|
178
|
+
requestOptions.headers.delete('content-length');
|
|
179
|
+
}
|
|
246
180
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
181
|
+
// Update referrer policy from response
|
|
182
|
+
const responseReferrerPolicy = parseReferrerPolicyFromHeader(headers);
|
|
183
|
+
if (responseReferrerPolicy) {
|
|
184
|
+
requestOptions.referrerPolicy = responseReferrerPolicy;
|
|
250
185
|
}
|
|
251
|
-
});
|
|
252
|
-
// see https://github.com/nodejs/node/pull/29376
|
|
253
|
-
/* c8 ignore next 3 */
|
|
254
|
-
// if (process.version < 'v12.10') {
|
|
255
|
-
// response_.on('aborted', abortAndFinalize);
|
|
256
|
-
// }
|
|
257
|
-
|
|
258
|
-
const responseOptions = {
|
|
259
|
-
url: request.url,
|
|
260
|
-
status: statusCode,
|
|
261
|
-
statusText: statusMessage,
|
|
262
|
-
headers,
|
|
263
|
-
size: request.size,
|
|
264
|
-
counter: request.counter,
|
|
265
|
-
highWaterMark: request.highWaterMark
|
|
266
|
-
};
|
|
267
|
-
|
|
268
|
-
// HTTP-network fetch step 12.1.1.3
|
|
269
|
-
const codings = headers.get('Content-Encoding');
|
|
270
|
-
|
|
271
|
-
// HTTP-network fetch step 12.1.1.4: handle content codings
|
|
272
|
-
|
|
273
|
-
// in following scenarios we ignore compression support
|
|
274
|
-
// 1. compression support is disabled
|
|
275
|
-
// 2. HEAD request
|
|
276
|
-
// 3. no Content-Encoding header
|
|
277
|
-
// 4. no content response (204)
|
|
278
|
-
// 5. content not modified response (304)
|
|
279
|
-
if (!request.compress || request.method === 'HEAD' || codings === null || statusCode === 204 || statusCode === 304) {
|
|
280
|
-
response = new Response(body, responseOptions);
|
|
281
|
-
resolve(response);
|
|
282
|
-
return;
|
|
283
|
-
}
|
|
284
186
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
// servers send slightly invalid responses that are still accepted
|
|
288
|
-
// by common browsers.
|
|
289
|
-
// Always using Z_SYNC_FLUSH is what cURL does.
|
|
290
|
-
const zlibOptions = {
|
|
291
|
-
flush: zlib.Z_SYNC_FLUSH,
|
|
292
|
-
finishFlush: zlib.Z_SYNC_FLUSH
|
|
293
|
-
};
|
|
294
|
-
|
|
295
|
-
// For gzip
|
|
296
|
-
if (codings === 'gzip' || codings === 'x-gzip') {
|
|
297
|
-
body = pump(body, zlib.createGunzip(zlibOptions), error => {
|
|
298
|
-
if (error) {
|
|
299
|
-
reject(error);
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
response = new Response(body, responseOptions);
|
|
303
|
-
resolve(response);
|
|
304
|
-
return;
|
|
187
|
+
finalize();
|
|
188
|
+
return fetch(new Request(locationURL, requestOptions as unknown as RequestInit));
|
|
305
189
|
}
|
|
306
190
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
if (
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
}
|
|
354
|
-
});
|
|
355
|
-
response = new Response(body, responseOptions);
|
|
356
|
-
resolve(response);
|
|
357
|
-
return;
|
|
191
|
+
default:
|
|
192
|
+
throw new TypeError(`Redirect option '${request.redirect}' is not a valid value of RequestRedirect`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Build response
|
|
197
|
+
const responseOptions = {
|
|
198
|
+
url: request.url,
|
|
199
|
+
status: statusCode,
|
|
200
|
+
statusText: statusMessage,
|
|
201
|
+
headers,
|
|
202
|
+
size: request.size,
|
|
203
|
+
counter: request.counter,
|
|
204
|
+
highWaterMark: request.highWaterMark
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// Handle content encoding (decompression)
|
|
208
|
+
const codings = headers.get('Content-Encoding');
|
|
209
|
+
|
|
210
|
+
// Skip decompression when:
|
|
211
|
+
// 1. compression support is disabled
|
|
212
|
+
// 2. HEAD request
|
|
213
|
+
// 3. no Content-Encoding header
|
|
214
|
+
// 4. no content response (204)
|
|
215
|
+
// 5. content not modified response (304)
|
|
216
|
+
if (!request.compress || request.method === 'HEAD' || codings === null || statusCode === 204 || statusCode === 304) {
|
|
217
|
+
finalize();
|
|
218
|
+
return new Response(readable, responseOptions);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Try to use DecompressionStream Web API (available in modern SpiderMonkey)
|
|
222
|
+
if (typeof DecompressionStream !== 'undefined') {
|
|
223
|
+
let format: CompressionFormat | null = null;
|
|
224
|
+
|
|
225
|
+
if (codings === 'gzip' || codings === 'x-gzip') {
|
|
226
|
+
format = 'gzip';
|
|
227
|
+
} else if (codings === 'deflate' || codings === 'x-deflate') {
|
|
228
|
+
format = 'deflate';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (format) {
|
|
232
|
+
const webBody = new Response(readable, responseOptions).body;
|
|
233
|
+
if (webBody) {
|
|
234
|
+
const decompressed = webBody.pipeThrough(new DecompressionStream(format) as ReadableWritablePair<Uint8Array, Uint8Array>);
|
|
235
|
+
finalize();
|
|
236
|
+
return new Response(decompressed as unknown as ReadableStream, responseOptions);
|
|
358
237
|
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
359
240
|
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
241
|
+
// Fallback: return the body as-is (no streaming decompression available)
|
|
242
|
+
finalize();
|
|
243
|
+
return new Response(readable, responseOptions);
|
|
244
|
+
}
|
|
364
245
|
|
|
365
|
-
|
|
366
|
-
|
|
246
|
+
// Register Fetch API globals on GJS (pattern: @gjsify/abort-controller, @gjsify/eventsource)
|
|
247
|
+
// On Node.js, native globals are already fully functional — only overwrite on GJS.
|
|
248
|
+
const _isGJS = typeof (globalThis as any).imports !== 'undefined';
|
|
249
|
+
if (_isGJS) {
|
|
250
|
+
(globalThis as any).fetch = fetch;
|
|
251
|
+
(globalThis as any).Headers = Headers;
|
|
252
|
+
(globalThis as any).Request = Request;
|
|
253
|
+
(globalThis as any).Response = Response;
|
|
367
254
|
}
|