@gjsify/fetch 0.0.4 → 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.
Files changed (88) hide show
  1. package/README.md +27 -2
  2. package/globals.mjs +12 -0
  3. package/lib/body.d.ts +69 -0
  4. package/lib/body.js +375 -0
  5. package/lib/errors/abort-error.d.ts +7 -0
  6. package/lib/errors/abort-error.js +9 -0
  7. package/lib/errors/base.d.ts +6 -0
  8. package/lib/errors/base.js +17 -0
  9. package/lib/errors/fetch-error.d.ts +16 -0
  10. package/lib/errors/fetch-error.js +23 -0
  11. package/lib/esm/body.js +104 -56
  12. package/lib/esm/errors/base.js +3 -1
  13. package/lib/esm/headers.js +116 -131
  14. package/lib/esm/index.js +145 -190
  15. package/lib/esm/request.js +42 -41
  16. package/lib/esm/response.js +19 -4
  17. package/lib/esm/utils/blob-from.js +2 -98
  18. package/lib/esm/utils/data-uri.js +23 -0
  19. package/lib/esm/utils/is.js +7 -3
  20. package/lib/esm/utils/multipart-parser.js +5 -2
  21. package/lib/esm/utils/referrer.js +10 -10
  22. package/lib/esm/utils/soup-helpers.js +22 -0
  23. package/lib/headers.d.ts +33 -0
  24. package/lib/headers.js +195 -0
  25. package/lib/index.d.ts +18 -0
  26. package/lib/index.js +205 -0
  27. package/lib/request.d.ts +101 -0
  28. package/lib/request.js +308 -0
  29. package/lib/response.d.ts +73 -0
  30. package/lib/response.js +158 -0
  31. package/lib/types/index.d.ts +1 -0
  32. package/lib/types/index.js +1 -0
  33. package/lib/types/system-error.d.ts +11 -0
  34. package/lib/types/system-error.js +2 -0
  35. package/lib/utils/blob-from.d.ts +2 -0
  36. package/lib/utils/blob-from.js +4 -0
  37. package/lib/utils/data-uri.d.ts +10 -0
  38. package/lib/utils/data-uri.js +27 -0
  39. package/lib/utils/get-search.d.ts +1 -0
  40. package/lib/utils/get-search.js +8 -0
  41. package/lib/utils/is-redirect.d.ts +7 -0
  42. package/lib/utils/is-redirect.js +10 -0
  43. package/lib/utils/is.d.ts +35 -0
  44. package/lib/utils/is.js +74 -0
  45. package/lib/utils/multipart-parser.d.ts +2 -0
  46. package/lib/utils/multipart-parser.js +396 -0
  47. package/lib/utils/referrer.d.ts +76 -0
  48. package/lib/utils/referrer.js +283 -0
  49. package/lib/utils/soup-helpers.d.ts +12 -0
  50. package/lib/utils/soup-helpers.js +25 -0
  51. package/package.json +23 -27
  52. package/src/body.ts +181 -169
  53. package/src/errors/base.ts +3 -1
  54. package/src/headers.ts +155 -202
  55. package/src/index.spec.ts +268 -3
  56. package/src/index.ts +199 -312
  57. package/src/request.ts +84 -75
  58. package/src/response.ts +48 -18
  59. package/src/test.mts +1 -1
  60. package/src/utils/blob-from.ts +4 -164
  61. package/src/utils/data-uri.ts +29 -0
  62. package/src/utils/is.ts +15 -15
  63. package/src/utils/multipart-parser.ts +3 -3
  64. package/src/utils/referrer.ts +11 -11
  65. package/src/utils/soup-helpers.ts +37 -0
  66. package/tsconfig.json +4 -4
  67. package/tsconfig.tsbuildinfo +1 -0
  68. package/lib/cjs/body.js +0 -255
  69. package/lib/cjs/errors/abort-error.js +0 -9
  70. package/lib/cjs/errors/base.js +0 -17
  71. package/lib/cjs/errors/fetch-error.js +0 -21
  72. package/lib/cjs/headers.js +0 -202
  73. package/lib/cjs/index.js +0 -224
  74. package/lib/cjs/request.js +0 -281
  75. package/lib/cjs/response.js +0 -133
  76. package/lib/cjs/types/index.js +0 -1
  77. package/lib/cjs/types/system-error.js +0 -1
  78. package/lib/cjs/utils/blob-from.js +0 -101
  79. package/lib/cjs/utils/get-search.js +0 -11
  80. package/lib/cjs/utils/is-redirect.js +0 -7
  81. package/lib/cjs/utils/is.js +0 -28
  82. package/lib/cjs/utils/multipart-parser.js +0 -353
  83. package/lib/cjs/utils/referrer.js +0 -153
  84. package/test.gjs.js +0 -34758
  85. package/test.gjs.mjs +0 -53172
  86. package/test.node.js +0 -1226
  87. package/test.node.mjs +0 -6273
  88. package/tsconfig.types.json +0 -8
package/lib/index.js ADDED
@@ -0,0 +1,205 @@
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)
5
+ import Stream from 'node:stream';
6
+ import { parseDataUri } from './utils/data-uri.js';
7
+ import { clone } from './body.js';
8
+ import Response from './response.js';
9
+ import Headers from './headers.js';
10
+ import Request, { getSoupRequestOptions } from './request.js';
11
+ import { FetchError } from './errors/fetch-error.js';
12
+ import { AbortError } from './errors/abort-error.js';
13
+ import { isRedirect } from './utils/is-redirect.js';
14
+ import { FormData } from '@gjsify/formdata';
15
+ import { isDomainOrSubdomain, isSameProtocol } from './utils/is.js';
16
+ import { parseReferrerPolicyFromHeader } from './utils/referrer.js';
17
+ import { Blob, File, } from './utils/blob-from.js';
18
+ import { URL } from '@gjsify/url';
19
+ export { FormData, Headers, Request, Response, FetchError, AbortError, isRedirect };
20
+ export { Blob, File };
21
+ const supportedSchemas = new Set(['data:', 'http:', 'https:']);
22
+ /**
23
+ * Fetch function
24
+ *
25
+ * @param url Absolute url or Request instance
26
+ * @param init Fetch options
27
+ */
28
+ export default async function fetch(url, init = {}) {
29
+ // Build request object
30
+ const request = new Request(url, init);
31
+ const { parsedURL, options } = getSoupRequestOptions(request);
32
+ if (!supportedSchemas.has(parsedURL.protocol)) {
33
+ throw new TypeError(`@gjsify/fetch cannot load ${url}. URL scheme "${parsedURL.protocol.replace(/:$/, '')}" is not supported.`);
34
+ }
35
+ // Handle data: URIs
36
+ if (parsedURL.protocol === 'data:') {
37
+ const { buffer, typeFull } = parseDataUri(request.url);
38
+ const response = new Response(Buffer.from(buffer), { headers: { 'Content-Type': typeFull } });
39
+ return response;
40
+ }
41
+ const { signal } = request;
42
+ // Check if already aborted
43
+ if (signal && signal.aborted) {
44
+ throw new AbortError('The operation was aborted.');
45
+ }
46
+ // Send HTTP request via Soup
47
+ let readable;
48
+ let cancellable;
49
+ try {
50
+ const sendRes = await request._send(options);
51
+ readable = sendRes.readable;
52
+ cancellable = sendRes.cancellable;
53
+ }
54
+ catch (error) {
55
+ const err = error instanceof Error ? error : new Error(String(error));
56
+ throw new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err);
57
+ }
58
+ // Wire up abort signal to cancellable
59
+ const abortHandler = () => {
60
+ cancellable.cancel();
61
+ };
62
+ if (signal) {
63
+ signal.addEventListener('abort', abortHandler, { once: true });
64
+ }
65
+ const finalize = () => {
66
+ if (signal) {
67
+ signal.removeEventListener('abort', abortHandler);
68
+ }
69
+ };
70
+ // Listen for cancellation
71
+ cancellable.connect('cancelled', () => {
72
+ readable.destroy(new AbortError('The operation was aborted.'));
73
+ });
74
+ // Handle stream errors
75
+ readable.on('error', (error) => {
76
+ finalize();
77
+ // Error is consumed by the body when read
78
+ });
79
+ const message = request._message;
80
+ const headers = Headers._newFromSoupMessage(message);
81
+ const statusCode = message.status_code;
82
+ const statusMessage = message.get_reason_phrase();
83
+ // HTTP fetch step 5 — handle redirects
84
+ if (isRedirect(statusCode)) {
85
+ const location = headers.get('Location');
86
+ let locationURL = null;
87
+ try {
88
+ locationURL = location === null ? null : new URL(location, request.url);
89
+ }
90
+ catch {
91
+ if (request.redirect !== 'manual') {
92
+ finalize();
93
+ throw new FetchError(`uri requested responds with an invalid redirect URL: ${location}`, 'invalid-redirect');
94
+ }
95
+ }
96
+ switch (request.redirect) {
97
+ case 'error':
98
+ finalize();
99
+ throw new FetchError(`uri requested responds with a redirect, redirect mode is set to error: ${request.url}`, 'no-redirect');
100
+ case 'manual':
101
+ // Nothing to do — return opaque redirect response
102
+ break;
103
+ case 'follow': {
104
+ if (locationURL === null) {
105
+ break;
106
+ }
107
+ if (request.counter >= request.follow) {
108
+ finalize();
109
+ throw new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect');
110
+ }
111
+ const requestOptions = {
112
+ headers: new Headers(request.headers),
113
+ follow: request.follow,
114
+ counter: request.counter + 1,
115
+ agent: request.agent,
116
+ compress: request.compress,
117
+ method: request.method,
118
+ body: clone(request),
119
+ signal: request.signal,
120
+ size: request.size,
121
+ referrer: request.referrer,
122
+ referrerPolicy: request.referrerPolicy
123
+ };
124
+ // Don't forward sensitive headers to different domains/protocols
125
+ if (!isDomainOrSubdomain(request.url, locationURL) || !isSameProtocol(request.url, locationURL)) {
126
+ for (const name of ['authorization', 'www-authenticate', 'cookie', 'cookie2']) {
127
+ requestOptions.headers.delete(name);
128
+ }
129
+ }
130
+ // Cannot follow redirect with body being a readable stream
131
+ if (statusCode !== 303 && request.body && init.body instanceof Stream.Readable) {
132
+ finalize();
133
+ throw new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect');
134
+ }
135
+ // 303 or POST→GET conversion
136
+ if (statusCode === 303 || ((statusCode === 301 || statusCode === 302) && request.method === 'POST')) {
137
+ requestOptions.method = 'GET';
138
+ requestOptions.body = undefined;
139
+ requestOptions.headers.delete('content-length');
140
+ }
141
+ // Update referrer policy from response
142
+ const responseReferrerPolicy = parseReferrerPolicyFromHeader(headers);
143
+ if (responseReferrerPolicy) {
144
+ requestOptions.referrerPolicy = responseReferrerPolicy;
145
+ }
146
+ finalize();
147
+ return fetch(new Request(locationURL, requestOptions));
148
+ }
149
+ default:
150
+ throw new TypeError(`Redirect option '${request.redirect}' is not a valid value of RequestRedirect`);
151
+ }
152
+ }
153
+ // Build response
154
+ const responseOptions = {
155
+ url: request.url,
156
+ status: statusCode,
157
+ statusText: statusMessage,
158
+ headers,
159
+ size: request.size,
160
+ counter: request.counter,
161
+ highWaterMark: request.highWaterMark
162
+ };
163
+ // Handle content encoding (decompression)
164
+ const codings = headers.get('Content-Encoding');
165
+ // Skip decompression when:
166
+ // 1. compression support is disabled
167
+ // 2. HEAD request
168
+ // 3. no Content-Encoding header
169
+ // 4. no content response (204)
170
+ // 5. content not modified response (304)
171
+ if (!request.compress || request.method === 'HEAD' || codings === null || statusCode === 204 || statusCode === 304) {
172
+ finalize();
173
+ return new Response(readable, responseOptions);
174
+ }
175
+ // Try to use DecompressionStream Web API (available in modern SpiderMonkey)
176
+ if (typeof DecompressionStream !== 'undefined') {
177
+ let format = null;
178
+ if (codings === 'gzip' || codings === 'x-gzip') {
179
+ format = 'gzip';
180
+ }
181
+ else if (codings === 'deflate' || codings === 'x-deflate') {
182
+ format = 'deflate';
183
+ }
184
+ if (format) {
185
+ const webBody = new Response(readable, responseOptions).body;
186
+ if (webBody) {
187
+ const decompressed = webBody.pipeThrough(new DecompressionStream(format));
188
+ finalize();
189
+ return new Response(decompressed, responseOptions);
190
+ }
191
+ }
192
+ }
193
+ // Fallback: return the body as-is (no streaming decompression available)
194
+ finalize();
195
+ return new Response(readable, responseOptions);
196
+ }
197
+ // Register Fetch API globals on GJS (pattern: @gjsify/abort-controller, @gjsify/eventsource)
198
+ // On Node.js, native globals are already fully functional — only overwrite on GJS.
199
+ const _isGJS = typeof globalThis.imports !== 'undefined';
200
+ if (_isGJS) {
201
+ globalThis.fetch = fetch;
202
+ globalThis.Headers = Headers;
203
+ globalThis.Request = Request;
204
+ globalThis.Response = Response;
205
+ }
@@ -0,0 +1,101 @@
1
+ import GLib from '@girs/glib-2.0';
2
+ import Soup from '@girs/soup-3.0';
3
+ import Gio from '@girs/gio-2.0';
4
+ import { URL } from '@gjsify/url';
5
+ import { Blob } from './utils/blob-from.js';
6
+ import { Readable } from 'node:stream';
7
+ import Headers from './headers.js';
8
+ import Body from './body.js';
9
+ import type { FormData } from '@gjsify/formdata';
10
+ declare const INTERNALS: unique symbol;
11
+ export interface Request extends globalThis.Request {
12
+ }
13
+ /** This Fetch API interface represents a resource request. */
14
+ export declare class Request extends Body {
15
+ /** Returns the cache mode associated with request, which is a string indicating how the request will interact with the browser's cache when fetching. */
16
+ readonly cache: RequestCache;
17
+ /** Returns the credentials mode associated with request, which is a string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. */
18
+ readonly credentials: RequestCredentials;
19
+ /** Returns the kind of resource requested by request, e.g., "document" or "script". */
20
+ readonly destination: RequestDestination;
21
+ /** Returns a Headers object consisting of the headers associated with request. Note that headers added in the network layer by the user agent will not be accounted for in this object, e.g., the "Host" header. */
22
+ get headers(): Headers;
23
+ /** Returns request's subresource integrity metadata, which is a cryptographic hash of the resource being fetched. Its value consists of multiple hashes separated by whitespace. [SRI] */
24
+ readonly integrity: string;
25
+ /** Returns a boolean indicating whether or not request can outlive the global in which it was created. */
26
+ readonly keepalive: boolean;
27
+ /** Returns request's HTTP method, which is "GET" by default. */
28
+ get method(): string;
29
+ /** Returns the mode associated with request, which is a string indicating whether the request will use CORS, or will be restricted to same-origin URLs. */
30
+ readonly mode: RequestMode;
31
+ /** Returns the redirect mode associated with request, which is a string indicating how redirects for the request will be handled during fetching. A request will follow redirects by default. */
32
+ get redirect(): RequestRedirect;
33
+ /**
34
+ * Returns the referrer of request.
35
+ * Its value can be a same-origin URL if explicitly set in init, the empty string to indicate no referrer, and "about:client" when defaulting to the global's default.
36
+ * This is used during fetching to determine the value of the `Referer` header of the request being made.
37
+ * @see https://fetch.spec.whatwg.org/#dom-request-referrer
38
+ **/
39
+ get referrer(): string;
40
+ /** Returns the referrer policy associated with request. This is used during fetching to compute the value of the request's referrer. */
41
+ get referrerPolicy(): ReferrerPolicy;
42
+ set referrerPolicy(referrerPolicy: ReferrerPolicy);
43
+ /** Returns the signal associated with request, which is an AbortSignal object indicating whether or not request has been aborted, and its abort event handler. */
44
+ get signal(): AbortSignal;
45
+ /** Returns the URL of request as a string. */
46
+ get url(): string;
47
+ get _uri(): GLib.Uri;
48
+ get _session(): Soup.Session;
49
+ get _message(): Soup.Message;
50
+ get _inputStream(): Gio.InputStream;
51
+ get [Symbol.toStringTag](): string;
52
+ [INTERNALS]: {
53
+ method: string;
54
+ redirect: RequestRedirect;
55
+ headers: Headers;
56
+ parsedURL: URL;
57
+ signal: AbortSignal;
58
+ referrer: string | URL;
59
+ referrerPolicy: ReferrerPolicy;
60
+ session: Soup.Session | null;
61
+ message: Soup.Message | null;
62
+ inputStream?: Gio.InputStream;
63
+ readable?: Readable;
64
+ };
65
+ follow: number;
66
+ compress: boolean;
67
+ counter: number;
68
+ agent: string | ((url: URL) => string);
69
+ highWaterMark: number;
70
+ insecureHTTPParser: boolean;
71
+ constructor(input: RequestInfo | URL | Request, init?: RequestInit);
72
+ /**
73
+ * Send the request using Soup.
74
+ */
75
+ _send(options: {
76
+ headers: Headers;
77
+ }): Promise<{
78
+ inputStream: Gio.InputStream;
79
+ readable: Readable;
80
+ cancellable: Gio.Cancellable;
81
+ }>;
82
+ /**
83
+ * Clone this request
84
+ */
85
+ clone(): Request;
86
+ arrayBuffer(): Promise<ArrayBuffer>;
87
+ blob(): Promise<Blob>;
88
+ formData(): Promise<FormData>;
89
+ json(): Promise<unknown>;
90
+ text(): Promise<string>;
91
+ }
92
+ export default Request;
93
+ /**
94
+ * @param request
95
+ */
96
+ export declare const getSoupRequestOptions: (request: Request) => {
97
+ parsedURL: URL;
98
+ options: {
99
+ headers: Headers;
100
+ };
101
+ };
package/lib/request.js ADDED
@@ -0,0 +1,308 @@
1
+ // SPDX-License-Identifier: MIT
2
+ // Adapted from node-fetch (https://github.com/node-fetch/node-fetch/blob/main/src/request.js)
3
+ // Copyright (c) node-fetch contributors. MIT license.
4
+ // Modifications: Rewritten for GJS using Soup.Message and Gio
5
+ import GLib from '@girs/glib-2.0';
6
+ import Soup from '@girs/soup-3.0';
7
+ import Gio from '@girs/gio-2.0';
8
+ import { soupSendAsync, inputStreamToReadable } from './utils/soup-helpers.js';
9
+ import { URL } from '@gjsify/url';
10
+ import Headers from './headers.js';
11
+ import Body, { clone, extractContentType, getTotalBytes } from './body.js';
12
+ import { isAbortSignal } from './utils/is.js';
13
+ import { validateReferrerPolicy, determineRequestsReferrer, DEFAULT_REFERRER_POLICY } from './utils/referrer.js';
14
+ const INTERNALS = Symbol('Request internals');
15
+ /**
16
+ * Check if `obj` is an instance of Request.
17
+ */
18
+ const isRequest = (obj) => {
19
+ return (typeof obj === 'object' &&
20
+ typeof obj.url === 'string');
21
+ };
22
+ /** This Fetch API interface represents a resource request. */
23
+ export class Request extends Body {
24
+ /** Returns the cache mode associated with request, which is a string indicating how the request will interact with the browser's cache when fetching. */
25
+ cache;
26
+ /** Returns the credentials mode associated with request, which is a string indicating whether credentials will be sent with the request always, never, or only when sent to a same-origin URL. */
27
+ credentials;
28
+ /** Returns the kind of resource requested by request, e.g., "document" or "script". */
29
+ destination;
30
+ /** Returns a Headers object consisting of the headers associated with request. Note that headers added in the network layer by the user agent will not be accounted for in this object, e.g., the "Host" header. */
31
+ get headers() {
32
+ return this[INTERNALS].headers;
33
+ }
34
+ /** Returns request's subresource integrity metadata, which is a cryptographic hash of the resource being fetched. Its value consists of multiple hashes separated by whitespace. [SRI] */
35
+ integrity;
36
+ /** Returns a boolean indicating whether or not request can outlive the global in which it was created. */
37
+ keepalive;
38
+ /** Returns request's HTTP method, which is "GET" by default. */
39
+ get method() {
40
+ return this[INTERNALS].method;
41
+ }
42
+ /** Returns the mode associated with request, which is a string indicating whether the request will use CORS, or will be restricted to same-origin URLs. */
43
+ mode;
44
+ /** Returns the redirect mode associated with request, which is a string indicating how redirects for the request will be handled during fetching. A request will follow redirects by default. */
45
+ get redirect() {
46
+ return this[INTERNALS].redirect;
47
+ }
48
+ /**
49
+ * Returns the referrer of request.
50
+ * Its value can be a same-origin URL if explicitly set in init, the empty string to indicate no referrer, and "about:client" when defaulting to the global's default.
51
+ * This is used during fetching to determine the value of the `Referer` header of the request being made.
52
+ * @see https://fetch.spec.whatwg.org/#dom-request-referrer
53
+ **/
54
+ get referrer() {
55
+ if (this[INTERNALS].referrer === 'no-referrer') {
56
+ return '';
57
+ }
58
+ if (this[INTERNALS].referrer === 'client') {
59
+ return 'about:client';
60
+ }
61
+ if (this[INTERNALS].referrer) {
62
+ return this[INTERNALS].referrer.toString();
63
+ }
64
+ return undefined;
65
+ }
66
+ /** Returns the referrer policy associated with request. This is used during fetching to compute the value of the request's referrer. */
67
+ get referrerPolicy() {
68
+ return this[INTERNALS].referrerPolicy;
69
+ }
70
+ set referrerPolicy(referrerPolicy) {
71
+ this[INTERNALS].referrerPolicy = validateReferrerPolicy(referrerPolicy);
72
+ }
73
+ /** Returns the signal associated with request, which is an AbortSignal object indicating whether or not request has been aborted, and its abort event handler. */
74
+ get signal() {
75
+ return this[INTERNALS].signal;
76
+ }
77
+ /** Returns the URL of request as a string. */
78
+ get url() {
79
+ return this[INTERNALS].parsedURL.toString();
80
+ }
81
+ get _uri() {
82
+ return GLib.Uri.parse(this.url, GLib.UriFlags.NONE);
83
+ }
84
+ get _session() {
85
+ return this[INTERNALS].session;
86
+ }
87
+ get _message() {
88
+ return this[INTERNALS].message;
89
+ }
90
+ get _inputStream() {
91
+ return this[INTERNALS].inputStream;
92
+ }
93
+ get [Symbol.toStringTag]() {
94
+ return 'Request';
95
+ }
96
+ [INTERNALS];
97
+ // Node-fetch-only options
98
+ follow;
99
+ compress = false;
100
+ counter = 0;
101
+ agent = '';
102
+ highWaterMark = 16384;
103
+ insecureHTTPParser = false;
104
+ constructor(input, init) {
105
+ const inputRL = input;
106
+ const initRL = (init || {});
107
+ let parsedURL;
108
+ let requestObj = {};
109
+ if (isRequest(input)) {
110
+ parsedURL = new URL(inputRL.url);
111
+ requestObj = inputRL;
112
+ }
113
+ else {
114
+ parsedURL = new URL(input);
115
+ }
116
+ if (parsedURL.username !== '' || parsedURL.password !== '') {
117
+ throw new TypeError(`${parsedURL} is an url with embedded credentials.`);
118
+ }
119
+ let method = initRL.method || requestObj.method || 'GET';
120
+ if (/^(delete|get|head|options|post|put)$/i.test(method)) {
121
+ method = method.toUpperCase();
122
+ }
123
+ if ((init?.body != null || (isRequest(input) && inputRL.body !== null)) &&
124
+ (method === 'GET' || method === 'HEAD')) {
125
+ throw new TypeError('Request with GET/HEAD method cannot have body');
126
+ }
127
+ const inputBody = init?.body ? init.body : (isRequest(input) && inputRL.body !== null ? clone(input) : null);
128
+ super(inputBody, {
129
+ size: initRL.size || 0
130
+ });
131
+ const headers = new Headers((init?.headers || inputRL.headers || {}));
132
+ if (inputBody !== null && !headers.has('Content-Type')) {
133
+ const contentType = extractContentType(inputBody, this);
134
+ if (contentType) {
135
+ headers.set('Content-Type', contentType);
136
+ }
137
+ }
138
+ let signal = isRequest(input) ?
139
+ inputRL.signal :
140
+ null;
141
+ if (init && 'signal' in init) {
142
+ signal = init.signal;
143
+ }
144
+ if (signal != null && !isAbortSignal(signal)) {
145
+ throw new TypeError('Expected signal to be an instanceof AbortSignal or EventTarget');
146
+ }
147
+ // §5.4, Request constructor steps, step 15.1
148
+ let referrer = init?.referrer == null ? inputRL.referrer : init.referrer;
149
+ if (referrer === '') {
150
+ // §5.4, Request constructor steps, step 15.2
151
+ referrer = 'no-referrer';
152
+ }
153
+ else if (referrer) {
154
+ // §5.4, Request constructor steps, step 15.3.1, 15.3.2
155
+ const parsedReferrer = new URL(referrer);
156
+ // §5.4, Request constructor steps, step 15.3.3, 15.3.4
157
+ referrer = /^about:(\/\/)?client$/.test(parsedReferrer.toString()) ? 'client' : parsedReferrer;
158
+ }
159
+ else {
160
+ referrer = undefined;
161
+ }
162
+ // Only create Soup objects for HTTP/HTTPS — data: URIs etc. don't go through Soup
163
+ const scheme = parsedURL.protocol;
164
+ let session = null;
165
+ let message = null;
166
+ if (scheme === 'http:' || scheme === 'https:') {
167
+ session = new Soup.Session();
168
+ message = new Soup.Message({
169
+ method,
170
+ uri: GLib.Uri.parse(parsedURL.toString(), GLib.UriFlags.NONE),
171
+ });
172
+ }
173
+ this[INTERNALS] = {
174
+ method,
175
+ redirect: init?.redirect || inputRL.redirect || 'follow',
176
+ headers,
177
+ parsedURL,
178
+ signal,
179
+ referrer,
180
+ referrerPolicy: '',
181
+ session,
182
+ message,
183
+ };
184
+ // Node-fetch-only options
185
+ this.follow = initRL.follow === undefined ? (inputRL.follow === undefined ? 20 : inputRL.follow) : initRL.follow;
186
+ this.compress = initRL.compress === undefined ? (inputRL.compress === undefined ? true : inputRL.compress) : initRL.compress;
187
+ this.counter = initRL.counter || inputRL.counter || 0;
188
+ this.agent = initRL.agent || inputRL.agent;
189
+ this.highWaterMark = initRL.highWaterMark || inputRL.highWaterMark || 16384;
190
+ this.insecureHTTPParser = initRL.insecureHTTPParser || inputRL.insecureHTTPParser || false;
191
+ // §5.4, Request constructor steps, step 16.
192
+ // Default is empty string per https://fetch.spec.whatwg.org/#concept-request-referrer-policy
193
+ this.referrerPolicy = init?.referrerPolicy || inputRL.referrerPolicy || '';
194
+ }
195
+ /**
196
+ * Send the request using Soup.
197
+ */
198
+ async _send(options) {
199
+ const { session, message } = this[INTERNALS];
200
+ if (!session || !message) {
201
+ throw new Error('Cannot send request: no Soup session (non-HTTP URL?)');
202
+ }
203
+ options.headers._appendToSoupMessage(message);
204
+ const cancellable = new Gio.Cancellable();
205
+ this[INTERNALS].inputStream = await soupSendAsync(session, message, GLib.PRIORITY_DEFAULT, cancellable);
206
+ this[INTERNALS].readable = inputStreamToReadable(this[INTERNALS].inputStream);
207
+ return {
208
+ inputStream: this[INTERNALS].inputStream,
209
+ readable: this[INTERNALS].readable,
210
+ cancellable
211
+ };
212
+ }
213
+ /**
214
+ * Clone this request
215
+ */
216
+ clone() {
217
+ return new Request(this);
218
+ }
219
+ async arrayBuffer() {
220
+ return super.arrayBuffer();
221
+ }
222
+ async blob() {
223
+ return super.blob();
224
+ }
225
+ async formData() {
226
+ return super.formData();
227
+ }
228
+ async json() {
229
+ return super.json();
230
+ }
231
+ async text() {
232
+ return super.text();
233
+ }
234
+ }
235
+ Object.defineProperties(Request.prototype, {
236
+ method: { enumerable: true },
237
+ url: { enumerable: true },
238
+ headers: { enumerable: true },
239
+ redirect: { enumerable: true },
240
+ clone: { enumerable: true },
241
+ signal: { enumerable: true },
242
+ referrer: { enumerable: true },
243
+ referrerPolicy: { enumerable: true }
244
+ });
245
+ export default Request;
246
+ /**
247
+ * @param request
248
+ */
249
+ export const getSoupRequestOptions = (request) => {
250
+ const { parsedURL } = request[INTERNALS];
251
+ const headers = new Headers(request[INTERNALS].headers);
252
+ // Fetch step 1.3
253
+ if (!headers.has('Accept')) {
254
+ headers.set('Accept', '*/*');
255
+ }
256
+ // HTTP-network-or-cache fetch steps 2.4-2.7
257
+ let contentLengthValue = null;
258
+ if (request.body === null && /^(post|put)$/i.test(request.method)) {
259
+ contentLengthValue = '0';
260
+ }
261
+ if (request.body !== null) {
262
+ const totalBytes = getTotalBytes(request);
263
+ // Set Content-Length if totalBytes is a Number (that is not NaN)
264
+ if (typeof totalBytes === 'number' && !Number.isNaN(totalBytes)) {
265
+ contentLengthValue = String(totalBytes);
266
+ }
267
+ }
268
+ if (contentLengthValue) {
269
+ headers.set('Content-Length', contentLengthValue);
270
+ }
271
+ // 4.1. Main fetch, step 2.6
272
+ if (request.referrerPolicy === '') {
273
+ request.referrerPolicy = DEFAULT_REFERRER_POLICY;
274
+ }
275
+ // 4.1. Main fetch, step 2.7
276
+ if (request.referrer && request.referrer !== 'no-referrer') {
277
+ request[INTERNALS].referrer = determineRequestsReferrer(request);
278
+ }
279
+ else {
280
+ request[INTERNALS].referrer = 'no-referrer';
281
+ }
282
+ // 4.5. HTTP-network-or-cache fetch, step 6.9
283
+ if (request[INTERNALS].referrer instanceof URL) {
284
+ headers.set('Referer', request.referrer);
285
+ }
286
+ // HTTP-network-or-cache fetch step 2.11
287
+ if (!headers.has('User-Agent')) {
288
+ headers.set('User-Agent', 'gjsify-fetch');
289
+ }
290
+ // HTTP-network-or-cache fetch step 2.15
291
+ if (request.compress && !headers.has('Accept-Encoding')) {
292
+ headers.set('Accept-Encoding', 'gzip, deflate, br');
293
+ }
294
+ let { agent } = request;
295
+ if (typeof agent === 'function') {
296
+ agent = agent(parsedURL);
297
+ }
298
+ if (!headers.has('Connection') && !agent) {
299
+ headers.set('Connection', 'close');
300
+ }
301
+ const options = {
302
+ headers,
303
+ };
304
+ return {
305
+ parsedURL,
306
+ options
307
+ };
308
+ };
@@ -0,0 +1,73 @@
1
+ import Gio from '@girs/gio-2.0';
2
+ import Headers from './headers.js';
3
+ import Body from './body.js';
4
+ import { Blob } from './utils/blob-from.js';
5
+ import type { Readable } from 'node:stream';
6
+ declare const INTERNALS: unique symbol;
7
+ interface ResponseOptions {
8
+ status?: number;
9
+ statusText?: string;
10
+ headers?: HeadersInit | Headers;
11
+ url?: string;
12
+ type?: ResponseType;
13
+ ok?: boolean;
14
+ redirected?: boolean;
15
+ size?: number;
16
+ counter?: number;
17
+ highWaterMark?: number;
18
+ }
19
+ /**
20
+ * Response class
21
+ *
22
+ * Ref: https://fetch.spec.whatwg.org/#response-class
23
+ *
24
+ * @param body Readable stream
25
+ * @param opts Response options
26
+ */
27
+ export declare class Response extends Body {
28
+ [INTERNALS]: {
29
+ type: ResponseType;
30
+ url: string;
31
+ status: number;
32
+ statusText: string;
33
+ headers: Headers;
34
+ counter: number;
35
+ highWaterMark: number;
36
+ };
37
+ _inputStream: Gio.InputStream | null;
38
+ constructor(body?: BodyInit | Readable | Blob | Buffer | null, options?: ResponseOptions);
39
+ get type(): ResponseType;
40
+ get url(): string;
41
+ get status(): number;
42
+ /**
43
+ * Convenience property representing if the request ended normally
44
+ */
45
+ get ok(): boolean;
46
+ get redirected(): boolean;
47
+ get statusText(): string;
48
+ get headers(): Headers;
49
+ get highWaterMark(): number;
50
+ /**
51
+ * Clone this response
52
+ *
53
+ * @return Response
54
+ */
55
+ clone(): Response;
56
+ /**
57
+ * @param url The URL that the new response is to originate from.
58
+ * @param status An optional status code for the response (e.g., 302.)
59
+ * @returns A Response object.
60
+ */
61
+ static redirect(url: string, status?: number): Response;
62
+ static error(): Response;
63
+ /**
64
+ * Create a Response with a JSON body.
65
+ * @param data The data to serialize as JSON.
66
+ * @param init Optional response init options.
67
+ * @returns A Response with the JSON body and appropriate content-type header.
68
+ */
69
+ static json(data: unknown, init?: ResponseOptions): Response;
70
+ get [Symbol.toStringTag](): string;
71
+ text(): Promise<string>;
72
+ }
73
+ export default Response;