mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-11-07 23:18:14 +01:00
feat(http): send fetch request bodies as raw IPC payloads (#3732)
This commit is contained in:
1 parent
cd94082746
commit
2fbd563427
6 files changed
+52
-21
No files matched your search
@@ -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
@@ -6897,6 +6897,7 @@ dependencies = [
|
||||
"cookie_store",
|
||||
"data-url",
|
||||
"http",
|
||||
"percent-encoding",
|
||||
"regex",
|
||||
"reqwest 0.12.28",
|
||||
"schemars",
|
||||
|
||||
@@ -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 @@
|
||||
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__})}
|
||||
@@ -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(() => {})
|
||||
|
||||
@@ -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())?;
|
||||
|
||||
Reference in new issue
Block a user