From 4bc57234f5f5840adb01f246ac979278d1632996 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Wed, 2 Aug 2023 12:05:17 -0300 Subject: [PATCH] body on its own command --- examples/api/src/views/Http.svelte | 34 +++++++++---- plugins/http/guest-js/index.ts | 56 ++++++++++++++++++--- plugins/http/src/api-iife.js | 2 +- plugins/http/src/commands.rs | 78 ++++++++++++++++++++++++++---- plugins/http/src/lib.rs | 16 ++---- 5 files changed, 149 insertions(+), 37 deletions(-) diff --git a/examples/api/src/views/Http.svelte b/examples/api/src/views/Http.svelte index b17559e2..43758b50 100644 --- a/examples/api/src/views/Http.svelte +++ b/examples/api/src/views/Http.svelte @@ -12,20 +12,33 @@ const options = { method: method || "GET", + headers: {}, }; - if ( - (httpBody.startsWith("{") && httpBody.endsWith("}")) || - (httpBody.startsWith("[") && httpBody.endsWith("]")) - ) { - options.body = JSON.parse(httpBody); - } else if (httpBody !== "") { + let bodyType; + + if (method !== "GET") { options.body = httpBody; + + if ( + (httpBody.startsWith("{") && httpBody.endsWith("}")) || + (httpBody.startsWith("[") && httpBody.endsWith("]")) + ) { + options.headers["Content-Type"] = "application/json"; + bodyType = "json"; + } else if (httpBody !== "") { + bodyType = "text"; + } } - tauriFetch("http://localhost:3003", options) - .then(onMessage) - .catch(onMessage); + const response = await tauriFetch("http://localhost:3003", options); + const body = + bodyType === "json" ? await response.json() : await response.text(); + onMessage({ + url: response.url, + status: response.status, + body, + }); } /// http form @@ -46,9 +59,10 @@ : undefined, }); result = { + url: response.url, status: response.status, headers: JSON.parse(JSON.stringify(response.headers)), - body: response.body, + body: await response.text(), }; } diff --git a/plugins/http/guest-js/index.ts b/plugins/http/guest-js/index.ts index 8c1ea94e..9c88e280 100644 --- a/plugins/http/guest-js/index.ts +++ b/plugins/http/guest-js/index.ts @@ -30,6 +30,51 @@ declare global { } } +async function readBody(rid: number, kind: "blob" | "text"): Promise { + return await window.__TAURI_INVOKE__("plugin:http|fetch_read_body", { + rid, + kind + }); +} + +class TauriResponse extends Response { + _rid: number = 0 + + blob(): Promise { + return readBody(this._rid, "blob").then(bytes => new Blob([bytes], { type: this.headers.get("content-type") || "application/octet-stream" })) + } + + json(): Promise { + return readBody(this._rid, "text").then(data => { + try { + return JSON.parse(data); + } catch (e) { + if (this.ok && data === "") { + return {}; + } else if (this.ok) { + throw Error( + `Failed to parse response \`${data}\` as JSON: ${e}` + ); + } + } + }) + } + + formData(): Promise { + return this.json().then((json: Record) => { + const form = new FormData() + for (const [key, value] of Object.entries(json)) { + form.append(key, value) + } + return form + }) + } + + text(): Promise { + return readBody(this._rid, "text") + } +} + /** * Options to configure the Rust client used to make fetch requests * @@ -62,7 +107,7 @@ export interface ClientOptions { export async function fetch( input: URL | Request | string, init?: RequestInit & ClientOptions -): Promise { +): Promise { const maxRedirections = init?.maxRedirections; const connectTimeout = init?.maxRedirections; @@ -96,22 +141,21 @@ export async function fetch( status: number; statusText: string; headers: [[string, string]]; - data: number[]; url: string; } - const { status, statusText, url, headers, data } = + const { status, statusText, url, headers } = await window.__TAURI_INVOKE__("plugin:http|fetch_send", { rid, }); - console.log(status, url, headers, data) - const res = new Response(Uint8Array.from(data), { + const res = new TauriResponse(null, { headers, status, statusText, }); - + res._rid = rid; + // url is read only but seems like we can do this Object.defineProperty(res, "url", { value: url }); return res; diff --git a/plugins/http/src/api-iife.js b/plugins/http/src/api-iife.js index 7a161d75..a975f869 100644 --- a/plugins/http/src/api-iife.js +++ b/plugins/http/src/api-iife.js @@ -1 +1 @@ -if("__TAURI__"in window){var __TAURI_HTTP__=function(e){"use strict";return e.fetch=async function(e,t){const n=null==t?void 0:t.maxRedirections,r=null==t?void 0:t.maxRedirections;t&&(delete t.maxRedirections,delete t.connectTimeout);const i=new Request(e,t),a=await i.arrayBuffer(),_=a.byteLength?Array.from(new Uint8Array(a)):null,o=await window.__TAURI_INVOKE__("plugin:http|fetch",{cmd:"fetch",method:i.method,url:i.url,headers:Array.from(i.headers.entries()),data:_,maxRedirections:n,connectTimeout:r});i.signal.addEventListener("abort",(()=>{window.__TAURI_INVOKE__("plugin:http|fetch_cancel",{rid:o})}));const{status:s,statusText:d,url:c,headers:u,data:l}=await window.__TAURI_INVOKE__("plugin:http|fetch_send",{rid:o});console.log(s,c,u,l);const h=new Response(Uint8Array.from(l),{headers:u,status:s,statusText:d});return Object.defineProperty(h,"url",{value:c}),h},e}({});Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_HTTP__})} +if("__TAURI__"in window){var __TAURI_HTTP__=function(t){"use strict";async function e(t,e){return await window.__TAURI_INVOKE__("plugin:http|fetch_read_body",{rid:t,kind:e})}class r extends Response{constructor(){super(...arguments),this._rid=0}blob(){return e(this._rid,"blob").then((t=>new Blob([t],{type:this.headers.get("content-type")||"application/octet-stream"})))}json(){return e(this._rid,"text").then((t=>{try{return JSON.parse(t)}catch(e){if(this.ok&&""===t)return{};if(this.ok)throw Error(`Failed to parse response \`${t}\` as JSON: ${e}`)}}))}formData(){return this.json().then((t=>{const e=new FormData;for(const[r,n]of Object.entries(t))e.append(r,n);return e}))}text(){return e(this._rid,"text")}}return t.fetch=async function(t,e){const n=null==e?void 0:e.maxRedirections,i=null==e?void 0:e.maxRedirections;e&&(delete e.maxRedirections,delete e.connectTimeout);const s=new Request(t,e),o=await s.arrayBuffer(),a=o.byteLength?Array.from(new Uint8Array(o)):null,_=await window.__TAURI_INVOKE__("plugin:http|fetch",{cmd:"fetch",method:s.method,url:s.url,headers:Array.from(s.headers.entries()),data:a,maxRedirections:n,connectTimeout:i});s.signal.addEventListener("abort",(()=>{window.__TAURI_INVOKE__("plugin:http|fetch_cancel",{rid:_})}));const{status:d,statusText:c,url:u,headers:h}=await window.__TAURI_INVOKE__("plugin:http|fetch_send",{rid:_}),l=new r(null,{headers:h,status:d,statusText:c});return l._rid=_,Object.defineProperty(l,"url",{value:u}),l},t}({});Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_HTTP__})} diff --git a/plugins/http/src/commands.rs b/plugins/http/src/commands.rs index 3bc23976..adac0458 100644 --- a/plugins/http/src/commands.rs +++ b/plugins/http/src/commands.rs @@ -2,16 +2,60 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::{collections::HashMap, time::Duration}; +use std::{collections::HashMap, str::FromStr, time::Duration}; use http::{header, HeaderName, HeaderValue, Method, StatusCode}; use reqwest::redirect::Policy; +use serde::{de::Deserializer, Deserialize, Serialize}; use tauri::{command, AppHandle, Runtime}; -use crate::{Error, FetchRequest, FetchResponse, HttpExt, RequestId}; +use crate::{Error, FetchRequest, HttpExt, RequestId}; + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +pub struct FetchResponse { + status: u16, + status_text: String, + headers: Vec<(String, String)>, + url: String, +} + +#[derive(Serialize)] +#[serde(untagged)] +pub enum ResponseBody { + Blob(Vec), + Text(String), +} + +pub enum BodyKind { + Blob, + Text, +} + +impl FromStr for BodyKind { + type Err = &'static str; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "blob" => Ok(Self::Blob), + "text" => Ok(Self::Text), + _ => Err("unknown body kind"), + } + } +} + +impl<'de> Deserialize<'de> for BodyKind { + fn deserialize(deserializer: D) -> std::result::Result + where + D: Deserializer<'de>, + { + let kind = String::deserialize(deserializer)?; + kind.parse().map_err(serde::de::Error::custom) + } +} #[command] -pub(crate) async fn fetch( +pub async fn fetch( app: AppHandle, method: String, url: url::Url, @@ -109,10 +153,7 @@ pub(crate) async fn fetch( } #[command] -pub(crate) async fn fetch_cancel( - app: AppHandle, - rid: RequestId, -) -> crate::Result<()> { +pub async fn fetch_cancel(app: AppHandle, rid: RequestId) -> crate::Result<()> { let mut request_table = app.http().requests.lock().await; let req = request_table .get_mut(&rid) @@ -122,7 +163,7 @@ pub(crate) async fn fetch_cancel( } #[command] -pub(crate) async fn fetch_send( +pub async fn fetch_send( app: AppHandle, rid: RequestId, ) -> crate::Result { @@ -146,11 +187,30 @@ pub(crate) async fn fetch_send( )); } + app.http().responses.lock().await.insert(rid, res); + Ok(FetchResponse { status: status.as_u16(), status_text: status.canonical_reason().unwrap_or_default().to_string(), headers, url, - data: res.bytes().await?.to_vec(), }) } + +// TODO: change return value to tauri::ipc::Response on next alpha +#[command] +pub(crate) async fn fetch_read_body( + app: AppHandle, + rid: RequestId, + kind: BodyKind, +) -> crate::Result { + let mut response_table = app.http().responses.lock().await; + let res = response_table + .remove(&rid) + .ok_or(Error::InvalidRequestId(rid))?; + + match kind { + BodyKind::Blob => Ok(ResponseBody::Blob(res.bytes().await?.to_vec())), + BodyKind::Text => Ok(ResponseBody::Text(res.text().await?)), + } +} diff --git a/plugins/http/src/lib.rs b/plugins/http/src/lib.rs index d86745cc..7bf9217b 100644 --- a/plugins/http/src/lib.rs +++ b/plugins/http/src/lib.rs @@ -10,7 +10,7 @@ use std::sync::atomic::AtomicU32; use std::{collections::HashMap, future::Future, pin::Pin}; pub use reqwest; -use serde::Serialize; +use reqwest::Response; use tauri::async_runtime::Mutex; use tauri::{ plugin::{Builder, TauriPlugin}, @@ -30,6 +30,7 @@ type CancelableResponseResult = Result>; type CancelableResponseFuture = Pin + Send + Sync>>; type RequestTable = HashMap; +type ResponseTable = HashMap; struct FetchRequest(Mutex); impl FetchRequest { @@ -38,22 +39,13 @@ impl FetchRequest { } } -#[derive(Serialize)] -#[serde(rename_all = "camelCase")] -struct FetchResponse { - status: u16, - status_text: String, - headers: Vec<(String, String)>, - url: String, - data: Vec, -} - struct Http { #[allow(dead_code)] app: AppHandle, scope: scope::Scope, current_id: AtomicU32, requests: Mutex, + responses: Mutex, } impl Http { @@ -80,6 +72,7 @@ pub fn init() -> TauriPlugin> { commands::fetch, commands::fetch_cancel, commands::fetch_send, + commands::fetch_read_body, ]) .setup(|app, api| { let default_scope = HttpAllowlistScope::default(); @@ -87,6 +80,7 @@ pub fn init() -> TauriPlugin> { app: app.clone(), current_id: 0.into(), requests: Default::default(), + responses: Default::default(), scope: scope::Scope::new( api.config() .as_ref()