feat: add stream support

This commit is contained in:
adrieljss
2025-03-02 23:07:44 +08:00
parent 643039c17e
commit 5edea81680
7 changed files with 54 additions and 86 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "tauri-plugin-http"
version = "2.4.0"
version = "2.3.0"
description = "Access an HTTP client written in Rust."
edition = { workspace = true }
authors = { workspace = true }
+22 -35
View File
@@ -26,7 +26,7 @@
* @module
*/
import { Channel, invoke } from '@tauri-apps/api/core'
import { invoke } from '@tauri-apps/api/core'
/**
* Configuration of a proxy that a Client should pass requests to.
@@ -106,20 +106,6 @@ export interface DangerousSettings {
acceptInvalidHostnames?: boolean
}
/**
* Stream Packet for IPC
*/
export interface StreamMessage {
/**
* The chunk - an array of bytes sent from Rust.
*/
value?: ArrayBuffer | number[]
/**
* Is the stream done.
*/
done: boolean
}
const ERROR_REQUEST_CANCELLED = 'Request canceled'
/**
@@ -200,19 +186,6 @@ export async function fetch(
throw new Error(ERROR_REQUEST_CANCELLED)
}
const streamChannel = new Channel<StreamMessage>()
const readableStreamBody = new ReadableStream({
start: (controller) => {
streamChannel.onmessage = (res: StreamMessage) => {
// close early if aborted
if (signal?.aborted) controller.error(ERROR_REQUEST_CANCELLED)
if (res.done) controller.close()
controller.enqueue(res.value)
}
}
})
const rid = await invoke<number>('plugin:http|fetch', {
clientConfig: {
method: req.method,
@@ -223,8 +196,7 @@ export async function fetch(
connectTimeout,
proxy,
danger
},
streamChannel
}
})
const abort = () => invoke('plugin:http|fetch_cancel', { rid })
@@ -251,15 +223,30 @@ export async function fetch(
status,
statusText,
url,
headers: responseHeaders
headers: responseHeaders,
rid: responseRid
} = await invoke<FetchSendResponse>('plugin:http|fetch_send', {
rid
})
const res = new Response(readableStreamBody, {
status,
statusText
})
const body = await invoke<ArrayBuffer | number[]>(
'plugin:http|fetch_read_body',
{
rid: responseRid
}
)
const res = new Response(
body instanceof ArrayBuffer && body.byteLength !== 0
? body
: body instanceof Array && body.length > 0
? new Uint8Array(body)
: null,
{
status,
statusText
}
)
// url and headers are read only properties
// but seems like we can set them like this
+2 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@tauri-apps/plugin-http",
"version": "2.4.0",
"version": "2.3.0",
"license": "MIT OR Apache-2.0",
"authors": [
"Tauri Programme within The Commons Conservancy"
@@ -24,7 +24,6 @@
"LICENSE"
],
"dependencies": {
"@tauri-apps/api": "^2.0.0",
"@tauri-apps/plugin-http": "link:"
"@tauri-apps/api": "^2.0.0"
}
}
+15 -29
View File
@@ -2,7 +2,6 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::{future::Future, pin::Pin, str::FromStr, sync::Arc, time::Duration};
use http::{header, HeaderMap, HeaderName, HeaderValue, Method, StatusCode};
@@ -11,7 +10,7 @@ use serde::{Deserialize, Serialize};
use tauri::{
async_runtime::Mutex,
command,
ipc::{Channel, CommandScope, GlobalScope},
ipc::{CommandScope, GlobalScope},
Manager, ResourceId, ResourceTable, Runtime, State, Webview,
};
use tokio::sync::oneshot::{channel, Receiver, Sender};
@@ -23,8 +22,6 @@ use crate::{
const HTTP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),);
// reqwest::Response is never read, but might be needed for future use.
#[allow(dead_code)]
struct ReqwestResponse(reqwest::Response);
impl tauri::Resource for ReqwestResponse {}
@@ -129,12 +126,6 @@ pub struct BasicAuth {
password: String,
}
#[derive(Clone, Serialize)]
pub struct StreamMessage {
value: Option<Vec<u8>>,
done: bool,
}
#[inline]
fn proxy_creator(
url_or_config: UrlOrConfig,
@@ -190,7 +181,6 @@ pub async fn fetch<R: Runtime>(
client_config: ClientConfig,
command_scope: CommandScope<Entry>,
global_scope: GlobalScope<Entry>,
stream_channel: Channel<StreamMessage>
) -> crate::Result<ResourceId> {
let ClientConfig {
method,
@@ -324,24 +314,7 @@ pub async fn fetch<R: Runtime>(
#[cfg(feature = "tracing")]
tracing::trace!("{:?}", request);
let fut = async move {
let mut res = request.send().await?;
// send response through IPC channel
while let Some(chunk) = res.chunk().await? {
stream_channel.send(StreamMessage{
value: Some(chunk.to_vec()),
done: false,
})?;
}
stream_channel.send(StreamMessage { value: None, done: true })?;
// return that response
Ok(res)
};
let fut = async move { request.send().await.map_err(Into::into) };
let mut resources_table = webview.resources_table();
let rid = resources_table.add_request(Box::pin(fut));
@@ -437,6 +410,19 @@ pub async fn fetch_send<R: Runtime>(
})
}
#[tauri::command]
pub(crate) async fn fetch_read_body<R: Runtime>(
webview: Webview<R>,
rid: ResourceId,
) -> crate::Result<tauri::ipc::Response> {
let res = {
let mut resources_table = webview.resources_table();
resources_table.take::<ReqwestResponse>(rid)?
};
let res = Arc::into_inner(res).unwrap().0;
Ok(tauri::ipc::Response::new(res.bytes().await?.to_vec()))
}
// forbidden headers per fetch spec https://fetch.spec.whatwg.org/#terminology-headers
#[cfg(not(feature = "unsafe-headers"))]
fn is_unsafe_header(header: &HeaderName) -> bool {
+2 -1
View File
@@ -36,7 +36,8 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
.invoke_handler(tauri::generate_handler![
commands::fetch,
commands::fetch_cancel,
commands::fetch_send
commands::fetch_send,
commands::fetch_read_body,
])
.build()
}