@gjsify/fetch 0.4.28 → 0.4.30
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/body.d.ts +2 -1
- package/lib/body.js +21 -19
- package/lib/esm/request.js +1 -1
- package/lib/esm/utils/referrer.js +1 -1
- package/lib/headers.js +2 -2
- package/lib/index.js +10 -8
- package/lib/index.spec.js +27 -14
- package/lib/request.d.ts +2 -2
- package/lib/request.js +77 -21
- package/lib/response.d.ts +1 -1
- package/lib/response.js +5 -5
- package/lib/soup-session.gjs.spec.d.ts +2 -0
- package/lib/soup-session.gjs.spec.js +118 -0
- package/lib/types/system-error.js +0 -1
- package/lib/utils/blob-from.d.ts +1 -1
- package/lib/utils/blob-from.js +1 -1
- package/lib/utils/is-redirect.js +1 -1
- package/lib/utils/is.d.ts +1 -1
- package/lib/utils/is.js +2 -3
- package/lib/utils/multipart-parser.js +2 -2
- package/lib/utils/referrer.d.ts +2 -2
- package/lib/utils/referrer.js +4 -3
- package/lib/utils/soup-helpers.d.ts +2 -2
- package/lib/xhr.js +8 -7
- package/package.json +7 -7
package/lib/body.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Blob } from './utils/blob-from.js';
|
|
2
|
-
import {
|
|
2
|
+
import type { Writable } from 'node:stream';
|
|
3
|
+
import { Readable } from 'node:stream';
|
|
3
4
|
import { Buffer } from 'node:buffer';
|
|
4
5
|
import { FormData } from '@gjsify/formdata';
|
|
5
6
|
import { FetchBaseError } from './errors/base.js';
|
package/lib/body.js
CHANGED
|
@@ -20,15 +20,7 @@ const pipeline = (source, dest) => new Promise((resolve, reject) => {
|
|
|
20
20
|
});
|
|
21
21
|
const INTERNALS = Symbol('Body internals');
|
|
22
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));
|
|
23
|
+
return val instanceof ArrayBuffer || (typeof SharedArrayBuffer !== 'undefined' && val instanceof SharedArrayBuffer);
|
|
32
24
|
}
|
|
33
25
|
/**
|
|
34
26
|
* Body mixin
|
|
@@ -133,7 +125,9 @@ export default class Body {
|
|
|
133
125
|
try {
|
|
134
126
|
controller.enqueue(chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk));
|
|
135
127
|
}
|
|
136
|
-
catch {
|
|
128
|
+
catch {
|
|
129
|
+
/* consumer cancelled — drop */
|
|
130
|
+
}
|
|
137
131
|
});
|
|
138
132
|
stream.on('end', () => {
|
|
139
133
|
if (closed)
|
|
@@ -145,7 +139,9 @@ export default class Body {
|
|
|
145
139
|
try {
|
|
146
140
|
controller.close();
|
|
147
141
|
}
|
|
148
|
-
catch {
|
|
142
|
+
catch {
|
|
143
|
+
/* already closed/cancelled */
|
|
144
|
+
}
|
|
149
145
|
});
|
|
150
146
|
stream.on('error', (err) => {
|
|
151
147
|
if (closed)
|
|
@@ -154,13 +150,15 @@ export default class Body {
|
|
|
154
150
|
try {
|
|
155
151
|
controller.error(err);
|
|
156
152
|
}
|
|
157
|
-
catch {
|
|
153
|
+
catch {
|
|
154
|
+
/* already closed/cancelled */
|
|
155
|
+
}
|
|
158
156
|
});
|
|
159
157
|
},
|
|
160
158
|
cancel() {
|
|
161
159
|
closed = true;
|
|
162
160
|
stream.destroy();
|
|
163
|
-
}
|
|
161
|
+
},
|
|
164
162
|
});
|
|
165
163
|
}
|
|
166
164
|
return null;
|
|
@@ -206,10 +204,12 @@ export default class Body {
|
|
|
206
204
|
* Return raw response as Blob
|
|
207
205
|
*/
|
|
208
206
|
async blob() {
|
|
209
|
-
const ct =
|
|
207
|
+
const ct = this.headers?.get('content-type') ||
|
|
208
|
+
(this[INTERNALS].body && this[INTERNALS].body.type) ||
|
|
209
|
+
'';
|
|
210
210
|
const buf = await this.arrayBuffer();
|
|
211
211
|
return new Blob([buf], {
|
|
212
|
-
type: ct
|
|
212
|
+
type: ct,
|
|
213
213
|
});
|
|
214
214
|
}
|
|
215
215
|
/**
|
|
@@ -271,11 +271,13 @@ async function consumeBody(data) {
|
|
|
271
271
|
}
|
|
272
272
|
catch (error) {
|
|
273
273
|
const err = error instanceof Error ? error : new Error(String(error));
|
|
274
|
-
const error_ = error instanceof FetchBaseError
|
|
274
|
+
const error_ = error instanceof FetchBaseError
|
|
275
|
+
? error
|
|
276
|
+
: new FetchError(`Invalid response body while trying to fetch ${data.url}: ${err.message}`, 'system', err);
|
|
275
277
|
throw error_;
|
|
276
278
|
}
|
|
277
279
|
try {
|
|
278
|
-
if (accum.every(c => typeof c === 'string')) {
|
|
280
|
+
if (accum.every((c) => typeof c === 'string')) {
|
|
279
281
|
return Buffer.from(accum.join(''));
|
|
280
282
|
}
|
|
281
283
|
return Buffer.concat(accum, accumBytes);
|
|
@@ -295,7 +297,7 @@ export const clone = (instance, highWaterMark) => {
|
|
|
295
297
|
if (instance.bodyUsed) {
|
|
296
298
|
throw new Error('cannot clone body after it is used');
|
|
297
299
|
}
|
|
298
|
-
if (
|
|
300
|
+
if (body instanceof Stream && typeof body.getBoundary !== 'function') {
|
|
299
301
|
p1 = new PassThrough({ highWaterMark });
|
|
300
302
|
p2 = new PassThrough({ highWaterMark });
|
|
301
303
|
body.pipe(p1);
|
|
@@ -385,7 +387,7 @@ function readableStreamToReadable(webStream) {
|
|
|
385
387
|
},
|
|
386
388
|
destroy(_err, callback) {
|
|
387
389
|
reader.cancel().then(() => callback(null), callback);
|
|
388
|
-
}
|
|
390
|
+
},
|
|
389
391
|
});
|
|
390
392
|
}
|
|
391
393
|
/**
|
package/lib/esm/request.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import"./_virtual/_rolldown/runtime.js";import{isAbortSignal as e}from"./utils/is.js";import t,{clone as n,extractContentType as r,getTotalBytes as i}from"./body.js";import a from"./headers.js";import{inputStreamToReadable as o,soupSendAsync as s}from"./utils/soup-helpers.js";import{DEFAULT_REFERRER_POLICY as c,determineRequestsReferrer as l,validateReferrerPolicy as u}from"./utils/referrer.js";import{URL as d}from"@gjsify/url";import f from"@girs/soup-3.0";import p from"@girs/glib-2.0";import m from"@girs/gio-2.0";const h=Symbol(`Request internals`)
|
|
1
|
+
import"./_virtual/_rolldown/runtime.js";import{isAbortSignal as e}from"./utils/is.js";import t,{clone as n,extractContentType as r,getTotalBytes as i}from"./body.js";import a from"./headers.js";import{inputStreamToReadable as o,soupSendAsync as s}from"./utils/soup-helpers.js";import{DEFAULT_REFERRER_POLICY as c,determineRequestsReferrer as l,validateReferrerPolicy as u}from"./utils/referrer.js";import{URL as d}from"@gjsify/url";import f from"@girs/soup-3.0";import p from"@girs/glib-2.0";import m from"@girs/gio-2.0";const h=Symbol(`Request internals`);let g=null;function getSharedSession(){if(g===null){g=new f.Session;try{g.remove_feature_by_type(f.ContentDecoder.$gtype)}catch{}}return g}const isRequest=e=>typeof e==`object`&&typeof e.url==`string`;var _=class Request extends t{cache;credentials;destination;get headers(){return this[h].headers}integrity;keepalive;get method(){return this[h].method}mode;get redirect(){return this[h].redirect}get referrer(){if(this[h].referrer===`no-referrer`)return``;if(this[h].referrer===`client`)return`about:client`;if(this[h].referrer)return this[h].referrer.toString()}get referrerPolicy(){return this[h].referrerPolicy}set referrerPolicy(e){this[h].referrerPolicy=u(e)}get signal(){return this[h].signal}get url(){return this[h].parsedURL.toString()}get _uri(){return p.Uri.parse(this.url,p.UriFlags.NONE)}get _session(){return this[h].session}get _message(){return this[h].message}get _inputStream(){return this[h].inputStream}get[Symbol.toStringTag](){return`Request`}[h];follow;compress=!1;counter=0;agent=``;highWaterMark=16384;insecureHTTPParser=!1;constructor(t,i){let o=t,s=i||{},c,l={};if(isRequest(t)?(c=new d(o.url),l=o):c=new d(t),c.username!==``||c.password!==``)throw TypeError(`${c} is an url with embedded credentials.`);let u=s.method||l.method||`GET`;if(/^(delete|get|head|options|post|put)$/i.test(u)&&(u=u.toUpperCase()),(i?.body!=null||isRequest(t)&&o.body!==null)&&(u===`GET`||u===`HEAD`))throw TypeError(`Request with GET/HEAD method cannot have body`);let m=i?.body?i.body:isRequest(t)&&o.body!==null?n(t):null;super(m,{size:s.size||0});let g=new a(i?.headers||o.headers||{});if(m!==null&&!g.has(`Content-Type`)){let e=r(m,this);e&&g.set(`Content-Type`,e)}let _=isRequest(t)?o.signal:null;if(i&&`signal`in i&&(_=i.signal),_!=null&&!e(_))throw TypeError(`Expected signal to be an instanceof AbortSignal or EventTarget`);let v=i?.referrer==null?o.referrer:i.referrer;if(v===``)v=`no-referrer`;else if(v){let e=new d(v);v=/^about:(\/\/)?client$/.test(e.toString())?`client`:e}else v=void 0;let y=c.protocol,b=null,x=null;(y===`http:`||y===`https:`)&&(b=getSharedSession(),x=new f.Message({method:u,uri:p.Uri.parse(c.toString(),p.UriFlags.NONE)})),this[h]={method:u,redirect:i?.redirect||o.redirect||`follow`,headers:g,parsedURL:c,signal:_,referrer:v,referrerPolicy:``,session:b,message:x},this.follow=s.follow===void 0?o.follow===void 0?20:o.follow:s.follow,this.compress=s.compress===void 0?o.compress===void 0?!0:o.compress:s.compress,this.counter=s.counter||o.counter||0,this.agent=s.agent||o.agent,this.highWaterMark=s.highWaterMark||o.highWaterMark||16384,this.insecureHTTPParser=s.insecureHTTPParser||o.insecureHTTPParser||!1,this.referrerPolicy=i?.referrerPolicy||o.referrerPolicy||``}async _send(e){let{session:t,message:n}=this[h];if(!t||!n)throw Error(`Cannot send request: no Soup session (non-HTTP URL?)`);e.headers._appendToSoupMessage(n);let r=this._rawBodyBuffer;if(r!==null&&r.byteLength>0){let t=e.headers.get(`content-type`)||null;n.set_request_body_from_bytes(t,new p.Bytes(r))}let i=new m.Cancellable;return this[h].inputStream=await s(t,n,p.PRIORITY_DEFAULT,i),this[h].readable=o(this[h].inputStream),{inputStream:this[h].inputStream,readable:this[h].readable,cancellable:i}}clone(){return new Request(this)}async arrayBuffer(){return super.arrayBuffer()}async blob(){return super.blob()}async formData(){return super.formData()}async json(){return super.json()}async text(){return super.text()}};Object.defineProperties(_.prototype,{method:{enumerable:!0},url:{enumerable:!0},headers:{enumerable:!0},redirect:{enumerable:!0},clone:{enumerable:!0},signal:{enumerable:!0},referrer:{enumerable:!0},referrerPolicy:{enumerable:!0}});const getSoupRequestOptions=e=>{let{parsedURL:t}=e[h],n=new a(e[h].headers);n.has(`Accept`)||n.set(`Accept`,`*/*`);let r=null;if(e.body===null&&/^(post|put)$/i.test(e.method)&&(r=`0`),e.body!==null){let t=i(e);typeof t==`number`&&!Number.isNaN(t)&&(r=String(t))}r&&n.set(`Content-Length`,r),e.referrerPolicy===``&&(e.referrerPolicy=c),e.referrer&&e.referrer!==`no-referrer`?e[h].referrer=l(e):e[h].referrer=`no-referrer`,e[h].referrer instanceof d&&n.set(`Referer`,e.referrer),n.has(`User-Agent`)||n.set(`User-Agent`,`gjsify-fetch`),e.compress&&!n.has(`Accept-Encoding`)&&n.set(`Accept-Encoding`,`gzip, deflate`);let{agent:o}=e;return typeof o==`function`&&(o=o(t)),!n.has(`Connection`)&&!o&&n.set(`Connection`,`close`),{parsedURL:t,options:{headers:n}}};export{_ as Request,_ as default,getSoupRequestOptions};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import"../_virtual/_rolldown/runtime.js";import{URL as e}from"@gjsify/url";import{isIP as t}from"node:net";function stripURLForUseAsAReferrer(t,n=!1){if(t==null||t===`no-referrer`)return`no-referrer`;let r=new e(t);return/^(about|blob|data):$/.test(r.protocol)?`no-referrer`:(r.username=``,r.password=``,r.hash=``,n&&(r.pathname=``,r.search=``),r)}const n=new Set([``,`no-referrer`,`no-referrer-when-downgrade`,`same-origin`,`origin`,`strict-origin`,`origin-when-cross-origin`,`strict-origin-when-cross-origin`,`unsafe-url`]),r=`strict-origin-when-cross-origin`;function validateReferrerPolicy(e){if(!n.has(e))throw TypeError(`Invalid referrerPolicy: ${e}`);return e}function isOriginPotentiallyTrustworthy(e){if(/^(http|ws)s:$/.test(e.protocol))return!0;let n=e.host.replace(/(^\[)|(]$)/g,``),r=t(n);return r===4
|
|
1
|
+
import"../_virtual/_rolldown/runtime.js";import{URL as e}from"@gjsify/url";import{isIP as t}from"node:net";function stripURLForUseAsAReferrer(t,n=!1){if(t==null||t===`no-referrer`)return`no-referrer`;let r=new e(t);return/^(about|blob|data):$/.test(r.protocol)?`no-referrer`:(r.username=``,r.password=``,r.hash=``,n&&(r.pathname=``,r.search=``),r)}const n=new Set([``,`no-referrer`,`no-referrer-when-downgrade`,`same-origin`,`origin`,`strict-origin`,`origin-when-cross-origin`,`strict-origin-when-cross-origin`,`unsafe-url`]),r=`strict-origin-when-cross-origin`;function validateReferrerPolicy(e){if(!n.has(e))throw TypeError(`Invalid referrerPolicy: ${e}`);return e}function isOriginPotentiallyTrustworthy(e){if(/^(http|ws)s:$/.test(e.protocol))return!0;let n=e.host.replace(/(^\[)|(]$)/g,``),r=t(n);return r===4&&n.startsWith(`127.`)||r===6&&/^(((0+:){7})|(::(0+:){0,6}))0*1$/.test(n)?!0:e.host===`localhost`||e.host.endsWith(`.localhost`)?!1:e.protocol===`file:`}function isUrlPotentiallyTrustworthy(t){return/^about:(blank|srcdoc)$/.test(t.toString())||(typeof t==`string`&&(t=new e(t)),t.protocol===`data:`)||/^(blob|filesystem):$/.test(t.protocol)?!0:isOriginPotentiallyTrustworthy(t)}function determineRequestsReferrer(t,n={}){let{referrerURLCallback:r,referrerOriginCallback:i}=n;if(t.referrer===`no-referrer`||t.referrerPolicy===``)return null;let a=t.referrerPolicy;if(t.referrer===`about:client`)return`no-referrer`;let o=new e(t.referrer),s=stripURLForUseAsAReferrer(o),c=stripURLForUseAsAReferrer(o,!0);s.toString().length>4096&&(s=c),r&&(s=r(s)),i&&(c=i(c));let l=new e(t.url);switch(a){case`no-referrer`:return`no-referrer`;case`origin`:return c;case`unsafe-url`:return s;case`strict-origin`:return isUrlPotentiallyTrustworthy(s)&&!isUrlPotentiallyTrustworthy(l)?`no-referrer`:c.toString();case`strict-origin-when-cross-origin`:return s.origin===l.origin?s:isUrlPotentiallyTrustworthy(s)&&!isUrlPotentiallyTrustworthy(l)?`no-referrer`:c;case`same-origin`:return s.origin===l.origin?s:`no-referrer`;case`origin-when-cross-origin`:return s.origin===l.origin?s:c;case`no-referrer-when-downgrade`:return isUrlPotentiallyTrustworthy(s)&&!isUrlPotentiallyTrustworthy(l)?`no-referrer`:s;default:throw TypeError(`Invalid referrerPolicy: ${a}`)}}function parseReferrerPolicyFromHeader(e){let t=(e.get(`referrer-policy`)||``).split(/[,\s]+/),r=``;for(let e of t)e&&n.has(e)&&(r=e);return r}export{r as DEFAULT_REFERRER_POLICY,n as ReferrerPolicy,determineRequestsReferrer,isOriginPotentiallyTrustworthy,isUrlPotentiallyTrustworthy,parseReferrerPolicyFromHeader,stripURLForUseAsAReferrer,validateReferrerPolicy};
|
package/lib/headers.js
CHANGED
|
@@ -54,8 +54,8 @@ export default class Headers {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
else {
|
|
57
|
-
throw new TypeError(
|
|
58
|
-
'
|
|
57
|
+
throw new TypeError("Failed to construct 'Headers': The provided value is not of type " +
|
|
58
|
+
"'(sequence<sequence<ByteString>> or record<ByteString, ByteString>)'");
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
61
|
append(name, value) {
|
package/lib/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { isRedirect } from './utils/is-redirect.js';
|
|
|
15
15
|
import { FormData } from '@gjsify/formdata';
|
|
16
16
|
import { isDomainOrSubdomain, isSameProtocol } from './utils/is.js';
|
|
17
17
|
import { parseReferrerPolicyFromHeader } from './utils/referrer.js';
|
|
18
|
-
import { Blob, File
|
|
18
|
+
import { Blob, File } from './utils/blob-from.js';
|
|
19
19
|
import { URL } from '@gjsify/url';
|
|
20
20
|
export { FormData, Headers, Request, Response, FetchError, AbortError, isRedirect };
|
|
21
21
|
export { Blob, File };
|
|
@@ -30,9 +30,7 @@ function rewriteRootRelativeUrl(input) {
|
|
|
30
30
|
const DEBUG = _g.__GJSIFY_DEBUG_FETCH === true;
|
|
31
31
|
try {
|
|
32
32
|
// GJS-only: derive program dir from System.programInvocationName.
|
|
33
|
-
const programPath = _g.imports?.system?.programPath
|
|
34
|
-
?? _g.imports?.system?.programInvocationName
|
|
35
|
-
?? '';
|
|
33
|
+
const programPath = _g.imports?.system?.programPath ?? _g.imports?.system?.programInvocationName ?? '';
|
|
36
34
|
if (!programPath)
|
|
37
35
|
return input;
|
|
38
36
|
const dir = GLib.path_get_dirname(programPath);
|
|
@@ -136,7 +134,7 @@ export default async function fetch(url, init = {}) {
|
|
|
136
134
|
readable.destroy(new AbortError('The operation was aborted.'));
|
|
137
135
|
});
|
|
138
136
|
// Handle stream errors
|
|
139
|
-
readable.on('error', (
|
|
137
|
+
readable.on('error', (_error) => {
|
|
140
138
|
finalize();
|
|
141
139
|
// Error is consumed by the body when read
|
|
142
140
|
});
|
|
@@ -183,7 +181,7 @@ export default async function fetch(url, init = {}) {
|
|
|
183
181
|
signal: request.signal,
|
|
184
182
|
size: request.size,
|
|
185
183
|
referrer: request.referrer,
|
|
186
|
-
referrerPolicy: request.referrerPolicy
|
|
184
|
+
referrerPolicy: request.referrerPolicy,
|
|
187
185
|
};
|
|
188
186
|
// Don't forward sensitive headers to different domains/protocols
|
|
189
187
|
if (!isDomainOrSubdomain(request.url, locationURL) || !isSameProtocol(request.url, locationURL)) {
|
|
@@ -222,7 +220,7 @@ export default async function fetch(url, init = {}) {
|
|
|
222
220
|
headers,
|
|
223
221
|
size: request.size,
|
|
224
222
|
counter: request.counter,
|
|
225
|
-
highWaterMark: request.highWaterMark
|
|
223
|
+
highWaterMark: request.highWaterMark,
|
|
226
224
|
};
|
|
227
225
|
// Handle content encoding (decompression)
|
|
228
226
|
const codings = headers.get('Content-Encoding');
|
|
@@ -232,7 +230,11 @@ export default async function fetch(url, init = {}) {
|
|
|
232
230
|
// 3. no Content-Encoding header
|
|
233
231
|
// 4. no content response (204)
|
|
234
232
|
// 5. content not modified response (304)
|
|
235
|
-
if (!request.compress ||
|
|
233
|
+
if (!request.compress ||
|
|
234
|
+
request.method === 'HEAD' ||
|
|
235
|
+
codings === null ||
|
|
236
|
+
statusCode === 204 ||
|
|
237
|
+
statusCode === 304) {
|
|
236
238
|
finalize();
|
|
237
239
|
return new Response(readable, responseOptions);
|
|
238
240
|
}
|
package/lib/index.spec.js
CHANGED
|
@@ -7,7 +7,7 @@ import { describe, it, expect, on } from '@gjsify/unit';
|
|
|
7
7
|
export default async () => {
|
|
8
8
|
await describe('fetch', async () => {
|
|
9
9
|
await it('fetch should be a function', async () => {
|
|
10
|
-
expect(typeof fetch).toBe(
|
|
10
|
+
expect(typeof fetch).toBe('function');
|
|
11
11
|
});
|
|
12
12
|
});
|
|
13
13
|
await describe('Headers', async () => {
|
|
@@ -37,7 +37,10 @@ export default async () => {
|
|
|
37
37
|
expect(h.has('x-remove')).toBe(false);
|
|
38
38
|
});
|
|
39
39
|
await it('should construct from array pairs', async () => {
|
|
40
|
-
const h = new Headers([
|
|
40
|
+
const h = new Headers([
|
|
41
|
+
['a', '1'],
|
|
42
|
+
['b', '2'],
|
|
43
|
+
]);
|
|
41
44
|
expect(h.get('a')).toBe('1');
|
|
42
45
|
expect(h.get('b')).toBe('2');
|
|
43
46
|
});
|
|
@@ -71,15 +74,17 @@ export default async () => {
|
|
|
71
74
|
});
|
|
72
75
|
await describe('Headers forEach', async () => {
|
|
73
76
|
await it('should iterate all headers with forEach', async () => {
|
|
74
|
-
const h = new Headers({
|
|
77
|
+
const h = new Headers({ a: '1', b: '2' });
|
|
75
78
|
const collected = [];
|
|
76
|
-
h.forEach((value, key) => {
|
|
79
|
+
h.forEach((value, key) => {
|
|
80
|
+
collected.push(`${key}:${value}`);
|
|
81
|
+
});
|
|
77
82
|
expect(collected.length).toBe(2);
|
|
78
83
|
expect(collected[0]).toBe('a:1');
|
|
79
84
|
expect(collected[1]).toBe('b:2');
|
|
80
85
|
});
|
|
81
86
|
await it('should return correct values() iterator', async () => {
|
|
82
|
-
const h = new Headers({
|
|
87
|
+
const h = new Headers({ x: 'hello', y: 'world' });
|
|
83
88
|
const values = [...h.values()];
|
|
84
89
|
expect(values.length).toBe(2);
|
|
85
90
|
});
|
|
@@ -93,7 +98,7 @@ export default async () => {
|
|
|
93
98
|
expect(entries[1][0]).toBe('x-custom');
|
|
94
99
|
});
|
|
95
100
|
await it('should support spread operator', async () => {
|
|
96
|
-
const h = new Headers({
|
|
101
|
+
const h = new Headers({ a: '1' });
|
|
97
102
|
const arr = [...h];
|
|
98
103
|
expect(arr.length).toBe(1);
|
|
99
104
|
expect(arr[0][0]).toBe('a');
|
|
@@ -108,7 +113,7 @@ export default async () => {
|
|
|
108
113
|
expect(() => h.append('invalid header', 'value')).toThrow();
|
|
109
114
|
});
|
|
110
115
|
await it('should support entries() for destructuring', async () => {
|
|
111
|
-
const h = new Headers({
|
|
116
|
+
const h = new Headers({ host: 'example.com', accept: '*/*' });
|
|
112
117
|
const obj = {};
|
|
113
118
|
for (const [k, v] of h.entries())
|
|
114
119
|
obj[k] = v;
|
|
@@ -128,7 +133,7 @@ export default async () => {
|
|
|
128
133
|
});
|
|
129
134
|
await it('should set custom headers', async () => {
|
|
130
135
|
const r = new Request('https://example.com', {
|
|
131
|
-
headers: { 'X-Custom': 'test' }
|
|
136
|
+
headers: { 'X-Custom': 'test' },
|
|
132
137
|
});
|
|
133
138
|
expect(r.headers.get('x-custom')).toBe('test');
|
|
134
139
|
});
|
|
@@ -178,7 +183,7 @@ export default async () => {
|
|
|
178
183
|
});
|
|
179
184
|
await it('should parse json body', async () => {
|
|
180
185
|
const r = new Response('{"key": "value"}');
|
|
181
|
-
const json = await r.json();
|
|
186
|
+
const json = (await r.json());
|
|
182
187
|
expect(json.key).toBe('value');
|
|
183
188
|
});
|
|
184
189
|
await it('should parse arrayBuffer body', async () => {
|
|
@@ -207,7 +212,7 @@ export default async () => {
|
|
|
207
212
|
expect(r.status).toBe(200);
|
|
208
213
|
const ct = r.headers.get('content-type') || '';
|
|
209
214
|
expect(ct.includes('application/json')).toBe(true);
|
|
210
|
-
const data = await r.json();
|
|
215
|
+
const data = (await r.json());
|
|
211
216
|
expect(data.message).toBe('ok');
|
|
212
217
|
});
|
|
213
218
|
await it('should clone a response', async () => {
|
|
@@ -265,22 +270,30 @@ export default async () => {
|
|
|
265
270
|
xhr.send();
|
|
266
271
|
});
|
|
267
272
|
await it('responseType="arraybuffer" yields ArrayBuffer', async () => {
|
|
268
|
-
const xhr = await runXhr((x) => {
|
|
273
|
+
const xhr = await runXhr((x) => {
|
|
274
|
+
x.responseType = 'arraybuffer';
|
|
275
|
+
});
|
|
269
276
|
expect(xhr.response instanceof ArrayBuffer).toBe(true);
|
|
270
277
|
expect(xhr.response.byteLength).toBe(5);
|
|
271
278
|
});
|
|
272
279
|
await it('responseType="text" yields decoded string', async () => {
|
|
273
|
-
const xhr = await runXhr((x) => {
|
|
280
|
+
const xhr = await runXhr((x) => {
|
|
281
|
+
x.responseType = 'text';
|
|
282
|
+
});
|
|
274
283
|
expect(xhr.response).toBe('hello');
|
|
275
284
|
expect(xhr.responseText).toBe('hello');
|
|
276
285
|
});
|
|
277
286
|
await it('default responseType "" yields text', async () => {
|
|
278
|
-
const xhr = await runXhr(() => {
|
|
287
|
+
const xhr = await runXhr(() => {
|
|
288
|
+
/* responseType left at "" */
|
|
289
|
+
});
|
|
279
290
|
expect(xhr.response).toBe('hello');
|
|
280
291
|
expect(xhr.responseText).toBe('hello');
|
|
281
292
|
});
|
|
282
293
|
await it('responseType="blob" attaches _tmpPath for URL.createObjectURL', async () => {
|
|
283
|
-
const xhr = await runXhr((x) => {
|
|
294
|
+
const xhr = await runXhr((x) => {
|
|
295
|
+
x.responseType = 'blob';
|
|
296
|
+
});
|
|
284
297
|
const blob = xhr.response;
|
|
285
298
|
expect(blob instanceof Blob).toBe(true);
|
|
286
299
|
expect(typeof blob._tmpPath).toBe('string');
|
package/lib/request.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ import GLib from '@girs/glib-2.0';
|
|
|
2
2
|
import Soup from '@girs/soup-3.0';
|
|
3
3
|
import Gio from '@girs/gio-2.0';
|
|
4
4
|
import { URL } from '@gjsify/url';
|
|
5
|
-
import { Blob } from './utils/blob-from.js';
|
|
6
|
-
import { Readable } from 'node:stream';
|
|
5
|
+
import type { Blob } from './utils/blob-from.js';
|
|
6
|
+
import type { Readable } from 'node:stream';
|
|
7
7
|
import Headers from './headers.js';
|
|
8
8
|
import Body from './body.js';
|
|
9
9
|
import type { FormData } from '@gjsify/formdata';
|
package/lib/request.js
CHANGED
|
@@ -12,12 +12,61 @@ import Body, { clone, extractContentType, getTotalBytes } from './body.js';
|
|
|
12
12
|
import { isAbortSignal } from './utils/is.js';
|
|
13
13
|
import { validateReferrerPolicy, determineRequestsReferrer, DEFAULT_REFERRER_POLICY } from './utils/referrer.js';
|
|
14
14
|
const INTERNALS = Symbol('Request internals');
|
|
15
|
+
/**
|
|
16
|
+
* Process-wide shared `Soup.Session`.
|
|
17
|
+
*
|
|
18
|
+
* Why a singleton and not a fresh `new Soup.Session()` per request:
|
|
19
|
+
*
|
|
20
|
+
* libsoup's connection manager keeps each open connection registered on a
|
|
21
|
+
* per-host list (`SoupHost.conns`). That list is only emptied when the
|
|
22
|
+
* connection's `disconnected` signal is processed on the main loop. When a
|
|
23
|
+
* `Soup.Session` is *finalized* its connection manager is torn down
|
|
24
|
+
* (`soup_connection_manager_free` → `soup_host_free`), which asserts the host
|
|
25
|
+
* has no live connections — `g_warn_if_fail (host->conns == NULL)`. If a
|
|
26
|
+
* session is dropped while a connection is still registered (the disconnect
|
|
27
|
+
* hasn't been pumped yet), libsoup prints:
|
|
28
|
+
*
|
|
29
|
+
* (gjs:…): libsoup-CRITICAL **: runtime check failed: (host->conns == NULL)
|
|
30
|
+
*
|
|
31
|
+
* Under GJS this is a SpiderMonkey-GC race: a per-request session (the old
|
|
32
|
+
* behavior — one `new Soup.Session()` per `Request`) becomes garbage as soon as
|
|
33
|
+
* the `Response` body is read, and a GC sweep can finalize it on the very same
|
|
34
|
+
* turn the connection is still being cleaned up. The same class of GLib/Soup
|
|
35
|
+
* BoxedInstance GC race is mitigated elsewhere in gjsify (e.g. `@gjsify/timers`
|
|
36
|
+
* uses `GLib.timeout_add` instead of holding `GLib.Source` boxed instances).
|
|
37
|
+
*
|
|
38
|
+
* Holding one long-lived session at module scope keeps it permanently reachable
|
|
39
|
+
* (never GC-finalized for the lifetime of the program), so the teardown path
|
|
40
|
+
* that fires the assertion is never reached mid-flight. It also lets libsoup
|
|
41
|
+
* pool and reuse keep-alive connections across requests — a real win for
|
|
42
|
+
* heavy fetch users like `@gjsify/npm-registry` during `gjsify install`.
|
|
43
|
+
*
|
|
44
|
+
* Lazily created so merely importing `@gjsify/fetch` (e.g. for `Headers`/
|
|
45
|
+
* `Request`/`Response` types on Node, or in a browser bundle) does not
|
|
46
|
+
* instantiate a Soup object.
|
|
47
|
+
*/
|
|
48
|
+
let sharedSession = null;
|
|
49
|
+
function getSharedSession() {
|
|
50
|
+
if (sharedSession === null) {
|
|
51
|
+
sharedSession = new Soup.Session();
|
|
52
|
+
// Soup auto-adds a ContentDecoder to new sessions, but it decodes the
|
|
53
|
+
// body without removing the Content-Encoding header, causing
|
|
54
|
+
// double-decompression when index.ts also runs DecompressionStream.
|
|
55
|
+
// Remove it once here so our JS-level decompression handles everything.
|
|
56
|
+
try {
|
|
57
|
+
sharedSession.remove_feature_by_type(Soup.ContentDecoder.$gtype);
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
/* not present */
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return sharedSession;
|
|
64
|
+
}
|
|
15
65
|
/**
|
|
16
66
|
* Check if `obj` is an instance of Request.
|
|
17
67
|
*/
|
|
18
68
|
const isRequest = (obj) => {
|
|
19
|
-
return
|
|
20
|
-
typeof obj.url === 'string');
|
|
69
|
+
return typeof obj === 'object' && typeof obj.url === 'string';
|
|
21
70
|
};
|
|
22
71
|
/** This Fetch API interface represents a resource request. */
|
|
23
72
|
export class Request extends Body {
|
|
@@ -124,9 +173,13 @@ export class Request extends Body {
|
|
|
124
173
|
(method === 'GET' || method === 'HEAD')) {
|
|
125
174
|
throw new TypeError('Request with GET/HEAD method cannot have body');
|
|
126
175
|
}
|
|
127
|
-
const inputBody = init?.body
|
|
176
|
+
const inputBody = init?.body
|
|
177
|
+
? init.body
|
|
178
|
+
: isRequest(input) && inputRL.body !== null
|
|
179
|
+
? clone(input)
|
|
180
|
+
: null;
|
|
128
181
|
super(inputBody, {
|
|
129
|
-
size: initRL.size || 0
|
|
182
|
+
size: initRL.size || 0,
|
|
130
183
|
});
|
|
131
184
|
const headers = new Headers((init?.headers || inputRL.headers || {}));
|
|
132
185
|
if (inputBody !== null && !headers.has('Content-Type')) {
|
|
@@ -135,9 +188,7 @@ export class Request extends Body {
|
|
|
135
188
|
headers.set('Content-Type', contentType);
|
|
136
189
|
}
|
|
137
190
|
}
|
|
138
|
-
let signal = isRequest(input) ?
|
|
139
|
-
inputRL.signal :
|
|
140
|
-
null;
|
|
191
|
+
let signal = isRequest(input) ? inputRL.signal : null;
|
|
141
192
|
if (init && 'signal' in init) {
|
|
142
193
|
signal = init.signal;
|
|
143
194
|
}
|
|
@@ -164,7 +215,11 @@ export class Request extends Body {
|
|
|
164
215
|
let session = null;
|
|
165
216
|
let message = null;
|
|
166
217
|
if (scheme === 'http:' || scheme === 'https:') {
|
|
167
|
-
session
|
|
218
|
+
// Reuse the process-wide shared session (see getSharedSession docs):
|
|
219
|
+
// a per-request session that gets GC-finalized while a connection is
|
|
220
|
+
// still registered on its host triggers libsoup's
|
|
221
|
+
// `host->conns == NULL` CRITICAL.
|
|
222
|
+
session = getSharedSession();
|
|
168
223
|
message = new Soup.Message({
|
|
169
224
|
method,
|
|
170
225
|
uri: GLib.Uri.parse(parsedURL.toString(), GLib.UriFlags.NONE),
|
|
@@ -182,8 +237,14 @@ export class Request extends Body {
|
|
|
182
237
|
message,
|
|
183
238
|
};
|
|
184
239
|
// Node-fetch-only options
|
|
185
|
-
this.follow =
|
|
186
|
-
|
|
240
|
+
this.follow =
|
|
241
|
+
initRL.follow === undefined ? (inputRL.follow === undefined ? 20 : inputRL.follow) : initRL.follow;
|
|
242
|
+
this.compress =
|
|
243
|
+
initRL.compress === undefined
|
|
244
|
+
? inputRL.compress === undefined
|
|
245
|
+
? true
|
|
246
|
+
: inputRL.compress
|
|
247
|
+
: initRL.compress;
|
|
187
248
|
this.counter = initRL.counter || inputRL.counter || 0;
|
|
188
249
|
this.agent = initRL.agent || inputRL.agent;
|
|
189
250
|
this.highWaterMark = initRL.highWaterMark || inputRL.highWaterMark || 16384;
|
|
@@ -200,14 +261,9 @@ export class Request extends Body {
|
|
|
200
261
|
if (!session || !message) {
|
|
201
262
|
throw new Error('Cannot send request: no Soup session (non-HTTP URL?)');
|
|
202
263
|
}
|
|
203
|
-
//
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
// decompression in index.ts handles everything correctly.
|
|
207
|
-
try {
|
|
208
|
-
session.remove_feature_by_type(Soup.ContentDecoder.$gtype);
|
|
209
|
-
}
|
|
210
|
-
catch { /* not present */ }
|
|
264
|
+
// ContentDecoder is removed once on the shared session in
|
|
265
|
+
// getSharedSession() (so the Content-Encoding header survives for our
|
|
266
|
+
// JS-level DecompressionStream in index.ts). Nothing per-request here.
|
|
211
267
|
options.headers._appendToSoupMessage(message);
|
|
212
268
|
// Attach the request body to the Soup message (needed for POST/PUT/PATCH).
|
|
213
269
|
// Use _rawBodyBuffer to read the body without consuming the stream (the
|
|
@@ -224,7 +280,7 @@ export class Request extends Body {
|
|
|
224
280
|
return {
|
|
225
281
|
inputStream: this[INTERNALS].inputStream,
|
|
226
282
|
readable: this[INTERNALS].readable,
|
|
227
|
-
cancellable
|
|
283
|
+
cancellable,
|
|
228
284
|
};
|
|
229
285
|
}
|
|
230
286
|
/**
|
|
@@ -257,7 +313,7 @@ Object.defineProperties(Request.prototype, {
|
|
|
257
313
|
clone: { enumerable: true },
|
|
258
314
|
signal: { enumerable: true },
|
|
259
315
|
referrer: { enumerable: true },
|
|
260
|
-
referrerPolicy: { enumerable: true }
|
|
316
|
+
referrerPolicy: { enumerable: true },
|
|
261
317
|
});
|
|
262
318
|
export default Request;
|
|
263
319
|
/**
|
|
@@ -322,6 +378,6 @@ export const getSoupRequestOptions = (request) => {
|
|
|
322
378
|
};
|
|
323
379
|
return {
|
|
324
380
|
parsedURL,
|
|
325
|
-
options
|
|
381
|
+
options,
|
|
326
382
|
};
|
|
327
383
|
};
|
package/lib/response.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import Gio from '@girs/gio-2.0';
|
|
2
2
|
import Headers from './headers.js';
|
|
3
3
|
import Body from './body.js';
|
|
4
|
-
import { Blob } from './utils/blob-from.js';
|
|
4
|
+
import type { Blob } from './utils/blob-from.js';
|
|
5
5
|
import type { Readable } from 'node:stream';
|
|
6
6
|
declare const INTERNALS: unique symbol;
|
|
7
7
|
interface ResponseOptions {
|
package/lib/response.js
CHANGED
|
@@ -38,7 +38,7 @@ export class Response extends Body {
|
|
|
38
38
|
statusText: options.statusText || '',
|
|
39
39
|
headers,
|
|
40
40
|
counter: options.counter,
|
|
41
|
-
highWaterMark: options.highWaterMark
|
|
41
|
+
highWaterMark: options.highWaterMark,
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
get type() {
|
|
@@ -83,7 +83,7 @@ export class Response extends Body {
|
|
|
83
83
|
ok: this.ok,
|
|
84
84
|
redirected: this.redirected,
|
|
85
85
|
size: this.size,
|
|
86
|
-
highWaterMark: this.highWaterMark
|
|
86
|
+
highWaterMark: this.highWaterMark,
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
/**
|
|
@@ -97,9 +97,9 @@ export class Response extends Body {
|
|
|
97
97
|
}
|
|
98
98
|
return new Response(null, {
|
|
99
99
|
headers: {
|
|
100
|
-
location: new URL(url).toString()
|
|
100
|
+
location: new URL(url).toString(),
|
|
101
101
|
},
|
|
102
|
-
status
|
|
102
|
+
status,
|
|
103
103
|
});
|
|
104
104
|
}
|
|
105
105
|
static error() {
|
|
@@ -153,6 +153,6 @@ Object.defineProperties(Response.prototype, {
|
|
|
153
153
|
redirected: { enumerable: true },
|
|
154
154
|
statusText: { enumerable: true },
|
|
155
155
|
headers: { enumerable: true },
|
|
156
|
-
clone: { enumerable: true }
|
|
156
|
+
clone: { enumerable: true },
|
|
157
157
|
});
|
|
158
158
|
export default Response;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Soup.Session lifecycle for GJS for @gjsify/fetch — original regression test.
|
|
2
|
+
//
|
|
3
|
+
// Regression: @gjsify/fetch used to create a fresh `new Soup.Session()` inside
|
|
4
|
+
// every Request constructor. Once the Response body was read, that per-request
|
|
5
|
+
// session became unreachable and SpiderMonkey could finalize it on the next GC
|
|
6
|
+
// sweep while a libsoup connection was still registered on its host. libsoup's
|
|
7
|
+
// connection-manager teardown then asserted `host->conns == NULL` and printed:
|
|
8
|
+
//
|
|
9
|
+
// (gjs:…): libsoup-CRITICAL **: runtime check failed: (host->conns == NULL)
|
|
10
|
+
//
|
|
11
|
+
// on stderr during heavy fetch use (notably @gjsify/npm-registry's packument/
|
|
12
|
+
// tarball fetches inside `gjsify install`). The fix holds ONE process-wide
|
|
13
|
+
// `Soup.Session` at module scope so it is never GC-finalized mid-flight.
|
|
14
|
+
//
|
|
15
|
+
// This file is GJS-only (`.gjs.spec.ts`). The suite body runs under
|
|
16
|
+
// `on('Gjs', …)`, so it is a no-op on Node. To keep the Node bundle free of
|
|
17
|
+
// `gi://*` / `system` imports (the same `test.mts` aggregator drives
|
|
18
|
+
// `test:node`), all GJS runtime objects are read from `globalThis.imports`
|
|
19
|
+
// (the GJS bootstrap) rather than statically imported — type-only imports give
|
|
20
|
+
// us the real shapes at compile time and are stripped at runtime.
|
|
21
|
+
import { describe, it, expect, on } from '@gjsify/unit';
|
|
22
|
+
export default async () => {
|
|
23
|
+
await on('Gjs', async () => {
|
|
24
|
+
// Read GJS runtime objects lazily (no static gi:// import → Node bundle
|
|
25
|
+
// stays clean). `imports` is the GJS bootstrap global.
|
|
26
|
+
const gjs = globalThis.imports;
|
|
27
|
+
const Soup = gjs.gi.Soup;
|
|
28
|
+
const GLib = gjs.gi.GLib;
|
|
29
|
+
const Gio = gjs.gi.Gio;
|
|
30
|
+
const System = gjs.system;
|
|
31
|
+
// Request + fetch are installed as globals by `@gjsify/fetch/register`,
|
|
32
|
+
// pulled into the test bundle by test.mts.
|
|
33
|
+
const RequestCtor = globalThis.Request;
|
|
34
|
+
const fetchFn = globalThis.fetch;
|
|
35
|
+
await describe('@gjsify/fetch — Soup.Session lifecycle (host->conns regression)', async () => {
|
|
36
|
+
await it('reuses one shared Soup.Session across HTTP requests (no per-request session)', () => {
|
|
37
|
+
// Two distinct HTTP Requests must share the very same Soup.Session
|
|
38
|
+
// instance. A per-request session is what triggered the GC race;
|
|
39
|
+
// a shared singleton can never be finalized while in use.
|
|
40
|
+
const a = new RequestCtor('http://example.com/a');
|
|
41
|
+
const b = new RequestCtor('https://example.org/b');
|
|
42
|
+
expect(a._session).toBeTruthy();
|
|
43
|
+
expect(b._session).toBeTruthy();
|
|
44
|
+
expect(a._session === b._session).toBe(true);
|
|
45
|
+
expect(a._session instanceof Soup.Session).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
await it('non-HTTP requests do not allocate a Soup.Session', () => {
|
|
48
|
+
const dataReq = new RequestCtor('data:text/plain,hi');
|
|
49
|
+
expect(dataReq._session).toBeNull();
|
|
50
|
+
});
|
|
51
|
+
await it('many fetches + GC against a local server emit no host->conns / libsoup-CRITICAL warning', async () => {
|
|
52
|
+
// Exercise the live fetch path in-process first (the exact
|
|
53
|
+
// lifecycle: fetch → read body → drop refs → GC).
|
|
54
|
+
const server = new Soup.Server({});
|
|
55
|
+
server.add_handler(null, (_srv, msg) => {
|
|
56
|
+
msg.set_status(200, null);
|
|
57
|
+
msg.set_response('application/json', Soup.MemoryUse.COPY, new TextEncoder().encode('{"ok":true}'));
|
|
58
|
+
});
|
|
59
|
+
server.listen_local(0, Soup.ServerListenOptions.IPV4_ONLY);
|
|
60
|
+
const base = server.get_uris()[0].to_string();
|
|
61
|
+
for (let i = 0; i < 12; i++) {
|
|
62
|
+
const res = await fetchFn(base);
|
|
63
|
+
await res.json();
|
|
64
|
+
System.gc();
|
|
65
|
+
}
|
|
66
|
+
System.gc();
|
|
67
|
+
System.gc();
|
|
68
|
+
// Stronger assertion: run the same loop in a child gjs process and
|
|
69
|
+
// capture its stderr, so we can assert the libsoup warning is
|
|
70
|
+
// absent (GJS cannot redirect its own fd 2). The worker imports
|
|
71
|
+
// this package's built bundle by absolute path. If the bundle is
|
|
72
|
+
// not built (running specs from src only), skip the subprocess —
|
|
73
|
+
// the shared-session invariant above is the load-bearing check.
|
|
74
|
+
const pkgRoot = GLib.path_get_dirname(GLib.path_get_dirname(GLib.filename_from_uri(import.meta.url)[0]));
|
|
75
|
+
const fetchEntry = `${pkgRoot}/lib/esm/index.js`;
|
|
76
|
+
if (!GLib.file_test(fetchEntry, GLib.FileTest.EXISTS)) {
|
|
77
|
+
expect(true).toBe(true);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const worker = `
|
|
81
|
+
imports.gi.versions.Soup = '3.0';
|
|
82
|
+
const { Soup, GLib } = imports.gi;
|
|
83
|
+
const System = imports.system;
|
|
84
|
+
const { default: fetch } = await import(${JSON.stringify('file://' + fetchEntry)});
|
|
85
|
+
const server = new Soup.Server({});
|
|
86
|
+
server.add_handler(null, (_s, msg) => {
|
|
87
|
+
msg.set_status(200, null);
|
|
88
|
+
msg.set_response('application/json', Soup.MemoryUse.COPY,
|
|
89
|
+
new TextEncoder().encode('{"ok":true}'));
|
|
90
|
+
});
|
|
91
|
+
server.listen_local(0, Soup.ServerListenOptions.IPV4_ONLY);
|
|
92
|
+
const base = server.get_uris()[0].to_string();
|
|
93
|
+
for (let i = 0; i < 20; i++) {
|
|
94
|
+
const res = await fetch(base);
|
|
95
|
+
await res.json();
|
|
96
|
+
System.gc();
|
|
97
|
+
}
|
|
98
|
+
System.gc(); System.gc();
|
|
99
|
+
`;
|
|
100
|
+
const proc = Gio.Subprocess.new(['gjs', '-m', '-c', worker], Gio.SubprocessFlags.STDERR_PIPE | Gio.SubprocessFlags.STDOUT_SILENCE);
|
|
101
|
+
const stderrBytes = await new Promise((resolve, reject) => {
|
|
102
|
+
proc.communicate_async(null, null, (p, r) => {
|
|
103
|
+
try {
|
|
104
|
+
const [, , stderr] = p.communicate_finish(r);
|
|
105
|
+
resolve(stderr ? stderr.toArray() : new Uint8Array());
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
const stderr = new TextDecoder().decode(stderrBytes);
|
|
113
|
+
expect(stderr.includes('host->conns')).toBe(false);
|
|
114
|
+
expect(stderr.includes('libsoup-CRITICAL')).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
};
|
package/lib/utils/blob-from.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { Blob, File } from 'node:buffer';
|
|
2
|
-
export { Blob, File
|
|
2
|
+
export { Blob, File };
|
package/lib/utils/blob-from.js
CHANGED
package/lib/utils/is-redirect.js
CHANGED
package/lib/utils/is.d.ts
CHANGED
package/lib/utils/is.js
CHANGED
|
@@ -11,7 +11,7 @@ const NAME = Symbol.toStringTag;
|
|
|
11
11
|
* @param {*} object - Object to check for
|
|
12
12
|
* @return {boolean}
|
|
13
13
|
*/
|
|
14
|
-
export const isURLSearchParameters = object => {
|
|
14
|
+
export const isURLSearchParameters = (object) => {
|
|
15
15
|
return (typeof object === 'object' &&
|
|
16
16
|
typeof object.append === 'function' &&
|
|
17
17
|
typeof object.delete === 'function' &&
|
|
@@ -44,8 +44,7 @@ export const isAbortSignal = (object) => {
|
|
|
44
44
|
if (typeof object !== 'object' || object === null)
|
|
45
45
|
return false;
|
|
46
46
|
const obj = object;
|
|
47
|
-
return
|
|
48
|
-
obj[NAME] === 'EventTarget');
|
|
47
|
+
return obj[NAME] === 'AbortSignal' || obj[NAME] === 'EventTarget';
|
|
49
48
|
};
|
|
50
49
|
/**
|
|
51
50
|
* isDomainOrSubdomain reports whether sub is a subdomain (or exact match) of
|
|
@@ -29,7 +29,7 @@ var S;
|
|
|
29
29
|
let f = 1;
|
|
30
30
|
const F = {
|
|
31
31
|
PART_BOUNDARY: f,
|
|
32
|
-
LAST_BOUNDARY: f *= 2
|
|
32
|
+
LAST_BOUNDARY: (f *= 2),
|
|
33
33
|
};
|
|
34
34
|
const LF = 10;
|
|
35
35
|
const CR = 13;
|
|
@@ -38,7 +38,7 @@ const HYPHEN = 45;
|
|
|
38
38
|
const COLON = 58;
|
|
39
39
|
const A = 97;
|
|
40
40
|
const Z = 122;
|
|
41
|
-
const lower = c => c | 0x20;
|
|
41
|
+
const lower = (c) => c | 0x20;
|
|
42
42
|
const noop = (..._args) => { };
|
|
43
43
|
class MultipartParser {
|
|
44
44
|
index = 0;
|
package/lib/utils/referrer.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { URL } from '@gjsify/url';
|
|
2
|
-
import Request from '../request.js';
|
|
2
|
+
import type Request from '../request.js';
|
|
3
3
|
/**
|
|
4
4
|
* @external URL
|
|
5
5
|
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URL URL}
|
|
@@ -13,7 +13,7 @@ import Request from '../request.js';
|
|
|
13
13
|
* @param url
|
|
14
14
|
* @param originOnly
|
|
15
15
|
*/
|
|
16
|
-
export declare function stripURLForUseAsAReferrer(url: null | URL |
|
|
16
|
+
export declare function stripURLForUseAsAReferrer(url: null | URL | 'no-referrer', originOnly?: boolean): URL | "no-referrer";
|
|
17
17
|
/**
|
|
18
18
|
* @see {@link https://w3c.github.io/webappsec-referrer-policy/#enumdef-referrerpolicy enum ReferrerPolicy}
|
|
19
19
|
*/
|
package/lib/utils/referrer.js
CHANGED
|
@@ -15,7 +15,8 @@ import { isIP } from 'node:net';
|
|
|
15
15
|
*/
|
|
16
16
|
export function stripURLForUseAsAReferrer(url, originOnly = false) {
|
|
17
17
|
// 1. If url is null, return no referrer.
|
|
18
|
-
if (url == null || url ===
|
|
18
|
+
if (url == null || url === 'no-referrer') {
|
|
19
|
+
// eslint-disable-line no-eq-null, eqeqeq
|
|
19
20
|
return 'no-referrer';
|
|
20
21
|
}
|
|
21
22
|
const u = new URL(url);
|
|
@@ -55,7 +56,7 @@ export const ReferrerPolicy = new Set([
|
|
|
55
56
|
'strict-origin',
|
|
56
57
|
'origin-when-cross-origin',
|
|
57
58
|
'strict-origin-when-cross-origin',
|
|
58
|
-
'unsafe-url'
|
|
59
|
+
'unsafe-url',
|
|
59
60
|
]);
|
|
60
61
|
/**
|
|
61
62
|
* @see {@link https://w3c.github.io/webappsec-referrer-policy/#default-referrer-policy default referrer policy}
|
|
@@ -89,7 +90,7 @@ export function isOriginPotentiallyTrustworthy(url) {
|
|
|
89
90
|
// 4. If origin's host component matches one of the CIDR notations 127.0.0.0/8 or ::1/128 [RFC4632], return "Potentially Trustworthy".
|
|
90
91
|
const hostIp = url.host.replace(/(^\[)|(]$)/g, '');
|
|
91
92
|
const hostIPVersion = isIP(hostIp);
|
|
92
|
-
if (hostIPVersion === 4 &&
|
|
93
|
+
if (hostIPVersion === 4 && hostIp.startsWith('127.')) {
|
|
93
94
|
return true;
|
|
94
95
|
}
|
|
95
96
|
if (hostIPVersion === 6 && /^(((0+:){7})|(::(0+:){0,6}))0*1$/.test(hostIp)) {
|
package/lib/xhr.js
CHANGED
|
@@ -43,10 +43,7 @@ function guessBlobExt(url) {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
function writeBlobToTempFile(bytes, url) {
|
|
46
|
-
const tmpPath = GLib.build_filenamev([
|
|
47
|
-
GLib.get_tmp_dir(),
|
|
48
|
-
`gjsify-blob-${_blobCounter++}${guessBlobExt(url)}`,
|
|
49
|
-
]);
|
|
46
|
+
const tmpPath = GLib.build_filenamev([GLib.get_tmp_dir(), `gjsify-blob-${_blobCounter++}${guessBlobExt(url)}`]);
|
|
50
47
|
GLib.file_set_contents(tmpPath, bytes);
|
|
51
48
|
return tmpPath;
|
|
52
49
|
}
|
|
@@ -119,7 +116,9 @@ export class XMLHttpRequest extends EventTarget {
|
|
|
119
116
|
if (this._aborted)
|
|
120
117
|
return;
|
|
121
118
|
const headersInit = {};
|
|
122
|
-
this._headers.forEach((v, k) => {
|
|
119
|
+
this._headers.forEach((v, k) => {
|
|
120
|
+
headersInit[k] = v;
|
|
121
|
+
});
|
|
123
122
|
const fetchOptions = {
|
|
124
123
|
method: this._method,
|
|
125
124
|
headers: headersInit,
|
|
@@ -198,7 +197,7 @@ export class XMLHttpRequest extends EventTarget {
|
|
|
198
197
|
const text = await res.text();
|
|
199
198
|
// Strip UTF-8 BOM (U+FEFF) — browsers do this automatically; required
|
|
200
199
|
// for JSON.parse to succeed on BOM-prefixed JSON responses.
|
|
201
|
-
const stripped = text.charCodeAt(0) ===
|
|
200
|
+
const stripped = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
202
201
|
this.responseText = stripped;
|
|
203
202
|
this.response = stripped;
|
|
204
203
|
break;
|
|
@@ -248,7 +247,9 @@ export class XMLHttpRequest extends EventTarget {
|
|
|
248
247
|
if (this.onloadend)
|
|
249
248
|
this.onloadend(new ProgressEvent('loadend'));
|
|
250
249
|
}
|
|
251
|
-
overrideMimeType(_mime) {
|
|
250
|
+
overrideMimeType(_mime) {
|
|
251
|
+
/* no-op */
|
|
252
|
+
}
|
|
252
253
|
_onTimeout() {
|
|
253
254
|
if (this._aborted)
|
|
254
255
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gjsify/fetch",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.30",
|
|
4
4
|
"description": "Web and Node.js fetch module for Gjs",
|
|
5
5
|
"module": "lib/esm/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"fetch"
|
|
49
49
|
],
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@gjsify/cli": "^0.4.
|
|
52
|
-
"@gjsify/unit": "^0.4.
|
|
51
|
+
"@gjsify/cli": "^0.4.30",
|
|
52
|
+
"@gjsify/unit": "^0.4.30",
|
|
53
53
|
"@types/node": "^25.9.1",
|
|
54
54
|
"typescript": "^6.0.3"
|
|
55
55
|
},
|
|
@@ -58,9 +58,9 @@
|
|
|
58
58
|
"@girs/gjs": "4.0.1",
|
|
59
59
|
"@girs/glib-2.0": "2.88.0-4.0.1",
|
|
60
60
|
"@girs/soup-3.0": "3.6.6-4.0.1",
|
|
61
|
-
"@gjsify/formdata": "^0.4.
|
|
62
|
-
"@gjsify/http": "^0.4.
|
|
63
|
-
"@gjsify/url": "^0.4.
|
|
64
|
-
"@gjsify/utils": "^0.4.
|
|
61
|
+
"@gjsify/formdata": "^0.4.30",
|
|
62
|
+
"@gjsify/http": "^0.4.30",
|
|
63
|
+
"@gjsify/url": "^0.4.30",
|
|
64
|
+
"@gjsify/utils": "^0.4.30"
|
|
65
65
|
}
|
|
66
66
|
}
|