@gjsify/fetch 0.3.13 → 0.3.14

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/index.js CHANGED
@@ -1,217 +1,232 @@
1
- import GLib from "@girs/glib-2.0";
2
- import Stream from "node:stream";
3
- import { parseDataUri } from "./utils/data-uri.js";
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 Request, { getSoupRequestOptions } from "./request.js";
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 { FormData } from "@gjsify/formdata";
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
- Blob,
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
- const supportedSchemas = /* @__PURE__ */ new Set(["data:", "http:", "https:", "file:"]);
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
- if (typeof input !== "string") return input;
23
- if (!input.startsWith("/") || input.startsWith("//")) return input;
24
- const DEBUG = globalThis.__GJSIFY_DEBUG_FETCH === true;
25
- try {
26
- const imports = globalThis.imports;
27
- const programPath = imports?.system?.programPath ?? imports?.system?.programInvocationName ?? "";
28
- if (!programPath) return input;
29
- const dir = GLib.path_get_dirname(programPath);
30
- const rewritten = `file://${dir}${input}`;
31
- if (DEBUG) console.log(`[fetch] rewrite ${input} \u2192 ${rewritten}`);
32
- return rewritten;
33
- } catch (err) {
34
- if (DEBUG) console.warn(`[fetch] rewrite FAILED: ${err?.message ?? err}`);
35
- return input;
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
- url = rewriteRootRelativeUrl(url);
40
- const request = new Request(url, init);
41
- const { parsedURL, options } = getSoupRequestOptions(request);
42
- if (!supportedSchemas.has(parsedURL.protocol)) {
43
- throw new TypeError(`@gjsify/fetch cannot load ${url}. URL scheme "${parsedURL.protocol.replace(/:$/, "")}" is not supported.`);
44
- }
45
- if (parsedURL.protocol === "data:") {
46
- const { buffer, typeFull } = parseDataUri(request.url);
47
- const response = new Response(Buffer.from(buffer), { headers: { "Content-Type": typeFull } });
48
- return response;
49
- }
50
- if (parsedURL.protocol === "file:") {
51
- const DEBUG = globalThis.__GJSIFY_DEBUG_FETCH === true;
52
- if (DEBUG) console.log(`[fetch] file:// ${request.url}`);
53
- try {
54
- const path = GLib.filename_from_uri(request.url)[0];
55
- if (DEBUG) console.log(`[fetch] file:// path=${path}`);
56
- const [ok, contents] = GLib.file_get_contents(path);
57
- if (DEBUG) console.log(`[fetch] file:// ok=${ok} bytes=${contents?.byteLength ?? "?"}`);
58
- if (!ok) {
59
- throw new FetchError(`Failed to read file: ${path}`, "system");
60
- }
61
- const bytes = contents;
62
- const body = new Uint8Array(bytes.byteLength);
63
- body.set(bytes);
64
- const resp = new Response(body);
65
- if (DEBUG) console.log(`[fetch] file:// response created`);
66
- return resp;
67
- } catch (error) {
68
- const err = error instanceof Error ? error : new Error(String(error));
69
- if (DEBUG) console.warn(`[fetch] file:// FAIL: ${err.message}`);
70
- throw new FetchError(`request to ${request.url} failed, reason: ${err.message}`, "system", err);
71
- }
72
- }
73
- const { signal } = request;
74
- if (signal && signal.aborted) {
75
- throw new AbortError("The operation was aborted.");
76
- }
77
- let readable;
78
- let cancellable;
79
- try {
80
- const sendRes = await request._send(options);
81
- readable = sendRes.readable;
82
- cancellable = sendRes.cancellable;
83
- } catch (error) {
84
- const err = error instanceof Error ? error : new Error(String(error));
85
- throw new FetchError(`request to ${request.url} failed, reason: ${err.message}`, "system", err);
86
- }
87
- const abortHandler = () => {
88
- cancellable.cancel();
89
- };
90
- if (signal) {
91
- signal.addEventListener("abort", abortHandler, { once: true });
92
- }
93
- const finalize = () => {
94
- if (signal) {
95
- signal.removeEventListener("abort", abortHandler);
96
- }
97
- };
98
- cancellable.connect(() => {
99
- readable.destroy(new AbortError("The operation was aborted."));
100
- });
101
- readable.on("error", (error) => {
102
- finalize();
103
- });
104
- const message = request._message;
105
- const headers = Headers._newFromSoupMessage(message);
106
- const statusCode = message.status_code;
107
- const statusMessage = message.get_reason_phrase();
108
- if (isRedirect(statusCode)) {
109
- const location = headers.get("Location");
110
- let locationURL = null;
111
- try {
112
- locationURL = location === null ? null : new URL(location, request.url);
113
- } catch {
114
- if (request.redirect !== "manual") {
115
- finalize();
116
- throw new FetchError(`uri requested responds with an invalid redirect URL: ${location}`, "invalid-redirect");
117
- }
118
- }
119
- switch (request.redirect) {
120
- case "error":
121
- finalize();
122
- throw new FetchError(`uri requested responds with a redirect, redirect mode is set to error: ${request.url}`, "no-redirect");
123
- case "manual":
124
- break;
125
- case "follow": {
126
- if (locationURL === null) {
127
- break;
128
- }
129
- if (request.counter >= request.follow) {
130
- finalize();
131
- throw new FetchError(`maximum redirect reached at: ${request.url}`, "max-redirect");
132
- }
133
- const requestOptions = {
134
- headers: new Headers(request.headers),
135
- follow: request.follow,
136
- counter: request.counter + 1,
137
- agent: request.agent,
138
- compress: request.compress,
139
- method: request.method,
140
- body: clone(request),
141
- signal: request.signal,
142
- size: request.size,
143
- referrer: request.referrer,
144
- referrerPolicy: request.referrerPolicy
145
- };
146
- if (!isDomainOrSubdomain(request.url, locationURL) || !isSameProtocol(request.url, locationURL)) {
147
- for (const name of ["authorization", "www-authenticate", "cookie", "cookie2"]) {
148
- requestOptions.headers.delete(name);
149
- }
150
- }
151
- if (statusCode !== 303 && request.body && init.body instanceof Stream.Readable) {
152
- finalize();
153
- throw new FetchError("Cannot follow redirect with body being a readable stream", "unsupported-redirect");
154
- }
155
- if (statusCode === 303 || (statusCode === 301 || statusCode === 302) && request.method === "POST") {
156
- requestOptions.method = "GET";
157
- requestOptions.body = void 0;
158
- requestOptions.headers.delete("content-length");
159
- }
160
- const responseReferrerPolicy = parseReferrerPolicyFromHeader(headers);
161
- if (responseReferrerPolicy) {
162
- requestOptions.referrerPolicy = responseReferrerPolicy;
163
- }
164
- finalize();
165
- return fetch(new Request(locationURL, requestOptions));
166
- }
167
- default:
168
- throw new TypeError(`Redirect option '${request.redirect}' is not a valid value of RequestRedirect`);
169
- }
170
- }
171
- const responseOptions = {
172
- url: request.url,
173
- status: statusCode,
174
- statusText: statusMessage,
175
- headers,
176
- size: request.size,
177
- counter: request.counter,
178
- highWaterMark: request.highWaterMark
179
- };
180
- const codings = headers.get("Content-Encoding");
181
- if (!request.compress || request.method === "HEAD" || codings === null || statusCode === 204 || statusCode === 304) {
182
- finalize();
183
- return new Response(readable, responseOptions);
184
- }
185
- if (typeof DecompressionStream !== "undefined") {
186
- let format = null;
187
- if (codings === "gzip" || codings === "x-gzip") {
188
- format = "gzip";
189
- } else if (codings === "deflate" || codings === "x-deflate") {
190
- format = "deflate";
191
- }
192
- if (format) {
193
- const webBody = new Response(readable, responseOptions).body;
194
- if (webBody) {
195
- const decompressed = webBody.pipeThrough(new DecompressionStream(format));
196
- finalize();
197
- return new Response(decompressed, responseOptions);
198
- }
199
- }
200
- }
201
- finalize();
202
- return new Response(readable, responseOptions);
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
- export {
205
- AbortError,
206
- Blob,
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 fetch, { Headers, Request, Response } from "../index.js";
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
- globalThis.fetch = fetch;
8
+ globalThis.fetch = fetch;
4
9
  }
5
10
  if (typeof globalThis.Headers === "undefined") {
6
- globalThis.Headers = Headers;
11
+ globalThis.Headers = Headers;
7
12
  }
8
13
  if (typeof globalThis.Request === "undefined") {
9
- globalThis.Request = Request;
14
+ globalThis.Request = Request;
10
15
  }
11
16
  if (typeof globalThis.Response === "undefined") {
12
- globalThis.Response = Response;
17
+ globalThis.Response = Response;
13
18
  }
19
+
20
+ //#endregion
@@ -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
- globalThis.XMLHttpRequest = XMLHttpRequest;
5
+ globalThis.XMLHttpRequest = XMLHttpRequest;
4
6
  }
5
7
  if (typeof globalThis.XMLHttpRequestUpload === "undefined") {
6
- globalThis.XMLHttpRequestUpload = XMLHttpRequestUpload;
8
+ globalThis.XMLHttpRequestUpload = XMLHttpRequestUpload;
7
9
  }
10
+
11
+ //#endregion