@gjsify/fetch 0.16.9 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/esm/index.js CHANGED
@@ -1 +1 @@
1
- import"./_virtual/_rolldown/runtime.js";import{Blob as e,File as t}from"./utils/blob-from.js";import{FetchError as n}from"./errors/fetch-error.js";import{isDomainOrSubdomain as r,isSameProtocol as i}from"./utils/is.js";import{clone as a}from"./body.js";import o from"./headers.js";import{parseDataUri as s}from"./utils/data-uri.js";import{isRedirect as c}from"./utils/is-redirect.js";import{Response as l}from"./response.js";import{parseReferrerPolicyFromHeader as u}from"./utils/referrer.js";import{Request as d,getSoupRequestOptions as f}from"./request.js";import{AbortError as p}from"./errors/abort-error.js";import{XMLHttpRequest as m,XMLHttpRequestUpload as h}from"./xhr.js";import{URL as g}from"@gjsify/url";import _ from"node:stream";import{FormData as v}from"@gjsify/formdata";import y from"@girs/glib-2.0";const b=new Set([`data:`,`http:`,`https:`,`file:`]);function rewriteRootRelativeUrl(e){if(typeof e!=`string`||!e.startsWith(`/`)||e.startsWith(`//`))return e;let t=globalThis,n=t.__GJSIFY_DEBUG_FETCH===!0;try{let r=t.imports?.system?.programPath??t.imports?.system?.programInvocationName??``;if(!r)return e;let i=`file://${y.path_get_dirname(r)}${e}`;return n&&console.log(`[fetch] rewrite ${e} → ${i}`),i}catch(t){return n&&console.warn(`[fetch] rewrite FAILED: ${t instanceof Error?t.message:String(t)}`),e}}async function fetch(t,m={}){t=rewriteRootRelativeUrl(t);let h=new d(t,m),{parsedURL:v,options:x}=f(h);if(!b.has(v.protocol))throw TypeError(`@gjsify/fetch cannot load ${t}. URL scheme "${v.protocol.replace(/:$/,``)}" is not supported.`);if(v.protocol===`data:`){let{buffer:e,typeFull:t}=s(h.url);return new l(Buffer.from(e),{headers:{"Content-Type":t}})}if(v.protocol===`file:`){let e=globalThis.__GJSIFY_DEBUG_FETCH===!0;e&&console.log(`[fetch] file:// ${h.url}`);try{let t=y.filename_from_uri(h.url)[0];e&&console.log(`[fetch] file:// path=${t}`);let[r,i]=y.file_get_contents(t);if(e&&console.log(`[fetch] file:// ok=${r} bytes=${i?.byteLength??`?`}`),!r)throw new n(`Failed to read file: ${t}`,`system`);let a=i,o=new Uint8Array(a.byteLength);o.set(a);let s=new l(o);return e&&console.log(`[fetch] file:// response created`),s}catch(t){let r=t instanceof Error?t:Error(String(t));throw e&&console.warn(`[fetch] file:// FAIL: ${r.message}`),new n(`request to ${h.url} failed, reason: ${r.message}`,`system`,r)}}let{signal:S}=h;if(S&&S.aborted)throw new p(`The operation was aborted.`);let C,w;try{let e=await h._send(x);C=e.readable,w=e.cancellable}catch(e){let t=e instanceof Error?e:Error(String(e));throw new n(`request to ${h.url} failed, reason: ${t.message}`,`system`,t)}let abortHandler=()=>{w.cancel()};S&&S.addEventListener(`abort`,abortHandler,{once:!0});let finalize=()=>{S&&S.removeEventListener(`abort`,abortHandler)};w.connect(()=>{C.destroy(new p(`The operation was aborted.`))}),C.on(`error`,e=>{finalize()});let T=h._message,E=o._newFromSoupMessage(T),D=T.status_code,O=T.get_reason_phrase();if(c(D)){let e=E.get(`Location`),t=null;try{t=e===null?null:new g(e,h.url)}catch{if(h.redirect!==`manual`)throw finalize(),new n(`uri requested responds with an invalid redirect URL: ${e}`,`invalid-redirect`)}switch(h.redirect){case`error`:throw finalize(),new n(`uri requested responds with a redirect, redirect mode is set to error: ${h.url}`,`no-redirect`);case`manual`:break;case`follow`:{if(t===null)break;if(h.counter>=h.follow)throw finalize(),new n(`maximum redirect reached at: ${h.url}`,`max-redirect`);let e={headers:new o(h.headers),follow:h.follow,counter:h.counter+1,agent:h.agent,compress:h.compress,method:h.method,body:a(h),signal:h.signal,size:h.size,referrer:h.referrer,referrerPolicy:h.referrerPolicy};if(!r(h.url,t)||!i(h.url,t))for(let t of[`authorization`,`www-authenticate`,`cookie`,`cookie2`])e.headers.delete(t);if(D!==303&&h.body&&m.body instanceof _.Readable)throw finalize(),new n(`Cannot follow redirect with body being a readable stream`,`unsupported-redirect`);(D===303||(D===301||D===302)&&h.method===`POST`)&&(e.method=`GET`,e.body=void 0,e.headers.delete(`content-length`));let s=u(E);return s&&(e.referrerPolicy=s),finalize(),fetch(new d(t,e))}default:throw TypeError(`Redirect option '${h.redirect}' is not a valid value of RequestRedirect`)}}let k={url:h.url,status:D,statusText:O,headers:E,size:h.size,counter:h.counter,highWaterMark:h.highWaterMark},A=E.get(`Content-Encoding`);if(!h.compress||h.method===`HEAD`||A===null||D===204||D===304)return finalize(),new l(C,k);if(typeof DecompressionStream<`u`){let t=null;if(A===`gzip`||A===`x-gzip`?t=`gzip`:(A===`deflate`||A===`x-deflate`)&&(t=`deflate`),t){let n=new e([await new l(C,k).arrayBuffer()]).stream().pipeThrough(new DecompressionStream(t));return finalize(),new l(n,k)}}return finalize(),new l(C,k)}export{p as AbortError,e as Blob,n as FetchError,t as File,v as FormData,o as Headers,d as Request,l as Response,m as XMLHttpRequest,h as XMLHttpRequestUpload,fetch as default,c as isRedirect};
1
+ import"./_virtual/_rolldown/runtime.js";import{Blob as e,File as t}from"./utils/blob-from.js";import{FetchError as n}from"./errors/fetch-error.js";import{isDomainOrSubdomain as r,isSameProtocol as i}from"./utils/is.js";import{clone as a}from"./body.js";import o from"./headers.js";import{parseDataUri as s}from"./utils/data-uri.js";import{isRedirect as c}from"./utils/is-redirect.js";import{Response as l}from"./response.js";import{parseReferrerPolicyFromHeader as u}from"./utils/referrer.js";import{Request as d,getSoupRequestOptions as f}from"./request.js";import{AbortError as p}from"./errors/abort-error.js";import{XMLHttpRequest as m,XMLHttpRequestUpload as h}from"./xhr.js";import{URL as g}from"@gjsify/url";import _ from"node:stream";import{FormData as v}from"@gjsify/formdata";import y from"@girs/glib-2.0";const b=new Set([`data:`,`http:`,`https:`,`file:`]);function rewriteRootRelativeUrl(e){if(typeof e!=`string`||!e.startsWith(`/`)||e.startsWith(`//`))return e;let t=globalThis,n=t.__GJSIFY_DEBUG_FETCH===!0;try{let r=t.imports?.system?.programPath??t.imports?.system?.programInvocationName??``;if(!r)return e;let i=`file://${y.path_get_dirname(r)}${e}`;return n&&console.log(`[fetch] rewrite ${e} → ${i}`),i}catch(t){return n&&console.warn(`[fetch] rewrite FAILED: ${t instanceof Error?t.message:String(t)}`),e}}async function fetch(t,m={}){t=rewriteRootRelativeUrl(t);let h=new d(t,m),{parsedURL:v,options:x}=f(h);if(!b.has(v.protocol))throw TypeError(`@gjsify/fetch cannot load ${t}. URL scheme "${v.protocol.replace(/:$/,``)}" is not supported.`);if(v.protocol===`data:`){let{buffer:e,typeFull:t}=s(h.url);return new l(Buffer.from(e),{headers:{"Content-Type":t}})}if(v.protocol===`file:`){let e=globalThis.__GJSIFY_DEBUG_FETCH===!0;e&&console.log(`[fetch] file:// ${h.url}`);try{let t=y.filename_from_uri(h.url)[0];e&&console.log(`[fetch] file:// path=${t}`);let[r,i]=y.file_get_contents(t);if(e&&console.log(`[fetch] file:// ok=${r} bytes=${i?.byteLength??`?`}`),!r)throw new n(`Failed to read file: ${t}`,`system`);let a=i,o=new Uint8Array(a.byteLength);o.set(a);let s=new l(o);return e&&console.log(`[fetch] file:// response created`),s}catch(t){let r=t instanceof Error?t:Error(String(t));throw e&&console.warn(`[fetch] file:// FAIL: ${r.message}`),new n(`request to ${h.url} failed, reason: ${r.message}`,`system`,r)}}let{signal:S}=h;if(S&&S.aborted)throw new p(`The operation was aborted.`);let C,w;try{let e=await h._send(x);C=e.readable,w=e.cancellable}catch(e){if(S&&S.aborted)throw new p(`The operation was aborted.`);let t=e instanceof Error?e:Error(String(e));throw new n(`request to ${h.url} failed, reason: ${t.message}`,`system`,t)}let abortHandler=()=>{w.cancel()};S&&S.addEventListener(`abort`,abortHandler,{once:!0});let finalize=()=>{S&&S.removeEventListener(`abort`,abortHandler)};w.connect(()=>{C.destroy(new p(`The operation was aborted.`))}),C.on(`error`,e=>{finalize()});let T=h._message,E=o._newFromSoupMessage(T),D=T.status_code,O=T.get_reason_phrase();if(c(D)){let e=E.get(`Location`),t=null;try{t=e===null?null:new g(e,h.url)}catch{if(h.redirect!==`manual`)throw finalize(),new n(`uri requested responds with an invalid redirect URL: ${e}`,`invalid-redirect`)}switch(h.redirect){case`error`:throw finalize(),new n(`uri requested responds with a redirect, redirect mode is set to error: ${h.url}`,`no-redirect`);case`manual`:break;case`follow`:{if(t===null)break;if(h.counter>=h.follow)throw finalize(),new n(`maximum redirect reached at: ${h.url}`,`max-redirect`);let e={headers:new o(h.headers),follow:h.follow,counter:h.counter+1,agent:h.agent,compress:h.compress,method:h.method,body:a(h),signal:h.signal,size:h.size,referrer:h.referrer,referrerPolicy:h.referrerPolicy};if(!r(h.url,t)||!i(h.url,t))for(let t of[`authorization`,`www-authenticate`,`cookie`,`cookie2`])e.headers.delete(t);if(D!==303&&h.body&&m.body instanceof _.Readable)throw finalize(),new n(`Cannot follow redirect with body being a readable stream`,`unsupported-redirect`);(D===303||(D===301||D===302)&&h.method===`POST`)&&(e.method=`GET`,e.body=void 0,e.headers.delete(`content-length`));let s=u(E);return s&&(e.referrerPolicy=s),finalize(),fetch(new d(t,e))}default:throw TypeError(`Redirect option '${h.redirect}' is not a valid value of RequestRedirect`)}}let k={url:h.url,status:D,statusText:O,headers:E,size:h.size,counter:h.counter,highWaterMark:h.highWaterMark},A=E.get(`Content-Encoding`);if(!h.compress||h.method===`HEAD`||A===null||D===204||D===304)return finalize(),new l(C,k);if(typeof DecompressionStream<`u`){let t=null;if(A===`gzip`||A===`x-gzip`?t=`gzip`:(A===`deflate`||A===`x-deflate`)&&(t=`deflate`),t){let n=new e([await new l(C,k).arrayBuffer()]).stream().pipeThrough(new DecompressionStream(t));return finalize(),new l(n,k)}}return finalize(),new l(C,k)}export{p as AbortError,e as Blob,n as FetchError,t as File,v as FormData,o as Headers,d as Request,l as Response,m as XMLHttpRequest,h as XMLHttpRequestUpload,fetch as default,c as isRedirect};
@@ -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`);let g=null;function envPositiveInt(e,t){let n=p.getenv(e);if(!n)return t;let r=Number.parseInt(n,10);return Number.isFinite(r)&&r>0?r:t}function getSharedSession(){if(g===null){let e=envPositiveInt(`GJSIFY_FETCH_MAX_CONNS_PER_HOST`,16),t=Math.max(envPositiveInt(`GJSIFY_FETCH_MAX_CONNS`,64),e);g=new f.Session({maxConns:t,maxConnsPerHost:e});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.ENCODED)}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.ENCODED)})),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
+ 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 envPositiveInt(e,t){let n=p.getenv(e);if(!n)return t;let r=Number.parseInt(n,10);return Number.isFinite(r)&&r>0?r:t}function shouldAcceptAnyCert(e){return e===!1?!0:e===!0?!1:p.getenv(`NODE_TLS_REJECT_UNAUTHORIZED`)===`0`}function getSharedSession(){if(g===null){let e=envPositiveInt(`GJSIFY_FETCH_MAX_CONNS_PER_HOST`,16),t=Math.max(envPositiveInt(`GJSIFY_FETCH_MAX_CONNS`,64),e);g=new f.Session({maxConns:t,maxConnsPerHost:e});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.ENCODED)}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;rejectUnauthorized;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.ENCODED)})),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.rejectUnauthorized=s.rejectUnauthorized??o.rejectUnauthorized,this.referrerPolicy=i?.referrerPolicy||o.referrerPolicy||``}async _send(e){let{session:t,message:n,signal:r,parsedURL:i}=this[h];if(!t||!n)throw Error(`Cannot send request: no Soup session (non-HTTP URL?)`);e.headers._appendToSoupMessage(n);let a=this._rawBodyBuffer;if(a!==null&&a.byteLength>0){let t=e.headers.get(`content-type`)||null;n.set_request_body_from_bytes(t,new p.Bytes(a))}i.protocol===`https:`&&shouldAcceptAnyCert(this.rejectUnauthorized)&&n.connect(`accept-certificate`,(e,t,n)=>!0);let c=new m.Cancellable;return r&&(r.aborted?c.cancel():r.addEventListener(`abort`,()=>c.cancel(),{once:!0})),this[h].inputStream=await s(t,n,p.PRIORITY_DEFAULT,c),this[h].readable=o(this[h].inputStream),{inputStream:this[h].inputStream,readable:this[h].readable,cancellable:c}}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};
package/lib/index.js CHANGED
@@ -112,6 +112,11 @@ export default async function fetch(url, init = {}) {
112
112
  cancellable = sendRes.cancellable;
113
113
  }
114
114
  catch (error) {
115
+ // A send aborted during connect/handshake/TTFB surfaces as a cancelled send_async — report it
116
+ // as an AbortError (Node parity), not a generic system FetchError.
117
+ if (signal && signal.aborted) {
118
+ throw new AbortError('The operation was aborted.');
119
+ }
115
120
  const err = error instanceof Error ? error : new Error(String(error));
116
121
  throw new FetchError(`request to ${request.url} failed, reason: ${err.message}`, 'system', err);
117
122
  }
package/lib/request.d.ts CHANGED
@@ -68,6 +68,8 @@ export declare class Request extends Body {
68
68
  agent: string | ((url: URL) => string);
69
69
  highWaterMark: number;
70
70
  insecureHTTPParser: boolean;
71
+ /** Per-request TLS opt-out (undefined ⇒ verify, honoring NODE_TLS_REJECT_UNAUTHORIZED). */
72
+ rejectUnauthorized?: boolean;
71
73
  constructor(input: RequestInfo | URL | Request, init?: RequestInit);
72
74
  /**
73
75
  * Send the request using Soup.
package/lib/request.js CHANGED
@@ -73,6 +73,18 @@ function envPositiveInt(name, fallback) {
73
73
  const n = Number.parseInt(raw, 10);
74
74
  return Number.isFinite(n) && n > 0 ? n : fallback;
75
75
  }
76
+ /**
77
+ * Whether to accept ANY peer certificate for a request. Node parity: an explicit
78
+ * `rejectUnauthorized` wins (true keeps verification even under the env override); otherwise the
79
+ * process-global `NODE_TLS_REJECT_UNAUTHORIZED=0` disables verification (as Node's fetch does).
80
+ */
81
+ function shouldAcceptAnyCert(rejectUnauthorized) {
82
+ if (rejectUnauthorized === false)
83
+ return true;
84
+ if (rejectUnauthorized === true)
85
+ return false;
86
+ return GLib.getenv('NODE_TLS_REJECT_UNAUTHORIZED') === '0';
87
+ }
76
88
  function getSharedSession() {
77
89
  if (sharedSession === null) {
78
90
  const maxPerHost = envPositiveInt('GJSIFY_FETCH_MAX_CONNS_PER_HOST', DEFAULT_MAX_CONNS_PER_HOST);
@@ -193,6 +205,8 @@ export class Request extends Body {
193
205
  agent = '';
194
206
  highWaterMark = 16384;
195
207
  insecureHTTPParser = false;
208
+ /** Per-request TLS opt-out (undefined ⇒ verify, honoring NODE_TLS_REJECT_UNAUTHORIZED). */
209
+ rejectUnauthorized;
196
210
  constructor(input, init) {
197
211
  const inputRL = input;
198
212
  const initRL = (init || {});
@@ -303,6 +317,7 @@ export class Request extends Body {
303
317
  this.agent = initRL.agent || inputRL.agent;
304
318
  this.highWaterMark = initRL.highWaterMark || inputRL.highWaterMark || 16384;
305
319
  this.insecureHTTPParser = initRL.insecureHTTPParser || inputRL.insecureHTTPParser || false;
320
+ this.rejectUnauthorized = initRL.rejectUnauthorized ?? inputRL.rejectUnauthorized;
306
321
  // §5.4, Request constructor steps, step 16.
307
322
  // Default is empty string per https://fetch.spec.whatwg.org/#concept-request-referrer-policy
308
323
  this.referrerPolicy = init?.referrerPolicy || inputRL.referrerPolicy || '';
@@ -311,7 +326,7 @@ export class Request extends Body {
311
326
  * Send the request using Soup.
312
327
  */
313
328
  async _send(options) {
314
- const { session, message } = this[INTERNALS];
329
+ const { session, message, signal, parsedURL } = this[INTERNALS];
315
330
  if (!session || !message) {
316
331
  throw new Error('Cannot send request: no Soup session (non-HTTP URL?)');
317
332
  }
@@ -328,7 +343,29 @@ export class Request extends Body {
328
343
  const contentType = options.headers.get('content-type') || null;
329
344
  message.set_request_body_from_bytes(contentType, new GLib.Bytes(rawBuf));
330
345
  }
346
+ // Per-connection TLS policy: when this request opts out of verification
347
+ // (`rejectUnauthorized: false`, or the Node-global NODE_TLS_REJECT_UNAUTHORIZED=0), accept the
348
+ // peer cert for THIS message only — never the shared session, which would silently disable
349
+ // verification for every other fetch. Mirrors the `accept-certificate` idiom in
350
+ // @gjsify/{tls,http2}. `accept-certificate` lives on Soup.Message (not the session) in
351
+ // libsoup 3 and isn't in the typed SignalSignatures map, so widen to the string overload.
352
+ if (parsedURL.protocol === 'https:' && shouldAcceptAnyCert(this.rejectUnauthorized)) {
353
+ const signalName = 'accept-certificate';
354
+ message.connect(signalName, (_msg, _cert, _errors) => true);
355
+ }
331
356
  const cancellable = new Gio.Cancellable();
357
+ // Close the connect/TTFB abort gap: wire abort → cancel BEFORE send_async. index.ts only wires
358
+ // it AFTER _send resolves (i.e. after response headers arrive), so an abort fired during
359
+ // connect + request-send + time-to-first-byte was previously dropped — the hung-server timeout
360
+ // case. A cancelled send_async rejects; index.ts maps that to an AbortError when signal.aborted.
361
+ if (signal) {
362
+ if (signal.aborted) {
363
+ cancellable.cancel();
364
+ }
365
+ else {
366
+ signal.addEventListener('abort', () => cancellable.cancel(), { once: true });
367
+ }
368
+ }
332
369
  this[INTERNALS].inputStream = await soupSendAsync(session, message, GLib.PRIORITY_DEFAULT, cancellable);
333
370
  this[INTERNALS].readable = inputStreamToReadable(this[INTERNALS].inputStream);
334
371
  return {
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,263 @@
1
+ // TLS opt-out + abort behavior for @gjsify/fetch on GJS — original regression tests.
2
+ //
3
+ // Covers two Node-parity gaps closed in request.ts / index.ts:
4
+ // 1. Per-connection TLS: a request may opt out of certificate verification with
5
+ // `rejectUnauthorized: false` (mirroring undici's `connect.rejectUnauthorized`) — applied to
6
+ // THAT Soup.Message only via its `accept-certificate` signal, never the shared session. The
7
+ // process-global `NODE_TLS_REJECT_UNAUTHORIZED=0` is also honored (as Node's fetch does).
8
+ // 2. Abort: an aborted signal rejects the fetch with an AbortError (name === 'AbortError'), and
9
+ // the abort→cancel wiring is attached BEFORE send_async so an abort during connect/TTFB is not
10
+ // lost (see request.ts `_send`).
11
+ //
12
+ // GJS-only (`.gjs.spec.ts`): the body runs under `on('Gjs', …)`, a no-op on Node. Soup/GLib/Gio are
13
+ // read from the GJS bootstrap `imports` (same idiom as soup-session.gjs.spec.ts) so the Node bundle
14
+ // never resolves `gi://*`. Request/fetch are read from `globalThis` (installed by
15
+ // `@gjsify/fetch/register`, pulled in by test.mts). `@gjsify/abort-controller` is pure-TS
16
+ // cross-runtime, so its static import is safe on both legs.
17
+ import { describe, it, expect, on } from '@gjsify/unit';
18
+ import { AbortController, AbortSignal } from '@gjsify/abort-controller';
19
+ // A REAL self-signed cert+key (RSA-2048, CN=localhost, SAN DNS:localhost + IP:127.0.0.1, 100y) so a
20
+ // live TLS handshake against the local Soup.Server actually completes. Embedded inline to stay
21
+ // hermetic (no fixtures) — same approach as packages/node/tls TEST_CERT_AND_KEY. Regenerate with:
22
+ // openssl req -x509 -newkey rsa:2048 -nodes -keyout key.pem -out cert.pem -days 36500 \
23
+ // -subj /CN=localhost -addext "subjectAltName = DNS:localhost,IP:127.0.0.1"
24
+ const SELF_SIGNED_CERT_AND_KEY = `-----BEGIN CERTIFICATE-----
25
+ MIIDJzCCAg+gAwIBAgIUKYxN2Fykn0Sr0YIq7h11xSofpYQwDQYJKoZIhvcNAQEL
26
+ BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI2MDcxMTA4MzcwN1oYDzIxMjYw
27
+ NjE3MDgzNzA3WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
28
+ AQUAA4IBDwAwggEKAoIBAQC9S/lXTWdjIpw7dI2v2095KssX99/Kh6xznHZ9Ed3D
29
+ gGGivMXSkPJfFeEj/LecSTJn0YVxGtu/tdon3Gnswk3JfFBuzm0MG5ks4puRkKsu
30
+ JV8+TtpcsBHVglTVnTqiw44yfk+VZdLomZHdcd61sTG7r97+TtVHbjZqQ0rKjhwB
31
+ fv3TysLrvE07rrB1UbagISroYg+G5EysQH0Yu9JbRsvefOKaK9vwq/jbrdSWFrez
32
+ R6xePGZsInr38Zt57O32ok0HMsT+DOeB+2GE0xd4+gEgr3XXHSP2PrWOtDtq74d0
33
+ 25H9kFpp7C4kypY/lt6/i6D7CKu685zK68EKyb3EvyubAgMBAAGjbzBtMB0GA1Ud
34
+ DgQWBBTkl+nvQOXFs4mOZRQFIXy9MCCjJjAfBgNVHSMEGDAWgBTkl+nvQOXFs4mO
35
+ ZRQFIXy9MCCjJjAPBgNVHRMBAf8EBTADAQH/MBoGA1UdEQQTMBGCCWxvY2FsaG9z
36
+ dIcEfwAAATANBgkqhkiG9w0BAQsFAAOCAQEAfkpo3YBTfdLAGBcrkDo0WSJYpPAK
37
+ 4rIZ3zk8aWeMzockt9NZ3/1+1OwtFDmL2bHHxHJF03DuPbgQR4uq3EBNPWGP72dy
38
+ +WsTGe6NJakt3QVYq5+73mtxy7fu/e4IF0vPQ9O/euDZoPQiplT7u/AcBcVZvlRv
39
+ t/TGdolGlGT7lzx6WYXrAmpCyQnQu/CU/OM9DBFVy22i+QbtJpd6HJdr68Nry56V
40
+ exU4w6PAIpKdAY4fayvooSDBCOetDuYJ8sIeiFM4xNwK55h/dedvEZWbsHr5OwJ6
41
+ dW8IEugXvvXvGrs3QJjSOd1B9HFsmZLyzZ5Ym3s6jl96DaTEvqBM5kKMVw==
42
+ -----END CERTIFICATE-----
43
+ -----BEGIN PRIVATE KEY-----
44
+ MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC9S/lXTWdjIpw7
45
+ dI2v2095KssX99/Kh6xznHZ9Ed3DgGGivMXSkPJfFeEj/LecSTJn0YVxGtu/tdon
46
+ 3Gnswk3JfFBuzm0MG5ks4puRkKsuJV8+TtpcsBHVglTVnTqiw44yfk+VZdLomZHd
47
+ cd61sTG7r97+TtVHbjZqQ0rKjhwBfv3TysLrvE07rrB1UbagISroYg+G5EysQH0Y
48
+ u9JbRsvefOKaK9vwq/jbrdSWFrezR6xePGZsInr38Zt57O32ok0HMsT+DOeB+2GE
49
+ 0xd4+gEgr3XXHSP2PrWOtDtq74d025H9kFpp7C4kypY/lt6/i6D7CKu685zK68EK
50
+ yb3EvyubAgMBAAECggEABQaKqFFuUYIiKaUafByGSqXIlLmVsdwSMQfqlFX9l7uj
51
+ m/Hhs68YGSG9x0lPOcgBd+aMcMZtcfwUL1u7P9ZF0Fm8d2YLSaDqTB28njywrzZU
52
+ nI2LFGJ2v6/QQvFWo8yXAVulLTAuLy2R+bey0hodSt55l0/wL3FS4pGgubjyTgr+
53
+ ZY4DWjxwdPqZSC29nBN6HtOU0LMt+0UZS4QoSkb4LP/OSG7ZfZk2ssuVJioZnNwS
54
+ Gh6XB7YjzDkMcYtZ9Os8rvFdeECqM53N3DT8RHImbQZpkamodhJR5/Y7MNFAfg0l
55
+ yISz8u47iS2ZJV/3ePbxOotRtAPLbyErfo1PVkOsgQKBgQDz53RUbKTvEzXTVcTW
56
+ MhDc4pNtT7FWNmAhYCaVboJk6jisWOagzUHSZyJd+KFOPCwW0TZAd3DXbAjBCVg7
57
+ /Yuxu0K3tvYsDRAyaKrDrF4I03aS3d7GlbaNBxNmWZXwd6UloLmXQvmWooX7FlfM
58
+ WXV2BgfEc/RaQV4e+CcZgd96GwKBgQDGrz0HsgaiqBknWtDjVJEEnU3DMEEWPF6u
59
+ NQ+YZ3gOJE7yIIs/aJXK1TJE+i+CMI9y+O50eyHIqE6xbF/aM0gTgAzzWPVwan4z
60
+ P+8MZcHrRrLUWCi/hULQXjzKAh33zUc8+npEewIIaIe2SQp/j4NTYKjYqXS6Vkvh
61
+ cR6oQNIsgQKBgQCExsJ4VzCuiZmqs61VIao2ZQ322wRiN3W7lZlLjf5kK3GwuTDL
62
+ 5xFnFggKehht+6nQvhG3pI4EZ+aYF5s2BT/wAE5ArtRyhKYFmx8jKImEaoHJUAk/
63
+ uC7JtUllVW2fm7KGumluI+K1k0I2vyIKt8SO/Jdr5efm3mD7oQHADAh5cwKBgHUq
64
+ slrvaWpSp0LGE5l8LWkDOvB4bGmogE1LjT7bBbmRP90ZxARIsM1EnLiAsbOc33t1
65
+ wWl/k5S5bG7E8mHDDCYzxW9cbqE0q+edNGOjPpB4yMzgkGchNx2Z9U9LoFCaBgC3
66
+ 1ZK8154NxrqAqGhBQbjRr8DVbbjVWzo/c0zVrRwBAoGBAJ8USM/06E60u0Tq8zU9
67
+ q1z0p3hu++Gude5e9IHp3tnS2/MLIbJDyfvkVkx5gLJXKYdEykP10CLOqu8DC1c5
68
+ 9FvsYKlRm0GzDlTtX12jXGWHaQXy9Di69Sd3J6StLf04IEl3W4e9KTg7zDA9cg94
69
+ StMiAiohwWMqIzCc88GovQMF
70
+ -----END PRIVATE KEY-----
71
+ `;
72
+ export default async () => {
73
+ await on('Gjs', async () => {
74
+ // Read GJS runtime objects lazily (no static gi:// import → Node bundle stays clean).
75
+ const gjs = globalThis.imports;
76
+ const Soup = gjs.gi.Soup;
77
+ const GLib = gjs.gi.GLib;
78
+ const Gio = gjs.gi.Gio;
79
+ // Request + fetch are installed as globals by `@gjsify/fetch/register` (via test.mts). Type
80
+ // them with FetchInit so the GJS-only options type-check under the fetch package's DOM lib.
81
+ const RequestCtor = globalThis
82
+ .Request;
83
+ const fetchFn = globalThis
84
+ .fetch;
85
+ /** Start a fresh self-signed HTTPS Soup.Server on loopback; returns it plus its base URL. */
86
+ const startHttpsServer = () => {
87
+ const cert = Gio.TlsCertificate.new_from_pem(SELF_SIGNED_CERT_AND_KEY, -1);
88
+ const server = new Soup.Server({});
89
+ server.set_tls_certificate(cert);
90
+ server.add_handler(null, (_s, msg) => {
91
+ msg.set_status(200, null);
92
+ msg.set_response('text/plain', Soup.MemoryUse.COPY, new TextEncoder().encode('ok'));
93
+ });
94
+ server.listen_local(0, (Soup.ServerListenOptions.HTTPS | Soup.ServerListenOptions.IPV4_ONLY));
95
+ return { server, base: server.get_uris()[0].to_string() };
96
+ };
97
+ await describe('@gjsify/fetch — per-connection TLS opt-out plumbing', async () => {
98
+ await it('carries rejectUnauthorized:false from init onto the request', () => {
99
+ const req = new RequestCtor('https://example.com/', { rejectUnauthorized: false });
100
+ expect(req.rejectUnauthorized).toBe(false);
101
+ });
102
+ await it('defaults rejectUnauthorized to undefined (verification stays on)', () => {
103
+ const req = new RequestCtor('https://example.com/');
104
+ expect(req.rejectUnauthorized).toBe(undefined);
105
+ });
106
+ });
107
+ await describe('@gjsify/fetch — abort surfaces as AbortError', async () => {
108
+ await it('rejects with an AbortError when the signal is already aborted', async () => {
109
+ const ctrl = new AbortController();
110
+ ctrl.abort();
111
+ let name = '';
112
+ try {
113
+ // Already-aborted → rejects before any socket work, so no server is needed.
114
+ await fetchFn('http://127.0.0.1:1/', { signal: ctrl.signal });
115
+ }
116
+ catch (e) {
117
+ name = e.name;
118
+ }
119
+ expect(name).toBe('AbortError');
120
+ });
121
+ });
122
+ await describe('@gjsify/fetch — self-signed TLS (per-connection opt-out, no shared-session leak)', async () => {
123
+ await it('rejects a self-signed host by default (verification on)', async () => {
124
+ const { server, base } = startHttpsServer();
125
+ let rejected = false;
126
+ try {
127
+ await fetchFn(base);
128
+ }
129
+ catch {
130
+ rejected = true;
131
+ }
132
+ finally {
133
+ server.disconnect();
134
+ }
135
+ expect(rejected).toBe(true);
136
+ });
137
+ await it('resolves when the request opts out with rejectUnauthorized:false', async () => {
138
+ const { server, base } = startHttpsServer();
139
+ let status = 0;
140
+ let body = '';
141
+ try {
142
+ const res = await fetchFn(base, { rejectUnauthorized: false });
143
+ status = res.status;
144
+ body = await res.text();
145
+ }
146
+ finally {
147
+ server.disconnect();
148
+ }
149
+ expect(status).toBe(200);
150
+ expect(body).toBe('ok');
151
+ });
152
+ await it('honors NODE_TLS_REJECT_UNAUTHORIZED=0 for the default call', async () => {
153
+ const { server, base } = startHttpsServer();
154
+ const prev = GLib.getenv('NODE_TLS_REJECT_UNAUTHORIZED');
155
+ GLib.setenv('NODE_TLS_REJECT_UNAUTHORIZED', '0', true);
156
+ let status = 0;
157
+ try {
158
+ const res = await fetchFn(base); // no per-request opt-out → env is what opens it
159
+ status = res.status;
160
+ await res.text();
161
+ }
162
+ finally {
163
+ if (prev === null)
164
+ GLib.unsetenv('NODE_TLS_REJECT_UNAUTHORIZED');
165
+ else
166
+ GLib.setenv('NODE_TLS_REJECT_UNAUTHORIZED', prev, true);
167
+ server.disconnect();
168
+ }
169
+ expect(status).toBe(200);
170
+ });
171
+ await it('lets an explicit rejectUnauthorized:true win over NODE_TLS_REJECT_UNAUTHORIZED=0', async () => {
172
+ const { server, base } = startHttpsServer();
173
+ const prev = GLib.getenv('NODE_TLS_REJECT_UNAUTHORIZED');
174
+ GLib.setenv('NODE_TLS_REJECT_UNAUTHORIZED', '0', true);
175
+ let rejected = false;
176
+ try {
177
+ await fetchFn(base, { rejectUnauthorized: true });
178
+ }
179
+ catch {
180
+ rejected = true;
181
+ }
182
+ finally {
183
+ if (prev === null)
184
+ GLib.unsetenv('NODE_TLS_REJECT_UNAUTHORIZED');
185
+ else
186
+ GLib.setenv('NODE_TLS_REJECT_UNAUTHORIZED', prev, true);
187
+ server.disconnect();
188
+ }
189
+ expect(rejected).toBe(true);
190
+ });
191
+ await it('scopes the accepted cert to the message: a normal fetch elsewhere still verifies', async () => {
192
+ // Two independent self-signed servers. Accepting server A's cert (per-message) must NOT
193
+ // teach the shared Soup.Session to trust server B — B is a fresh host:port with no
194
+ // pooled connection, so it must handshake + verify from scratch and reject.
195
+ const a = startHttpsServer();
196
+ const b = startHttpsServer();
197
+ let insecureStatus = 0;
198
+ let secureRejected = false;
199
+ try {
200
+ const res = await fetchFn(a.base, { rejectUnauthorized: false });
201
+ insecureStatus = res.status;
202
+ await res.text();
203
+ try {
204
+ await fetchFn(b.base); // default on the SAME shared session must still verify
205
+ }
206
+ catch {
207
+ secureRejected = true;
208
+ }
209
+ }
210
+ finally {
211
+ a.server.disconnect();
212
+ b.server.disconnect();
213
+ }
214
+ expect(insecureStatus).toBe(200);
215
+ expect(secureRejected).toBe(true);
216
+ });
217
+ });
218
+ await describe('@gjsify/fetch — AbortSignal.timeout against a hung server rejects promptly', async () => {
219
+ await it('rejects with AbortError while waiting for response headers (connect-phase fix)', async () => {
220
+ const server = new Soup.Server({});
221
+ server.add_handler(null, (_s, msg) => {
222
+ // Never respond: pause the message so the client blocks on time-to-first-byte. On
223
+ // `main` the abort wired only AFTER _send resolves is dropped here and the fetch
224
+ // hangs; the fix wires it before send_async, so AbortSignal.timeout cancels the
225
+ // in-flight send and index.ts maps the cancellation to an AbortError.
226
+ server.pause_message(msg);
227
+ });
228
+ server.listen_local(0, Soup.ServerListenOptions.IPV4_ONLY);
229
+ const base = server.get_uris()[0].to_string();
230
+ // Bounded so a regression fails the test instead of hanging CI: if the fetch has not
231
+ // settled within 3s the watchdog rejects and the AbortError assertion below fails.
232
+ let watchdogId = 0;
233
+ const watchdog = new Promise((_resolve, reject) => {
234
+ watchdogId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 3000, () => {
235
+ watchdogId = 0;
236
+ reject(new Error('fetch did not settle within 3s — connect-phase abort regressed'));
237
+ return GLib.SOURCE_REMOVE;
238
+ });
239
+ });
240
+ const startedUs = GLib.get_monotonic_time();
241
+ let name = '';
242
+ let elapsedMs = 0;
243
+ try {
244
+ await Promise.race([fetchFn(base, { signal: AbortSignal.timeout(50) }), watchdog]);
245
+ }
246
+ catch (e) {
247
+ name = e.name;
248
+ elapsedMs = (GLib.get_monotonic_time() - startedUs) / 1000;
249
+ }
250
+ finally {
251
+ if (watchdogId) {
252
+ GLib.source_remove(watchdogId);
253
+ watchdogId = 0;
254
+ }
255
+ server.disconnect();
256
+ }
257
+ expect(name).toBe('AbortError');
258
+ // The 50ms timeout must fire during connect/TTFB — well under the 3s watchdog.
259
+ expect(elapsedMs < 2000).toBe(true);
260
+ });
261
+ });
262
+ });
263
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/fetch",
3
- "version": "0.16.9",
3
+ "version": "0.18.0",
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",
@@ -41,7 +41,7 @@
41
41
  "build:test:gjs": "gjsify build src/test.mts --app gjs --outfile test.gjs.mjs",
42
42
  "build:test:browser": "gjsify build src/test.browser.mts --app browser --outfile dist/test.browser.mjs",
43
43
  "build:test:node": "gjsify build src/test.mts --app node --outfile test.node.mjs",
44
- "test": "gjsify run build:gjsify && gjsify run build:test:node && gjsify run test:node",
44
+ "test": "gjsify run build:gjsify && gjsify run build:test && gjsify run test:node && gjsify run test:gjs",
45
45
  "test:gjs": "gjsify run test.gjs.mjs",
46
46
  "test:node": "node test.node.mjs"
47
47
  },
@@ -51,8 +51,8 @@
51
51
  "fetch"
52
52
  ],
53
53
  "devDependencies": {
54
- "@gjsify/cli": "^0.16.9",
55
- "@gjsify/unit": "^0.16.9",
54
+ "@gjsify/cli": "^0.18.0",
55
+ "@gjsify/unit": "^0.18.0",
56
56
  "@types/node": "^25.9.2",
57
57
  "typescript": "^6.0.3"
58
58
  },
@@ -61,10 +61,10 @@
61
61
  "@girs/gjs": "^4.1.0",
62
62
  "@girs/glib-2.0": "^4.1.0",
63
63
  "@girs/soup-3.0": "^4.1.0",
64
- "@gjsify/formdata": "^0.16.9",
65
- "@gjsify/http": "^0.16.9",
66
- "@gjsify/url": "^0.16.9",
67
- "@gjsify/utils": "^0.16.9"
64
+ "@gjsify/formdata": "^0.18.0",
65
+ "@gjsify/http": "^0.18.0",
66
+ "@gjsify/url": "^0.18.0",
67
+ "@gjsify/utils": "^0.18.0"
68
68
  },
69
69
  "gjsify": {
70
70
  "runtimes": {