mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-11-11 23:29:55 +01:00
fix(http): release resources for null-body responses (#3756)
This commit is contained in:
1 parent
2fbd563427
commit
b11126eae6
7 files changed
+73
-2
No files matched your search
@@ -0,0 +1,6 @@
|
||||
---
|
||||
http: patch
|
||||
http-js: patch
|
||||
---
|
||||
|
||||
Release native HTTP response resources for responses with no body, such as `204 No Content`, even when the response is never read.
|
||||
@@ -3,7 +3,7 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
use serde::Deserialize;
|
||||
use tauri::command;
|
||||
use tauri::{Manager, command};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[allow(unused)]
|
||||
@@ -22,3 +22,13 @@ pub fn perform_request(endpoint: String, body: RequestBody) -> String {
|
||||
println!("{} {:?}", endpoint, body);
|
||||
"message response".into()
|
||||
}
|
||||
|
||||
// Used by e2e tests to check that plugin operations release their native resources.
|
||||
#[command]
|
||||
pub fn webview_resource_names(webview: tauri::Webview) -> Vec<String> {
|
||||
webview
|
||||
.resources_table()
|
||||
.names()
|
||||
.map(|(_, name)| name.into_owned())
|
||||
.collect()
|
||||
}
|
||||
@@ -196,6 +196,7 @@ pub fn run() {
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
cmd::log_operation,
|
||||
cmd::perform_request,
|
||||
cmd::webview_resource_names,
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application");
|
||||
|
||||
@@ -55,6 +55,7 @@ export interface FixtureServer {
|
||||
* per byte, so `headers` shows the exact bytes the client sent.
|
||||
* - `GET /raw-header` — an empty response whose `content-disposition` names
|
||||
* {@link RAW_HEADER_FILENAME} in raw UTF-8 bytes.
|
||||
* - `GET /empty/{204,205,304}` — a response with no body and the requested status.
|
||||
* - `ws /ws` — a WebSocket echo endpoint: text and binary messages are sent
|
||||
* back as-is, and {@link WEBSOCKET_CLOSE_REQUEST} makes the server close the
|
||||
* connection with {@link WEBSOCKET_CLOSE_CODE}. On `/ws/headers` the server
|
||||
@@ -71,6 +72,14 @@ export function startFixtureServer(): Promise<FixtureServer> {
|
||||
const url = new URL(req.url ?? '/', FIXTURE_SERVER_URL)
|
||||
const [, route, ...rest] = url.pathname.split('/')
|
||||
|
||||
if (route === 'empty' && req.method === 'GET') {
|
||||
const status = Number(rest[0])
|
||||
if ([204, 205, 304].includes(status)) {
|
||||
res.writeHead(status, { 'x-e2e-empty': 'present' }).end()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (route === 'updater' && req.method === 'GET') {
|
||||
const [target] = rest
|
||||
if (target === UPDATER_TARGET_NO_UPDATE) {
|
||||
|
||||
@@ -13,6 +13,46 @@ import { FIXTURE_SERVER_URL, RAW_HEADER_FILENAME } from '../helpers/server.js'
|
||||
const echoServer = 'http://localhost:3003'
|
||||
|
||||
describePlugin('http', () => {
|
||||
for (const status of [204, 205, 304]) {
|
||||
for (const readBody of [false, true]) {
|
||||
it(`fetch releases a ${status} response ${readBody ? 'after reading it' : 'without reading it'}`, async () => {
|
||||
const result = await tauri(
|
||||
async (api, url, status, readBody) => {
|
||||
const resourceNames = () =>
|
||||
api.core.invoke<string[]>('webview_resource_names')
|
||||
const before = await resourceNames()
|
||||
const response = await api.http.fetch(`${url}/empty/${status}`)
|
||||
const bytes = readBody
|
||||
? (await response.arrayBuffer()).byteLength
|
||||
: null
|
||||
const after = await resourceNames()
|
||||
const responseCount = (names: string[]) =>
|
||||
names.filter((name) => name.endsWith('::ReqwestResponse')).length
|
||||
return {
|
||||
status: response.status,
|
||||
bodyIsNull: response.body === null,
|
||||
bytes,
|
||||
url: response.url,
|
||||
header: response.headers.get('x-e2e-empty'),
|
||||
// Reading an empty JS body succeeds even if its Rust resource leaked.
|
||||
// Inspect the real native resource table instead.
|
||||
retained: responseCount(after) - responseCount(before)
|
||||
}
|
||||
},
|
||||
FIXTURE_SERVER_URL,
|
||||
status,
|
||||
readBody
|
||||
)
|
||||
expect(result.status).toBe(status)
|
||||
expect(result.bodyIsNull).toBe(true)
|
||||
expect(result.bytes).toBe(readBody ? 0 : null)
|
||||
expect(result.url).toBe(`${FIXTURE_SERVER_URL}/empty/${status}`)
|
||||
expect(result.header).toBe('present')
|
||||
expect(result.retained).toBe(0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
it('fetch performs a GET and exposes status, url and headers', async () => {
|
||||
const response = await tauri(async (api, url) => {
|
||||
const response = await api.http.fetch(url, {
|
||||
|
||||
@@ -1 +1 @@
|
||||
if("__TAURI__"in window){var __TAURI_PLUGIN_HTTP__=function(e,t){"use strict";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,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();for(const[e,t]of l.headers)d.get(e)||d.set(e,t);const h=(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 _={method:l.method,url:l.url,headers:h,maxRedirections:o,connectTimeout:i,proxy:s,danger:c},w=await t.invoke("plugin:http|fetch",new Uint8Array(u),{headers:{"client-config":encodeURIComponent(JSON.stringify(_))}}),f=()=>t.invoke("plugin:http|fetch_cancel",{rid:w}).catch(()=>{});if(a?.aborted)throw f(),new Error(r);a?.addEventListener("abort",()=>{f()});const{status:p,statusText:y,url:b,headers:g,rid:v}=await t.invoke("plugin:http|fetch_send",{rid:w});let T=!1;const m=()=>T?Promise.resolve():(T=!0,t.invoke("plugin:http|fetch_cancel_body",{rid:v}).catch(()=>{})),R=[101,103,204,205,304].includes(p)?null:new ReadableStream({start:e=>{a?.addEventListener("abort",()=>{e.error(r),m()})},pull:e=>(async e=>{let r;try{r=await t.invoke("plugin:http|fetch_read_body",{rid:v})}catch(t){return e.error(t),void m()}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:()=>{m()}}),P=new Response(R,{status:p,statusText:y});Object.defineProperty(P,"url",{value:b,writable:!1}),Object.defineProperty(P,"headers",{value:new Headers(g),writable:!1});const A=P.clone.bind(P);return Object.defineProperty(P,"clone",{value:()=>{const e=A();return Object.defineProperty(e,"url",{value:b,writable:!1}),Object.defineProperty(e,"headers",{value:new Headers(g),writable:!1}),e}}),P},e}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_PLUGIN_HTTP__})}
|
||||
if("__TAURI__"in window){var __TAURI_PLUGIN_HTTP__=function(e,t){"use strict";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,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();for(const[e,t]of l.headers)d.get(e)||d.set(e,t);const h=(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 w={method:l.method,url:l.url,headers:h,maxRedirections:o,connectTimeout:i,proxy:s,danger:c},_=await t.invoke("plugin:http|fetch",new Uint8Array(u),{headers:{"client-config":encodeURIComponent(JSON.stringify(w))}}),f=()=>t.invoke("plugin:http|fetch_cancel",{rid:_}).catch(()=>{});if(a?.aborted)throw f(),new Error(r);a?.addEventListener("abort",()=>{f()});const{status:p,statusText:y,url:b,headers:g,rid:v}=await t.invoke("plugin:http|fetch_send",{rid:_});let T=!1;const m=()=>T?Promise.resolve():(T=!0,t.invoke("plugin:http|fetch_cancel_body",{rid:v}).catch(()=>{})),R=[101,103,204,205,304].includes(p)?null:new ReadableStream({start:e=>{a?.addEventListener("abort",()=>{e.error(r),m()})},pull:e=>(async e=>{let r;try{r=await t.invoke("plugin:http|fetch_read_body",{rid:v})}catch(t){return e.error(t),void m()}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:()=>{m()}});null===R&&await m();const P=new Response(R,{status:p,statusText:y});Object.defineProperty(P,"url",{value:b,writable:!1}),Object.defineProperty(P,"headers",{value:new Headers(g),writable:!1});const A=P.clone.bind(P);return Object.defineProperty(P,"clone",{value:()=>{const e=A();return Object.defineProperty(e,"url",{value:b,writable:!1}),Object.defineProperty(e,"headers",{value:new Headers(g),writable:!1}),e}}),P},e}({},window.__TAURI__.core);Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_PLUGIN_HTTP__})}
|
||||
@@ -317,6 +317,11 @@ export async function fetch(
|
||||
}
|
||||
})
|
||||
|
||||
if (body === null) {
|
||||
// No stream will read or cancel this response's Rust resource.
|
||||
await dropBody()
|
||||
}
|
||||
|
||||
const res = new Response(body, {
|
||||
status,
|
||||
statusText
|
||||
|
||||
Reference in new issue
Block a user