feat(http): send fetch request bodies as raw IPC payloads (#3732)

This commit is contained in:
Den Ilin authored and GitHub committed 2026-11-04 19:01:53 +02:00
1 parent cd94082746
commit 2fbd563427
6 files changed
+52 -21

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
---
"http": minor:feat
"http-js": minor:feat
---
Improve `fetch` request by avoiding serialization when possible while handling the request body.
Generated
+1
View File
@@ -6897,6 +6897,7 @@ dependencies = [
"cookie_store",
"data-url",
"http",
"percent-encoding",
"regex",
"reqwest 0.12.28",
"schemars",
+1
View File
@@ -45,6 +45,7 @@ http = "1"
reqwest = { version = "0.12.16", default-features = false }
url = { workspace = true }
data-url = "0.3"
percent-encoding = "2"
cookie_store = { version = "0.22", optional = true, features = ["serde"] }
bytes = { version = "1.9", optional = true }
tracing = { workspace = true, optional = true }
+1 -1
View File
@@ -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(),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 _=(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=await t.invoke("plugin:http|fetch",{clientConfig:{method:l.method,url:l.url,headers:_,data:h,maxRedirections:o,connectTimeout:i,proxy:s,danger:c}}),f=()=>t.invoke("plugin:http|fetch_cancel",{rid:w}).catch(()=>{});if(a?.aborted)throw f(),new Error(r);a?.addEventListener("abort",()=>{f()});const{status:y,statusText:p,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(y)?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()}}),A=new Response(R,{status:y,statusText:p});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}({},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 _={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__})}
+18 -13
View File
@@ -178,8 +178,6 @@ export async function fetch(
const req = new Request(input, init)
const buffer = await req.arrayBuffer()
const data =
buffer.byteLength !== 0 ? Array.from(new Uint8Array(buffer)) : null
// append new headers created by the browser `Request` implementation,
// if not already declared by the caller of this function
@@ -211,18 +209,25 @@ export async function fetch(
throw new Error(ERROR_REQUEST_CANCELLED)
}
const rid = await invoke<number>('plugin:http|fetch', {
clientConfig: {
method: req.method,
url: req.url,
headers: mappedHeaders,
data,
maxRedirections,
connectTimeout,
proxy,
danger
const clientConfig = {
method: req.method,
url: req.url,
headers: mappedHeaders,
maxRedirections,
connectTimeout,
proxy,
danger
}
const rid = await invoke<number>(
'plugin:http|fetch',
new Uint8Array(buffer),
{
headers: {
'client-config': encodeURIComponent(JSON.stringify(clientConfig))
}
}
})
)
const abort = () =>
invoke('plugin:http|fetch_cancel', { rid }).catch(() => {})
+25 -7
View File
@@ -2,16 +2,16 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{future::Future, pin::Pin, str::FromStr, sync::Arc, time::Duration};
use std::{borrow::Cow, future::Future, pin::Pin, str::FromStr, sync::Arc, time::Duration};
use http::{HeaderMap, HeaderName, HeaderValue, Method, StatusCode, header};
use reqwest::{NoProxy, redirect::Policy};
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Serialize, de::Error as _};
use tauri::{
Manager, ResourceId, ResourceTable, Runtime, State, Webview,
async_runtime::Mutex,
command,
ipc::{CommandScope, GlobalScope},
ipc::{CommandScope, GlobalScope, InvokeBody},
};
use tokio::sync::oneshot::{Receiver, Sender, channel};
@@ -93,7 +93,6 @@ pub struct ClientConfig {
method: String,
url: url::Url,
headers: Vec<(String, String)>,
data: Option<Vec<u8>>,
connect_timeout: Option<u64>,
max_redirections: Option<usize>,
proxy: Option<Proxy>,
@@ -228,11 +227,23 @@ fn map_request_error(error: reqwest::Error) -> Error {
Error::Network(error)
}
/// IPC header carrying the percent-encoded JSON [`ClientConfig`].
const CLIENT_CONFIG_HEADER: &str = "client-config";
/// Reads the options from the IPC header.
fn read_client_config(headers: &HeaderMap) -> serde_json::Result<ClientConfig> {
let header = headers
.get(CLIENT_CONFIG_HEADER)
.ok_or_else(|| serde_json::Error::custom("missing client-config header"))?;
let header: Cow<'_, [u8]> = percent_encoding::percent_decode(header.as_bytes()).into();
serde_json::from_slice(&header)
}
#[command]
pub async fn fetch<R: Runtime>(
webview: Webview<R>,
state: State<'_, Http>,
client_config: ClientConfig,
request: tauri::ipc::Request<'_>,
command_scope: CommandScope<Entry>,
global_scope: GlobalScope<Entry>,
) -> crate::Result<ResourceId> {
@@ -240,12 +251,19 @@ pub async fn fetch<R: Runtime>(
method,
url,
headers: headers_raw,
data,
connect_timeout,
max_redirections,
proxy,
danger,
} = client_config;
} = read_client_config(request.headers())?;
// Even though JS sends a raw payload, Android and the `postMessage` fallback deliver it
// as a JSON array of numbers.
let data = match request.body() {
InvokeBody::Raw(data) => data.clone(),
InvokeBody::Json(data) => Vec::<u8>::deserialize(data)?,
};
let data = if data.is_empty() { None } else { Some(data) };
let scheme = url.scheme();
let method = Method::from_bytes(method.as_bytes())?;