@gjsify/fetch 0.4.44 → 0.5.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.
@@ -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.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
+ 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,2 +1,2 @@
1
- import"../_virtual/_rolldown/runtime.js";import{File as e}from"./blob-from.js";import{FormData as t}from"@gjsify/formdata";let n=1;const r={PART_BOUNDARY:n,LAST_BOUNDARY:n*=2},lower=e=>e|32,noop=(...e)=>{};var MultipartParser=class{index=0;flags=0;boundary;lookbehind;state=0;onHeaderEnd=noop;onHeaderField=noop;onHeadersEnd=noop;onHeaderValue=noop;onPartBegin=noop;onPartData=noop;onPartEnd=noop;boundaryChars={};constructor(e){e=`\r
2
- --`+e;let t=new Uint8Array(e.length);for(let n=0;n<e.length;n++)t[n]=e.charCodeAt(n),this.boundaryChars[t[n]]=!0;this.boundary=t,this.lookbehind=new Uint8Array(this.boundary.length+8)}write(e){let t=0,n=e.length,i=this.index,{lookbehind:a,boundary:o,boundaryChars:s,index:c,state:l,flags:u}=this,d=this.boundary.length,f=d-1,p=e.length,m,h,mark=e=>{this[e+`Mark`]=t},clear=e=>{delete this[e+`Mark`]},callback=(e,t,n,r)=>{(t===void 0||t!==n)&&this[e](r&&r.subarray(t,n))},dataCallback=(n,r=!1)=>{let i=n+`Mark`;i in this&&(r?(callback(n,this[i],t,e),delete this[i]):(callback(n,this[i],e.length,e),this[i]=0))};for(t=0;t<n;t++)switch(m=e[t],l){case 0:if(c===o.length-2){if(m===45)u|=r.LAST_BOUNDARY;else if(m!==13)return;c++;break}else if(c-1==o.length-2){if(u&r.LAST_BOUNDARY&&m===45)l=9,u=0;else if(!(u&r.LAST_BOUNDARY)&&m===10)c=0,callback(`onPartBegin`),l=1;else return;break}m!==o[c+2]&&(c=-2),m===o[c+2]&&c++;break;case 1:l=2,mark(`onHeaderField`),c=0;case 2:if(m===13){clear(`onHeaderField`),l=6;break}if(c++,m===45)break;if(m===58){if(c===1)return;dataCallback(`onHeaderField`,!0),l=3;break}if(h=lower(m),h<97||h>122)return;break;case 3:if(m===32)break;mark(`onHeaderValue`),l=4;case 4:m===13&&(dataCallback(`onHeaderValue`,!0),callback(`onHeaderEnd`),l=5);break;case 5:if(m!==10)return;l=1;break;case 6:if(m!==10)return;callback(`onHeadersEnd`),l=7;break;case 7:l=8,mark(`onPartData`);case 8:if(i=c,c===0){for(t+=f;t<p&&!(e[t]in s);)t+=d;t-=f,m=e[t]}if(c<o.length)o[c]===m?(c===0&&dataCallback(`onPartData`,!0),c++):c=0;else if(c===o.length)c++,m===13?u|=r.PART_BOUNDARY:m===45?u|=r.LAST_BOUNDARY:c=0;else if(c-1===o.length)if(u&r.PART_BOUNDARY){if(c=0,m===10){u&=~r.PART_BOUNDARY,callback(`onPartEnd`),callback(`onPartBegin`),l=1;break}}else u&r.LAST_BOUNDARY&&m===45?(callback(`onPartEnd`),l=9,u=0):c=0;if(c>0)a[c-1]=m;else if(i>0){let e=new Uint8Array(a.buffer,a.byteOffset,a.byteLength);callback(`onPartData`,0,i,e),i=0,mark(`onPartData`),t--}break;case 9:break;default:throw Error(`Unexpected state entered: ${l}`)}dataCallback(`onHeaderField`),dataCallback(`onHeaderValue`),dataCallback(`onPartData`),this.index=c,this.state=l,this.flags=u}end(){if(this.state===1&&this.index===0||this.state===8&&this.index===this.boundary.length)this.onPartEnd();else if(this.state!==9)throw Error(`MultipartParser.end(): stream ended unexpectedly`)}};function _fileName(e){let t=e.match(/\bfilename=("(.*?)"|([^()<>@,;:\\"/[\]?={}\s\t]+))($|;\s)/i);if(!t)return;let n=t[2]||t[3]||``,r=n.slice(n.lastIndexOf(`\\`)+1);return r=r.replace(/%22/g,`"`),r=r.replace(/&#(\d{4});/g,(e,t)=>String.fromCharCode(t)),r}async function toFormData(n,r){if(!/multipart/i.test(r))throw TypeError(`Failed to fetch`);let i=r.match(/boundary=(?:"([^"]+)"|([^;]+))/i);if(!i)throw TypeError(`no or bad content-type header, no multipart boundary`);let a=new MultipartParser(i[1]||i[2]),o,s,c,l,u,d,f=[],p=new t,onPartData=e=>{c+=m.decode(e,{stream:!0})},appendToFile=e=>{f.push(e)},appendFileToFormData=()=>{let t=new e(f,d,{type:u});p.append(l,t)},appendEntryToFormData=()=>{p.append(l,c)},m=new TextDecoder(`utf-8`);m.decode(),a.onPartBegin=function(){a.onPartData=onPartData,a.onPartEnd=appendEntryToFormData,o=``,s=``,c=``,l=``,u=``,d=null,f.length=0},a.onHeaderField=function(e){o+=m.decode(e,{stream:!0})},a.onHeaderValue=function(e){s+=m.decode(e,{stream:!0})},a.onHeaderEnd=function(){if(s+=m.decode(),o=o.toLowerCase(),o===`content-disposition`){let e=s.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i);e&&(l=e[2]||e[3]||``),d=_fileName(s),d&&(a.onPartData=appendToFile,a.onPartEnd=appendFileToFormData)}else o===`content-type`&&(u=s);s=``,o=``};for await(let e of n)a.write(e);return a.end(),p}export{toFormData};
1
+ import"../_virtual/_rolldown/runtime.js";import{File as e}from"./blob-from.js";import{FormData as t}from"@gjsify/formdata";var n=function(e){return e[e.START_BOUNDARY=0]=`START_BOUNDARY`,e[e.HEADER_FIELD_START=1]=`HEADER_FIELD_START`,e[e.HEADER_FIELD=2]=`HEADER_FIELD`,e[e.HEADER_VALUE_START=3]=`HEADER_VALUE_START`,e[e.HEADER_VALUE=4]=`HEADER_VALUE`,e[e.HEADER_VALUE_ALMOST_DONE=5]=`HEADER_VALUE_ALMOST_DONE`,e[e.HEADERS_ALMOST_DONE=6]=`HEADERS_ALMOST_DONE`,e[e.PART_DATA_START=7]=`PART_DATA_START`,e[e.PART_DATA=8]=`PART_DATA`,e[e.END=9]=`END`,e}(n||{});let r=1;const i={PART_BOUNDARY:r,LAST_BOUNDARY:r*=2},lower=e=>e|32,noop=(...e)=>{};var MultipartParser=class{index=0;flags=0;boundary;lookbehind;state=0;onHeaderEnd=noop;onHeaderField=noop;onHeadersEnd=noop;onHeaderValue=noop;onPartBegin=noop;onPartData=noop;onPartEnd=noop;boundaryChars={};constructor(e){e=`\r
2
+ --`+e;let t=new Uint8Array(e.length);for(let n=0;n<e.length;n++)t[n]=e.charCodeAt(n),this.boundaryChars[t[n]]=!0;this.boundary=t,this.lookbehind=new Uint8Array(this.boundary.length+8)}write(e){let t=0,n=e.length,r=this.index,{lookbehind:a,boundary:o,boundaryChars:s,index:c,state:l,flags:u}=this,d=this.boundary.length,f=d-1,p=e.length,m,h,mark=e=>{this[e+`Mark`]=t},clear=e=>{delete this[e+`Mark`]},callback=(e,t,n,r)=>{(t===void 0||t!==n)&&this[e](r&&r.subarray(t,n))},dataCallback=(n,r=!1)=>{let i=n+`Mark`;i in this&&(r?(callback(n,this[i],t,e),delete this[i]):(callback(n,this[i],e.length,e),this[i]=0))};for(t=0;t<n;t++)switch(m=e[t],l){case 0:if(c===o.length-2){if(m===45)u|=i.LAST_BOUNDARY;else if(m!==13)return;c++;break}else if(c-1==o.length-2){if(u&i.LAST_BOUNDARY&&m===45)l=9,u=0;else if(!(u&i.LAST_BOUNDARY)&&m===10)c=0,callback(`onPartBegin`),l=1;else return;break}m!==o[c+2]&&(c=-2),m===o[c+2]&&c++;break;case 1:l=2,mark(`onHeaderField`),c=0;case 2:if(m===13){clear(`onHeaderField`),l=6;break}if(c++,m===45)break;if(m===58){if(c===1)return;dataCallback(`onHeaderField`,!0),l=3;break}if(h=lower(m),h<97||h>122)return;break;case 3:if(m===32)break;mark(`onHeaderValue`),l=4;case 4:m===13&&(dataCallback(`onHeaderValue`,!0),callback(`onHeaderEnd`),l=5);break;case 5:if(m!==10)return;l=1;break;case 6:if(m!==10)return;callback(`onHeadersEnd`),l=7;break;case 7:l=8,mark(`onPartData`);case 8:if(r=c,c===0){for(t+=f;t<p&&!(e[t]in s);)t+=d;t-=f,m=e[t]}if(c<o.length)o[c]===m?(c===0&&dataCallback(`onPartData`,!0),c++):c=0;else if(c===o.length)c++,m===13?u|=i.PART_BOUNDARY:m===45?u|=i.LAST_BOUNDARY:c=0;else if(c-1===o.length)if(u&i.PART_BOUNDARY){if(c=0,m===10){u&=~i.PART_BOUNDARY,callback(`onPartEnd`),callback(`onPartBegin`),l=1;break}}else u&i.LAST_BOUNDARY&&m===45?(callback(`onPartEnd`),l=9,u=0):c=0;if(c>0)a[c-1]=m;else if(r>0){let e=new Uint8Array(a.buffer,a.byteOffset,a.byteLength);callback(`onPartData`,0,r,e),r=0,mark(`onPartData`),t--}break;case 9:break;default:throw Error(`Unexpected state entered: ${l}`)}dataCallback(`onHeaderField`),dataCallback(`onHeaderValue`),dataCallback(`onPartData`),this.index=c,this.state=l,this.flags=u}end(){if(this.state===1&&this.index===0||this.state===8&&this.index===this.boundary.length)this.onPartEnd();else if(this.state!==9)throw Error(`MultipartParser.end(): stream ended unexpectedly`)}};function _fileName(e){let t=e.match(/\bfilename=("(.*?)"|([^()<>@,;:\\"/[\]?={}\s\t]+))($|;\s)/i);if(!t)return;let n=t[2]||t[3]||``,r=n.slice(n.lastIndexOf(`\\`)+1);return r=r.replace(/%22/g,`"`),r=r.replace(/&#(\d{4});/g,(e,t)=>String.fromCharCode(t)),r}async function toFormData(n,r){if(!/multipart/i.test(r))throw TypeError(`Failed to fetch`);let i=r.match(/boundary=(?:"([^"]+)"|([^;]+))/i);if(!i)throw TypeError(`no or bad content-type header, no multipart boundary`);let a=new MultipartParser(i[1]||i[2]),o,s,c,l,u,d,f=[],p=new t,onPartData=e=>{c+=m.decode(e,{stream:!0})},appendToFile=e=>{f.push(e)},appendFileToFormData=()=>{let t=new e(f,d,{type:u});p.append(l,t)},appendEntryToFormData=()=>{p.append(l,c)},m=new TextDecoder(`utf-8`);m.decode(),a.onPartBegin=function(){a.onPartData=onPartData,a.onPartEnd=appendEntryToFormData,o=``,s=``,c=``,l=``,u=``,d=null,f.length=0},a.onHeaderField=function(e){o+=m.decode(e,{stream:!0})},a.onHeaderValue=function(e){s+=m.decode(e,{stream:!0})},a.onHeaderEnd=function(){if(s+=m.decode(),o=o.toLowerCase(),o===`content-disposition`){let e=s.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i);e&&(l=e[2]||e[3]||``),d=_fileName(s),d&&(a.onPartData=appendToFile,a.onPartEnd=appendFileToFormData)}else o===`content-type`&&(u=s);s=``,o=``};for await(let e of n)a.write(e);return a.end(),p}export{toFormData};
@@ -0,0 +1,2 @@
1
+ declare const _default: () => Promise<void>;
2
+ export default _default;
@@ -0,0 +1,287 @@
1
+ // Response-body PARTIAL_INPUT regression for @gjsify/fetch — GJS-only.
2
+ //
3
+ // Bug: fetch() failed to read SOME response bodies with
4
+ // Gio.IOErrorEnum: Weitere Eingaben erforderlich (G_IO_ERROR_PARTIAL_INPUT)
5
+ // Norman's accounting API responses tripped it while Qonto/Paperless were fine,
6
+ // and the exact same code path worked on Node. The trigger is a *response shape*
7
+ // the libsoup/zlib body reader mishandles at the tail — not the endpoint.
8
+ //
9
+ // Two independent places raise G_IO_ERROR_PARTIAL_INPUT (code 34) at end-of-body
10
+ // even though the full payload has already been produced:
11
+ //
12
+ // 1. Soup's chunked/length body input stream, when the server closes the
13
+ // socket without the terminating `0\r\n\r\n` chunk (or sends fewer bytes
14
+ // than a declared Content-Length). Surfaces as
15
+ // "Die Verbindung wurde unerwartet beendet" but matches PARTIAL_INPUT.
16
+ //
17
+ // 2. Gio.ZlibDecompressor (the GConverter backing node:zlib's gunzip/inflate,
18
+ // reached via fetch's DecompressionStream branch for gzip/deflate
19
+ // Content-Encoding) when the compressed stream's trailer (gzip CRC32+ISIZE
20
+ // / zlib adler tail) is missing — it has already emitted 100% of the
21
+ // decompressed bytes. THIS is the literal "Weitere Eingaben erforderlich".
22
+ //
23
+ // A real server that closes the keep-alive connection a touch early (or omits
24
+ // the chunk terminator under load) hits both. The body reader must treat a
25
+ // tail-only PARTIAL_INPUT as a clean EOF and return the bytes already delivered,
26
+ // exactly as Node's lenient stream/zlib paths do.
27
+ //
28
+ // This suite drives the REAL fetch() against a raw Gio.SocketService server so
29
+ // every byte on the wire is controlled. No external host is contacted.
30
+ //
31
+ // GJS-only (`.gjs.spec.ts`): the body runs under on('Gjs', …) and reads all GJS
32
+ // runtime objects from globalThis.imports so the Node bundle (same test.mts
33
+ // aggregator) never resolves gi://*.
34
+ import { describe, it, expect, on } from '@gjsify/unit';
35
+ const enc = (s) => new TextEncoder().encode(s);
36
+ function concatBytes(...arrs) {
37
+ const total = arrs.reduce((n, a) => n + a.length, 0);
38
+ const out = new Uint8Array(total);
39
+ let off = 0;
40
+ for (const a of arrs) {
41
+ out.set(a, off);
42
+ off += a.length;
43
+ }
44
+ return out;
45
+ }
46
+ export default async () => {
47
+ await on('Gjs', async () => {
48
+ const gjs = globalThis.imports;
49
+ const GLib = gjs.gi.GLib;
50
+ const Gio = gjs.gi.Gio;
51
+ const fetchFn = globalThis.fetch;
52
+ /** gzip `bytes` via Gio so the test does not depend on the polyfill it tests. */
53
+ const gzip = (bytes) => {
54
+ const comp = Gio.ZlibCompressor.new(Gio.ZlibCompressorFormat.GZIP, -1);
55
+ const mem = Gio.MemoryOutputStream.new_resizable();
56
+ const cs = Gio.ConverterOutputStream.new(mem, comp);
57
+ cs.write_all(bytes, null);
58
+ cs.flush(null);
59
+ cs.close(null);
60
+ return mem.steal_as_bytes().toArray().slice();
61
+ };
62
+ const deflate = (bytes) => {
63
+ const comp = Gio.ZlibCompressor.new(Gio.ZlibCompressorFormat.ZLIB, -1);
64
+ const mem = Gio.MemoryOutputStream.new_resizable();
65
+ const cs = Gio.ConverterOutputStream.new(mem, comp);
66
+ cs.write_all(bytes, null);
67
+ cs.flush(null);
68
+ cs.close(null);
69
+ return mem.steal_as_bytes().toArray().slice();
70
+ };
71
+ /**
72
+ * Start a raw TCP server that answers the first (and only) request on each
73
+ * connection with exactly `responseBytes`, then closes the socket. Returns
74
+ * the bound origin and a stop() fn. The framing is entirely up to the
75
+ * caller — that is the whole point.
76
+ */
77
+ const startRawServer = (responseBytes) => {
78
+ const service = new Gio.SocketService();
79
+ const port = service.add_any_inet_port(null);
80
+ service.connect('incoming', (_svc, conn) => {
81
+ const input = conn.get_input_stream();
82
+ const output = conn.get_output_stream();
83
+ // Drain the request headers, then write the canned bytes and close.
84
+ input.read_bytes_async(65536, GLib.PRIORITY_DEFAULT, null, (s, r) => {
85
+ try {
86
+ s.read_bytes_finish(r);
87
+ }
88
+ catch {
89
+ /* ignore request-read errors */
90
+ }
91
+ output.write_bytes_async(new GLib.Bytes(responseBytes), GLib.PRIORITY_DEFAULT, null, (os, or) => {
92
+ try {
93
+ os.write_bytes_finish(or);
94
+ }
95
+ catch {
96
+ /* ignore */
97
+ }
98
+ // Close WITHOUT any graceful HTTP framing beyond what the
99
+ // caller put in responseBytes.
100
+ try {
101
+ conn.close(null);
102
+ }
103
+ catch {
104
+ /* ignore */
105
+ }
106
+ });
107
+ });
108
+ return false;
109
+ });
110
+ service.start();
111
+ const base = `http://127.0.0.1:${port}/`;
112
+ return {
113
+ base,
114
+ stop: () => {
115
+ try {
116
+ service.stop();
117
+ service.close();
118
+ }
119
+ catch {
120
+ /* ignore */
121
+ }
122
+ },
123
+ };
124
+ };
125
+ const PAYLOAD = JSON.stringify({
126
+ hello: 'world',
127
+ items: [1, 2, 3, 4, 5],
128
+ // Long enough that gzip actually produces a multi-byte deflate block.
129
+ note: 'gjsify-fetch-partial-input-'.repeat(40),
130
+ });
131
+ // A payload whose GZIPPED form exceeds the body reader's 4096-byte read
132
+ // size, so the compressed stream is delivered to DecompressionStream in
133
+ // SEVERAL chunks. This is the primary Norman trigger: stateless per-chunk
134
+ // decompression breaks here (the first chunk alone is not a complete gzip
135
+ // stream) while native Node keeps inflate state across chunks. The body
136
+ // mixes structure + varied text so gzip can't collapse it below 4 KB.
137
+ const bigItems = [];
138
+ for (let i = 0; i < 500; i++) {
139
+ bigItems.push({
140
+ id: i,
141
+ name: `transaction ${i}`,
142
+ amount: (i * 3.14159).toFixed(2),
143
+ memo: `Rechnung Nr ${i} für Leistung im Monat ${(i % 12) + 1}`,
144
+ });
145
+ }
146
+ const BIG_PAYLOAD = JSON.stringify({ ok: true, items: bigItems });
147
+ await describe('@gjsify/fetch — response body PARTIAL_INPUT at EOF (Norman regression)', async () => {
148
+ await it('reads a chunked body terminated cleanly (baseline)', async () => {
149
+ const body = enc('HTTP/1.1 200 OK\r\n' +
150
+ 'Content-Type: application/json\r\n' +
151
+ 'Transfer-Encoding: chunked\r\n' +
152
+ '\r\n' +
153
+ PAYLOAD.length.toString(16) +
154
+ '\r\n' +
155
+ PAYLOAD +
156
+ '\r\n0\r\n\r\n');
157
+ const srv = startRawServer(body);
158
+ try {
159
+ const res = await fetchFn(srv.base);
160
+ const text = await res.text();
161
+ expect(text).toBe(PAYLOAD);
162
+ }
163
+ finally {
164
+ srv.stop();
165
+ }
166
+ });
167
+ await it('reads a chunked body whose terminating 0-chunk never arrives', async () => {
168
+ // Server streamed the whole payload then dropped the connection
169
+ // without `0\r\n\r\n`. Soup raises PARTIAL_INPUT on the final read;
170
+ // all bytes were already delivered, so .text() must still be whole.
171
+ const body = enc('HTTP/1.1 200 OK\r\n' +
172
+ 'Content-Type: application/json\r\n' +
173
+ 'Transfer-Encoding: chunked\r\n' +
174
+ '\r\n' +
175
+ PAYLOAD.length.toString(16) +
176
+ '\r\n' +
177
+ PAYLOAD);
178
+ const srv = startRawServer(body);
179
+ try {
180
+ const res = await fetchFn(srv.base);
181
+ const json = (await res.json());
182
+ expect(json.hello).toBe('world');
183
+ }
184
+ finally {
185
+ srv.stop();
186
+ }
187
+ });
188
+ await it('reads a Content-Length body that closes one frame early', async () => {
189
+ const body = enc('HTTP/1.1 200 OK\r\n' +
190
+ 'Content-Type: application/json\r\n' +
191
+ 'Content-Length: ' +
192
+ (PAYLOAD.length + 16) + // server lies: promises more than it sends
193
+ '\r\n\r\n' +
194
+ PAYLOAD);
195
+ const srv = startRawServer(body);
196
+ try {
197
+ const res = await fetchFn(srv.base);
198
+ const text = await res.text();
199
+ expect(text).toBe(PAYLOAD);
200
+ }
201
+ finally {
202
+ srv.stop();
203
+ }
204
+ });
205
+ await it('reads a read-to-close body (no length, Connection: close)', async () => {
206
+ const body = enc('HTTP/1.1 200 OK\r\n' +
207
+ 'Content-Type: application/json\r\n' +
208
+ 'Connection: close\r\n' +
209
+ '\r\n' +
210
+ PAYLOAD);
211
+ const srv = startRawServer(body);
212
+ try {
213
+ const res = await fetchFn(srv.base);
214
+ const text = await res.text();
215
+ expect(text).toBe(PAYLOAD);
216
+ }
217
+ finally {
218
+ srv.stop();
219
+ }
220
+ });
221
+ await it('decodes a clean gzip body (Content-Encoding: gzip, chunked)', async () => {
222
+ const gz = gzip(enc(PAYLOAD));
223
+ const body = concatBytes(enc('HTTP/1.1 200 OK\r\n' +
224
+ 'Content-Type: application/json\r\n' +
225
+ 'Content-Encoding: gzip\r\n' +
226
+ 'Transfer-Encoding: chunked\r\n' +
227
+ '\r\n' +
228
+ gz.length.toString(16) +
229
+ '\r\n'), gz, enc('\r\n0\r\n\r\n'));
230
+ const srv = startRawServer(body);
231
+ try {
232
+ const res = await fetchFn(srv.base);
233
+ const text = await res.text();
234
+ expect(text).toBe(PAYLOAD);
235
+ }
236
+ finally {
237
+ srv.stop();
238
+ }
239
+ });
240
+ await it('decodes a gzip body that spans multiple read chunks', async () => {
241
+ // The primary Norman shape: a complete, well-framed gzip response
242
+ // whose compressed size (> 4 KB) forces the body reader to deliver
243
+ // it in several chunks. Per-chunk decompression throws
244
+ // "Ungültige komprimierte Daten" / "Weitere Eingaben erforderlich"
245
+ // here; the streaming decode must reassemble the full payload.
246
+ const gz = gzip(enc(BIG_PAYLOAD));
247
+ expect(gz.length > 4096).toBe(true); // guard: actually multi-chunk
248
+ const body = concatBytes(enc('HTTP/1.1 200 OK\r\n' +
249
+ 'Content-Type: application/json\r\n' +
250
+ 'Content-Encoding: gzip\r\n' +
251
+ 'Content-Length: ' +
252
+ gz.length +
253
+ '\r\n\r\n'), gz);
254
+ const srv = startRawServer(body);
255
+ try {
256
+ const res = await fetchFn(srv.base);
257
+ const text = await res.text();
258
+ expect(text.length).toBe(BIG_PAYLOAD.length);
259
+ expect(text).toBe(BIG_PAYLOAD);
260
+ }
261
+ finally {
262
+ srv.stop();
263
+ }
264
+ });
265
+ await it('decodes a deflate body that spans multiple read chunks', async () => {
266
+ // Same multi-chunk reassembly for Content-Encoding: deflate.
267
+ const df = deflate(enc(BIG_PAYLOAD));
268
+ expect(df.length > 4096).toBe(true);
269
+ const body = concatBytes(enc('HTTP/1.1 200 OK\r\n' +
270
+ 'Content-Type: application/json\r\n' +
271
+ 'Content-Encoding: deflate\r\n' +
272
+ 'Content-Length: ' +
273
+ df.length +
274
+ '\r\n\r\n'), df);
275
+ const srv = startRawServer(body);
276
+ try {
277
+ const res = await fetchFn(srv.base);
278
+ const text = await res.text();
279
+ expect(text).toBe(BIG_PAYLOAD);
280
+ }
281
+ finally {
282
+ srv.stop();
283
+ }
284
+ });
285
+ });
286
+ });
287
+ };
package/lib/request.js CHANGED
@@ -164,7 +164,14 @@ export class Request extends Body {
164
164
  return this[INTERNALS].parsedURL.toString();
165
165
  }
166
166
  get _uri() {
167
- return GLib.Uri.parse(this.url, GLib.UriFlags.NONE);
167
+ // ENCODED, not NONE: `this.url` (a WHATWG-serialized URL) is already
168
+ // percent-encoded, so re-parsing with NONE would DECODE it a second
169
+ // time — turning an intentionally-escaped `%2F` in a path segment back
170
+ // into a literal `/`. That breaks scoped-package registry routes like
171
+ // `PUT /@scope%2Fname` (npm's package-create + OIDC token-exchange
172
+ // endpoints 404 on the decoded `/@scope/name` form). ENCODED preserves
173
+ // the existing escaping on round-trip. See request.ts uri construction.
174
+ return GLib.Uri.parse(this.url, GLib.UriFlags.ENCODED);
168
175
  }
169
176
  get _session() {
170
177
  return this[INTERNALS].session;
@@ -258,7 +265,18 @@ export class Request extends Body {
258
265
  session = getSharedSession();
259
266
  message = new Soup.Message({
260
267
  method,
261
- uri: GLib.Uri.parse(parsedURL.toString(), GLib.UriFlags.NONE),
268
+ // ENCODED, not NONE: `parsedURL.toString()` is already a
269
+ // percent-encoded WHATWG URL. Parsing with NONE decodes it a
270
+ // second time, collapsing an escaped `%2F` in a path segment to
271
+ // a literal `/` — which sends `PUT /@scope/name` instead of the
272
+ // required `PUT /@scope%2Fname`. npm's registry tolerates the
273
+ // literal slash when UPDATING an existing package but 404s on
274
+ // the package-CREATE route and the OIDC token-exchange endpoint
275
+ // (`/-/npm/v1/oidc/token/exchange/package/@scope%2Fname`), which
276
+ // broke first-publish of new scoped packages + Trusted-Publisher
277
+ // OIDC auth once the CLI began running under GJS. ENCODED keeps
278
+ // the escaping intact on the wire.
279
+ uri: GLib.Uri.parse(parsedURL.toString(), GLib.UriFlags.ENCODED),
262
280
  });
263
281
  }
264
282
  this[INTERNALS] = {
@@ -114,5 +114,23 @@ export default async () => {
114
114
  expect(stderr.includes('libsoup-CRITICAL')).toBe(false);
115
115
  });
116
116
  });
117
+ // Regression: @gjsify/fetch re-parsed the already-encoded request URL
118
+ // with `GLib.UriFlags.NONE`, which DECODES a second time — collapsing an
119
+ // escaped `%2F` inside a path segment back to a literal `/`. That sent
120
+ // `PUT /@scope/name` instead of the required `PUT /@scope%2Fname`, so
121
+ // npm's package-CREATE route and the OIDC token-exchange endpoint
122
+ // (`/-/npm/v1/oidc/token/exchange/package/@scope%2Fname`) 404'd — only
123
+ // visible once the CLI began running under GJS (the Node path preserved
124
+ // it). Fix: parse with `GLib.UriFlags.ENCODED`. See request.ts `_uri` +
125
+ // Soup.Message construction.
126
+ await describe('@gjsify/fetch — percent-encoded path segments (scoped-registry %2F)', async () => {
127
+ await it('preserves %2F in the request URI instead of decoding it to a literal /', () => {
128
+ const req = new RequestCtor('https://registry.npmjs.org/@gjsify%2Ffetch');
129
+ const uriStr = req._uri.to_string();
130
+ // %2F (any hex case) must survive; the decoded literal-slash form must NOT appear.
131
+ expect(/@gjsify%2[Ff]fetch/.test(uriStr)).toBe(true);
132
+ expect(/@gjsify\/fetch/.test(uriStr)).toBe(false);
133
+ });
134
+ });
117
135
  });
118
136
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjsify/fetch",
3
- "version": "0.4.44",
3
+ "version": "0.5.1",
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",
@@ -51,9 +51,9 @@
51
51
  "fetch"
52
52
  ],
53
53
  "devDependencies": {
54
- "@gjsify/cli": "^0.4.44",
55
- "@gjsify/unit": "^0.4.44",
56
- "@types/node": "^25.9.1",
54
+ "@gjsify/cli": "^0.5.1",
55
+ "@gjsify/unit": "^0.5.1",
56
+ "@types/node": "^25.9.2",
57
57
  "typescript": "^6.0.3"
58
58
  },
59
59
  "dependencies": {
@@ -61,10 +61,10 @@
61
61
  "@girs/gjs": "4.0.4",
62
62
  "@girs/glib-2.0": "2.88.0-4.0.4",
63
63
  "@girs/soup-3.0": "3.6.6-4.0.4",
64
- "@gjsify/formdata": "^0.4.44",
65
- "@gjsify/http": "^0.4.44",
66
- "@gjsify/url": "^0.4.44",
67
- "@gjsify/utils": "^0.4.44"
64
+ "@gjsify/formdata": "^0.5.1",
65
+ "@gjsify/http": "^0.5.1",
66
+ "@gjsify/url": "^0.5.1",
67
+ "@gjsify/utils": "^0.5.1"
68
68
  },
69
69
  "gjsify": {
70
70
  "runtimes": {