fix(http): stop unhandled rejections from the fetch cleanup path (#3566)

* fix(http): stop unhandled rejections from the fetch cleanup path

The request/body cleanup commands are fired as floating promises, and the
Rust side releases a resource only once: fetch_cancel_body is
resources_table.close(rid)?, and fetch_read_body also closes the rid at
end-of-body. So every release after the first rejects with BadResourceId
into a promise nobody is listening to.

Make dropBody idempotent and let both cleanup calls handle their own
rejection.

* chore: add changefile

* chore(http): rebuild api-iife.js
This commit is contained in:
Den Ilin
2026-09-03 21:00:34 +03:00
committed by GitHub
parent 845d8989cb
commit a21555ddd2
3 changed files with 17 additions and 6 deletions
@@ -0,0 +1,6 @@
---
"http": patch
"http-js": patch
---
Fix unhandled promise rejections on every `fetch` teardown: the request/body cleanup commands were fired as floating promises, and releasing an already-released resource rejects with `The resource id N is invalid.`. `dropBody` is now idempotent and both cleanup calls handle their own rejection.
+1 -1
View File
@@ -1 +1 @@
if("__TAURI__"in window){var __TAURI_PLUGIN_HTTP__=function(e){"use strict";async function t(e,t={},r){return window.__TAURI_INTERNALS__.invoke(e,t,r)}"function"==typeof SuppressedError&&SuppressedError;const r="Request cancelled";return e.fetch=async function(e,n){const a=n?.signal;if(a?.aborted)throw new Error(r);const o=n?.maxRedirections,i=n?.connectTimeout,s=n?.proxy,d=n?.danger;n&&(delete n.maxRedirections,delete n.connectTimeout,delete n.proxy,delete n.danger);const c=n?.headers?n.headers instanceof Headers?n.headers:new Headers(n.headers):new Headers,l=new Request(e,n),u=await l.arrayBuffer(),f=0!==u.byteLength?Array.from(new Uint8Array(u)):null;for(const[e,t]of l.headers)c.get(e)||c.set(e,t);const _=(c instanceof Headers?Array.from(c.entries()):Array.isArray(c)?c:Object.entries(c)).map(([e,t])=>[e,"string"==typeof t?t:t.toString()]);if(a?.aborted)throw new Error(r);const h=await t("plugin:http|fetch",{clientConfig:{method:l.method,url:l.url,headers:_,data:f,maxRedirections:o,connectTimeout:i,proxy:s,danger:d}}),p=()=>t("plugin:http|fetch_cancel",{rid:h});if(a?.aborted)throw p(),new Error(r);a?.addEventListener("abort",()=>{p()});const{status:w,statusText:y,url:b,headers:g,rid:T}=await t("plugin:http|fetch_send",{rid:h}),R=()=>t("plugin:http|fetch_cancel_body",{rid:T}),m=[101,103,204,205,304].includes(w)?null:new ReadableStream({start:e=>{a?.addEventListener("abort",()=>{e.error(r),R()})},pull:e=>(async e=>{let r;try{r=await t("plugin:http|fetch_read_body",{rid:T})}catch(t){return e.error(t),void R()}const n=new Uint8Array(r),a=n[n.byteLength-1],o=n.slice(0,n.byteLength-1);1!==a?e.enqueue(o):e.close()})(e),cancel:()=>{R()}}),A=new Response(m,{status:w,statusText:y});Object.defineProperty(A,"url",{value:b,writable:!1}),Object.defineProperty(A,"headers",{value:new Headers(g),writable:!1});const v=A.clone.bind(A);return Object.defineProperty(A,"clone",{value:()=>{const e=v();return Object.defineProperty(e,"url",{value:b,writable:!1}),Object.defineProperty(e,"headers",{value:new Headers(g),writable:!1}),e}}),A},e}({});Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_PLUGIN_HTTP__})}
if("__TAURI__"in window){var __TAURI_PLUGIN_HTTP__=function(e){"use strict";async function t(e,t={},r){return window.__TAURI_INTERNALS__.invoke(e,t,r)}"function"==typeof SuppressedError&&SuppressedError;const r="Request cancelled";return e.fetch=async function(e,n){const a=n?.signal;if(a?.aborted)throw new Error(r);const o=n?.maxRedirections,s=n?.connectTimeout,i=n?.proxy,c=n?.danger;n&&(delete n.maxRedirections,delete n.connectTimeout,delete n.proxy,delete n.danger);const d=n?.headers?n.headers instanceof Headers?n.headers:new Headers(n.headers):new Headers,l=new Request(e,n),u=await l.arrayBuffer(),h=0!==u.byteLength?Array.from(new Uint8Array(u)):null;for(const[e,t]of l.headers)d.get(e)||d.set(e,t);const f=(d instanceof Headers?Array.from(d.entries()):Array.isArray(d)?d:Object.entries(d)).map(([e,t])=>[e,"string"==typeof t?t:t.toString()]);if(a?.aborted)throw new Error(r);const _=await t("plugin:http|fetch",{clientConfig:{method:l.method,url:l.url,headers:f,data:h,maxRedirections:o,connectTimeout:s,proxy:i,danger:c}}),p=()=>t("plugin:http|fetch_cancel",{rid:_}).catch(()=>{});if(a?.aborted)throw p(),new Error(r);a?.addEventListener("abort",()=>{p()});const{status:w,statusText:y,url:b,headers:g,rid:T}=await t("plugin:http|fetch_send",{rid:_});let m=!1;const R=()=>m?Promise.resolve():(m=!0,t("plugin:http|fetch_cancel_body",{rid:T}).catch(()=>{})),v=[101,103,204,205,304].includes(w)?null:new ReadableStream({start:e=>{a?.addEventListener("abort",()=>{e.error(r),R()})},pull:e=>(async e=>{let r;try{r=await t("plugin:http|fetch_read_body",{rid:T})}catch(t){return e.error(t),void R()}const n=new Uint8Array(r),a=n[n.byteLength-1],o=n.slice(0,n.byteLength-1);1!==a?e.enqueue(o):e.close()})(e),cancel:()=>{R()}}),A=new Response(v,{status:w,statusText:y});Object.defineProperty(A,"url",{value:b,writable:!1}),Object.defineProperty(A,"headers",{value:new Headers(g),writable:!1});const P=A.clone.bind(A);return Object.defineProperty(A,"clone",{value:()=>{const e=P();return Object.defineProperty(e,"url",{value:b,writable:!1}),Object.defineProperty(e,"headers",{value:new Headers(g),writable:!1}),e}}),A},e}({});Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_PLUGIN_HTTP__})}
+10 -5
View File
@@ -199,14 +199,14 @@ export async function fetch(
}
})
const abort = () => invoke('plugin:http|fetch_cancel', { rid })
const abort = () =>
invoke('plugin:http|fetch_cancel', { rid }).catch(() => {})
// Optimistically check for abort signal
// and avoid doing any work after doing intial work on the Rust side
if (signal?.aborted) {
// we don't care about the result of this proimse
// eslint-disable-next-line @typescript-eslint/no-floating-promises
abort()
// we don't care about the result of this promise
void abort()
throw new Error(ERROR_REQUEST_CANCELLED)
}
@@ -230,8 +230,13 @@ export async function fetch(
rid
})
let bodyDropped = false
const dropBody = () => {
return invoke('plugin:http|fetch_cancel_body', { rid: responseRid })
if (bodyDropped) return Promise.resolve()
bodyDropped = true
return invoke('plugin:http|fetch_cancel_body', { rid: responseRid }).catch(
() => {}
)
}
const readChunk = async (