@gjsify/fetch 0.3.13 → 0.3.15
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/lib/esm/body.js +325 -303
- package/lib/esm/errors/abort-error.js +12 -7
- package/lib/esm/errors/base.js +19 -18
- package/lib/esm/errors/fetch-error.js +24 -19
- package/lib/esm/headers.js +188 -182
- package/lib/esm/index.js +223 -208
- package/lib/esm/register/fetch.js +12 -5
- package/lib/esm/register/xhr.js +6 -2
- package/lib/esm/request.js +279 -283
- package/lib/esm/response.js +152 -143
- package/lib/esm/types/index.js +1 -1
- package/lib/esm/types/system-error.js +3 -0
- package/lib/esm/utils/blob-from.js +2 -4
- package/lib/esm/utils/data-uri.js +31 -21
- package/lib/esm/utils/get-search.js +10 -9
- package/lib/esm/utils/is-redirect.js +18 -5
- package/lib/esm/utils/is.js +52 -20
- package/lib/esm/utils/multipart-parser.js +334 -338
- package/lib/esm/utils/referrer.js +189 -137
- package/lib/esm/utils/soup-helpers.js +23 -16
- package/lib/esm/xhr.js +234 -246
- package/package.json +11 -11
package/lib/esm/index.js
CHANGED
|
@@ -1,217 +1,232 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
1
|
+
import { Blob, File } from "./utils/blob-from.js";
|
|
2
|
+
import { FetchError } from "./errors/fetch-error.js";
|
|
3
|
+
import { isDomainOrSubdomain, isSameProtocol } from "./utils/is.js";
|
|
4
4
|
import { clone } from "./body.js";
|
|
5
|
-
import Response from "./response.js";
|
|
6
5
|
import Headers from "./headers.js";
|
|
7
|
-
import
|
|
8
|
-
import { FetchError } from "./errors/fetch-error.js";
|
|
9
|
-
import { AbortError } from "./errors/abort-error.js";
|
|
6
|
+
import { parseDataUri } from "./utils/data-uri.js";
|
|
10
7
|
import { isRedirect } from "./utils/is-redirect.js";
|
|
11
|
-
import {
|
|
12
|
-
import { isDomainOrSubdomain, isSameProtocol } from "./utils/is.js";
|
|
8
|
+
import { Response } from "./response.js";
|
|
13
9
|
import { parseReferrerPolicyFromHeader } from "./utils/referrer.js";
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
File
|
|
17
|
-
} from "./utils/blob-from.js";
|
|
18
|
-
import { URL } from "@gjsify/url";
|
|
10
|
+
import { Request, getSoupRequestOptions } from "./request.js";
|
|
11
|
+
import { AbortError } from "./errors/abort-error.js";
|
|
19
12
|
import { XMLHttpRequest, XMLHttpRequestUpload } from "./xhr.js";
|
|
20
|
-
|
|
13
|
+
import { URL } from "@gjsify/url";
|
|
14
|
+
import Stream from "node:stream";
|
|
15
|
+
import { FormData } from "@gjsify/formdata";
|
|
16
|
+
import GLib from "@girs/glib-2.0";
|
|
17
|
+
|
|
18
|
+
//#region src/index.ts
|
|
19
|
+
const supportedSchemas = new Set([
|
|
20
|
+
"data:",
|
|
21
|
+
"http:",
|
|
22
|
+
"https:",
|
|
23
|
+
"file:"
|
|
24
|
+
]);
|
|
25
|
+
/**
|
|
26
|
+
* Rewrite root-relative URLs (e.g. `/res/images/foo.png`) to `file://` relative
|
|
27
|
+
* to the program directory. In a browser these would resolve against the page
|
|
28
|
+
* origin; in GJS there is no origin, so we map them to the running bundle's
|
|
29
|
+
* directory. This lets apps use the same asset paths across browser and GJS.
|
|
30
|
+
*/
|
|
31
|
+
/**
|
|
32
|
+
* Rewrite root-relative URLs (e.g. `/res/images/foo.png`) to `file://` relative
|
|
33
|
+
* to the program directory. This lets GJS apps load bundled assets using the
|
|
34
|
+
* same paths as in the browser. The security implications (arbitrary file
|
|
35
|
+
* reads via fetch) are acceptable for the current use cases — revisit if
|
|
36
|
+
* @gjsify/fetch is ever used to handle untrusted input.
|
|
37
|
+
*/
|
|
21
38
|
function rewriteRootRelativeUrl(input) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
39
|
+
if (typeof input !== "string") return input;
|
|
40
|
+
if (!input.startsWith("/") || input.startsWith("//")) return input;
|
|
41
|
+
const DEBUG = globalThis.__GJSIFY_DEBUG_FETCH === true;
|
|
42
|
+
try {
|
|
43
|
+
const imports = globalThis.imports;
|
|
44
|
+
const programPath = imports?.system?.programPath ?? imports?.system?.programInvocationName ?? "";
|
|
45
|
+
if (!programPath) return input;
|
|
46
|
+
const dir = GLib.path_get_dirname(programPath);
|
|
47
|
+
const rewritten = `file://${dir}${input}`;
|
|
48
|
+
if (DEBUG) console.log(`[fetch] rewrite ${input} → ${rewritten}`);
|
|
49
|
+
return rewritten;
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (DEBUG) console.warn(`[fetch] rewrite FAILED: ${err?.message ?? err}`);
|
|
52
|
+
return input;
|
|
53
|
+
}
|
|
37
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Fetch function
|
|
57
|
+
*
|
|
58
|
+
* @param url Absolute url or Request instance
|
|
59
|
+
* @param init Fetch options
|
|
60
|
+
*/
|
|
38
61
|
async function fetch(url, init = {}) {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
62
|
+
url = rewriteRootRelativeUrl(url);
|
|
63
|
+
const request = new Request(url, init);
|
|
64
|
+
const { parsedURL, options } = getSoupRequestOptions(request);
|
|
65
|
+
if (!supportedSchemas.has(parsedURL.protocol)) {
|
|
66
|
+
throw new TypeError(`@gjsify/fetch cannot load ${url}. URL scheme "${parsedURL.protocol.replace(/:$/, "")}" is not supported.`);
|
|
67
|
+
}
|
|
68
|
+
if (parsedURL.protocol === "data:") {
|
|
69
|
+
const { buffer, typeFull } = parseDataUri(request.url);
|
|
70
|
+
const response = new Response(Buffer.from(buffer), { headers: { "Content-Type": typeFull } });
|
|
71
|
+
return response;
|
|
72
|
+
}
|
|
73
|
+
if (parsedURL.protocol === "file:") {
|
|
74
|
+
const DEBUG = globalThis.__GJSIFY_DEBUG_FETCH === true;
|
|
75
|
+
if (DEBUG) console.log(`[fetch] file:// ${request.url}`);
|
|
76
|
+
try {
|
|
77
|
+
const path = GLib.filename_from_uri(request.url)[0];
|
|
78
|
+
if (DEBUG) console.log(`[fetch] file:// path=${path}`);
|
|
79
|
+
const [ok, contents] = GLib.file_get_contents(path);
|
|
80
|
+
if (DEBUG) console.log(`[fetch] file:// ok=${ok} bytes=${contents?.byteLength ?? "?"}`);
|
|
81
|
+
if (!ok) {
|
|
82
|
+
throw new FetchError(`Failed to read file: ${path}`, "system");
|
|
83
|
+
}
|
|
84
|
+
const bytes = contents;
|
|
85
|
+
const body = new Uint8Array(bytes.byteLength);
|
|
86
|
+
body.set(bytes);
|
|
87
|
+
const resp = new Response(body);
|
|
88
|
+
if (DEBUG) console.log(`[fetch] file:// response created`);
|
|
89
|
+
return resp;
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
92
|
+
if (DEBUG) console.warn(`[fetch] file:// FAIL: ${err.message}`);
|
|
93
|
+
throw new FetchError(`request to ${request.url} failed, reason: ${err.message}`, "system", err);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const { signal } = request;
|
|
97
|
+
if (signal && signal.aborted) {
|
|
98
|
+
throw new AbortError("The operation was aborted.");
|
|
99
|
+
}
|
|
100
|
+
let readable;
|
|
101
|
+
let cancellable;
|
|
102
|
+
try {
|
|
103
|
+
const sendRes = await request._send(options);
|
|
104
|
+
readable = sendRes.readable;
|
|
105
|
+
cancellable = sendRes.cancellable;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
108
|
+
throw new FetchError(`request to ${request.url} failed, reason: ${err.message}`, "system", err);
|
|
109
|
+
}
|
|
110
|
+
const abortHandler = () => {
|
|
111
|
+
cancellable.cancel();
|
|
112
|
+
};
|
|
113
|
+
if (signal) {
|
|
114
|
+
signal.addEventListener("abort", abortHandler, { once: true });
|
|
115
|
+
}
|
|
116
|
+
const finalize = () => {
|
|
117
|
+
if (signal) {
|
|
118
|
+
signal.removeEventListener("abort", abortHandler);
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
cancellable.connect(() => {
|
|
122
|
+
readable.destroy(new AbortError("The operation was aborted."));
|
|
123
|
+
});
|
|
124
|
+
readable.on("error", (error) => {
|
|
125
|
+
finalize();
|
|
126
|
+
});
|
|
127
|
+
const message = request._message;
|
|
128
|
+
const headers = Headers._newFromSoupMessage(message);
|
|
129
|
+
const statusCode = message.status_code;
|
|
130
|
+
const statusMessage = message.get_reason_phrase();
|
|
131
|
+
if (isRedirect(statusCode)) {
|
|
132
|
+
const location = headers.get("Location");
|
|
133
|
+
let locationURL = null;
|
|
134
|
+
try {
|
|
135
|
+
locationURL = location === null ? null : new URL(location, request.url);
|
|
136
|
+
} catch {
|
|
137
|
+
if (request.redirect !== "manual") {
|
|
138
|
+
finalize();
|
|
139
|
+
throw new FetchError(`uri requested responds with an invalid redirect URL: ${location}`, "invalid-redirect");
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
switch (request.redirect) {
|
|
143
|
+
case "error":
|
|
144
|
+
finalize();
|
|
145
|
+
throw new FetchError(`uri requested responds with a redirect, redirect mode is set to error: ${request.url}`, "no-redirect");
|
|
146
|
+
case "manual": break;
|
|
147
|
+
case "follow": {
|
|
148
|
+
if (locationURL === null) {
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
if (request.counter >= request.follow) {
|
|
152
|
+
finalize();
|
|
153
|
+
throw new FetchError(`maximum redirect reached at: ${request.url}`, "max-redirect");
|
|
154
|
+
}
|
|
155
|
+
const requestOptions = {
|
|
156
|
+
headers: new Headers(request.headers),
|
|
157
|
+
follow: request.follow,
|
|
158
|
+
counter: request.counter + 1,
|
|
159
|
+
agent: request.agent,
|
|
160
|
+
compress: request.compress,
|
|
161
|
+
method: request.method,
|
|
162
|
+
body: clone(request),
|
|
163
|
+
signal: request.signal,
|
|
164
|
+
size: request.size,
|
|
165
|
+
referrer: request.referrer,
|
|
166
|
+
referrerPolicy: request.referrerPolicy
|
|
167
|
+
};
|
|
168
|
+
if (!isDomainOrSubdomain(request.url, locationURL) || !isSameProtocol(request.url, locationURL)) {
|
|
169
|
+
for (const name of [
|
|
170
|
+
"authorization",
|
|
171
|
+
"www-authenticate",
|
|
172
|
+
"cookie",
|
|
173
|
+
"cookie2"
|
|
174
|
+
]) {
|
|
175
|
+
requestOptions.headers.delete(name);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (statusCode !== 303 && request.body && init.body instanceof Stream.Readable) {
|
|
179
|
+
finalize();
|
|
180
|
+
throw new FetchError("Cannot follow redirect with body being a readable stream", "unsupported-redirect");
|
|
181
|
+
}
|
|
182
|
+
if (statusCode === 303 || (statusCode === 301 || statusCode === 302) && request.method === "POST") {
|
|
183
|
+
requestOptions.method = "GET";
|
|
184
|
+
requestOptions.body = undefined;
|
|
185
|
+
requestOptions.headers.delete("content-length");
|
|
186
|
+
}
|
|
187
|
+
const responseReferrerPolicy = parseReferrerPolicyFromHeader(headers);
|
|
188
|
+
if (responseReferrerPolicy) {
|
|
189
|
+
requestOptions.referrerPolicy = responseReferrerPolicy;
|
|
190
|
+
}
|
|
191
|
+
finalize();
|
|
192
|
+
return fetch(new Request(locationURL, requestOptions));
|
|
193
|
+
}
|
|
194
|
+
default: throw new TypeError(`Redirect option '${request.redirect}' is not a valid value of RequestRedirect`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
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
|
+
const codings = headers.get("Content-Encoding");
|
|
207
|
+
if (!request.compress || request.method === "HEAD" || codings === null || statusCode === 204 || statusCode === 304) {
|
|
208
|
+
finalize();
|
|
209
|
+
return new Response(readable, responseOptions);
|
|
210
|
+
}
|
|
211
|
+
if (typeof DecompressionStream !== "undefined") {
|
|
212
|
+
let format = null;
|
|
213
|
+
if (codings === "gzip" || codings === "x-gzip") {
|
|
214
|
+
format = "gzip";
|
|
215
|
+
} else if (codings === "deflate" || codings === "x-deflate") {
|
|
216
|
+
format = "deflate";
|
|
217
|
+
}
|
|
218
|
+
if (format) {
|
|
219
|
+
const webBody = new Response(readable, responseOptions).body;
|
|
220
|
+
if (webBody) {
|
|
221
|
+
const decompressed = webBody.pipeThrough(new DecompressionStream(format));
|
|
222
|
+
finalize();
|
|
223
|
+
return new Response(decompressed, responseOptions);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
finalize();
|
|
228
|
+
return new Response(readable, responseOptions);
|
|
203
229
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
FetchError,
|
|
208
|
-
File,
|
|
209
|
-
FormData,
|
|
210
|
-
Headers,
|
|
211
|
-
Request,
|
|
212
|
-
Response,
|
|
213
|
-
XMLHttpRequest,
|
|
214
|
-
XMLHttpRequestUpload,
|
|
215
|
-
fetch as default,
|
|
216
|
-
isRedirect
|
|
217
|
-
};
|
|
230
|
+
|
|
231
|
+
//#endregion
|
|
232
|
+
export { AbortError, Blob, FetchError, File, FormData, Headers, Request, Response, XMLHttpRequest, XMLHttpRequestUpload, fetch as default, isRedirect };
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import Headers from "../headers.js";
|
|
2
|
+
import { Response } from "../response.js";
|
|
3
|
+
import { Request } from "../request.js";
|
|
4
|
+
import fetch from "../index.js";
|
|
5
|
+
|
|
6
|
+
//#region src/register/fetch.ts
|
|
2
7
|
if (typeof globalThis.fetch === "undefined") {
|
|
3
|
-
|
|
8
|
+
globalThis.fetch = fetch;
|
|
4
9
|
}
|
|
5
10
|
if (typeof globalThis.Headers === "undefined") {
|
|
6
|
-
|
|
11
|
+
globalThis.Headers = Headers;
|
|
7
12
|
}
|
|
8
13
|
if (typeof globalThis.Request === "undefined") {
|
|
9
|
-
|
|
14
|
+
globalThis.Request = Request;
|
|
10
15
|
}
|
|
11
16
|
if (typeof globalThis.Response === "undefined") {
|
|
12
|
-
|
|
17
|
+
globalThis.Response = Response;
|
|
13
18
|
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
package/lib/esm/register/xhr.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { XMLHttpRequest, XMLHttpRequestUpload } from "../xhr.js";
|
|
2
|
+
|
|
3
|
+
//#region src/register/xhr.ts
|
|
2
4
|
if (typeof globalThis.XMLHttpRequest === "undefined") {
|
|
3
|
-
|
|
5
|
+
globalThis.XMLHttpRequest = XMLHttpRequest;
|
|
4
6
|
}
|
|
5
7
|
if (typeof globalThis.XMLHttpRequestUpload === "undefined") {
|
|
6
|
-
|
|
8
|
+
globalThis.XMLHttpRequestUpload = XMLHttpRequestUpload;
|
|
7
9
|
}
|
|
10
|
+
|
|
11
|
+
//#endregion
|