From e0eac5594a74c7e53e49e5a23c8b29a642ad4e8b Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 8 Jun 2023 20:20:26 +0300 Subject: [PATCH 01/10] feat(http): refactor and improvemnts --- examples/api/src/views/Http.svelte | 26 +- plugins/http/Cargo.toml | 30 +- plugins/http/guest-js/index.ts | 549 +++------------------------- plugins/http/src/api-iife.js | 2 +- plugins/http/src/commands.rs | 140 +++++++ plugins/http/src/commands/client.rs | 341 ----------------- plugins/http/src/commands/mod.rs | 78 ---- plugins/http/src/config.rs | 5 +- plugins/http/src/error.rs | 35 +- plugins/http/src/lib.rs | 59 ++- plugins/http/src/scope.rs | 14 +- 11 files changed, 295 insertions(+), 984 deletions(-) create mode 100644 plugins/http/src/commands.rs delete mode 100644 plugins/http/src/commands/client.rs delete mode 100644 plugins/http/src/commands/mod.rs diff --git a/examples/api/src/views/Http.svelte b/examples/api/src/views/Http.svelte index 5a1d3032a7..998392edf3 100644 --- a/examples/api/src/views/Http.svelte +++ b/examples/api/src/views/Http.svelte @@ -1,5 +1,5 @@ diff --git a/plugins/http/Cargo.toml b/plugins/http/Cargo.toml index f1fb60826b..95f74bfe2d 100644 --- a/plugins/http/Cargo.toml +++ b/plugins/http/Cargo.toml @@ -13,14 +13,28 @@ tauri = { workspace = true } thiserror = { workspace = true } tauri-plugin-fs = { path = "../fs", version = "2.0.0-alpha.0" } glob = "0.3" -rand = "0.8" -bytes = { version = "1", features = [ "serde" ] } -serde_repr = "0.1" http = "0.2" -reqwest = { version = "0.11", default-features = false, features = [ "json", "stream" ] } +reqwest = { version = "0.11", default-features = false } +url = "2.4" +data-url = "0.3" [features] -multipart = [ "reqwest/multipart" ] -native-tls = [ "reqwest/native-tls" ] -native-tls-vendored = [ "reqwest/native-tls-vendored" ] -rustls-tls = [ "reqwest/rustls-tls" ] +multipart = ["reqwest/multipart"] +json = ["reqwest/json"] +stream = ["reqwest/stream"] +native-tls = ["reqwest/native-tls"] +native-tls-vendored = ["reqwest/native-tls-vendored"] +rustls-tls = ["reqwest/rustls-tls"] +default-tls = ["reqwest/default-tls"] +native-tls-alpn = ["reqwest/native-tls-alpn"] +rustls-tls-manual-roots = ["reqwest/rustls-tls-manual-roots"] +rustls-tls-webpki-roots = ["reqwest/rustls-tls-webpki-roots"] +rustls-tls-native-roots = ["reqwest/rustls-tls-native-roots"] +blocking = ["reqwest/blocking"] +cookies = ["reqwest/cookies"] +gzip = ["reqwest/gzip"] +brotli = ["reqwest/brotli"] +deflate = ["reqwest/deflate"] +trust-dns = ["reqwest/trust-dns"] +socks = ["reqwest/socks"] +http3 = ["reqwest/http3"] diff --git a/plugins/http/guest-js/index.ts b/plugins/http/guest-js/index.ts index 603784022d..dac171452c 100644 --- a/plugins/http/guest-js/index.ts +++ b/plugins/http/guest-js/index.ts @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT /** - * Access the HTTP client written in Rust. + * Make HTTP requests with the Rust backend. * * ## Security * @@ -31,518 +31,61 @@ declare global { } /** - * @since 2.0.0 - */ -interface Duration { - secs: number; - nanos: number; -} - -/** - * @since 2.0.0 - */ -interface ClientOptions { - /** - * Defines the maximum number of redirects the client should follow. - * If set to 0, no redirects will be followed. - */ - maxRedirections?: number; - connectTimeout?: number | Duration; -} - -/** - * @since 2.0.0 - */ -enum ResponseType { - JSON = 1, - Text = 2, - Binary = 3, -} - -/** - * @since 2.0.0 - */ -interface FilePart { - file: string | T; - mime?: string; - fileName?: string; -} - -type Part = string | Uint8Array | FilePart; - -/** - * The body object to be used on POST and PUT requests. - * - * @since 2.0.0 - */ -class Body { - type: string; - payload: unknown; - - /** @ignore */ - private constructor(type: string, payload: unknown) { - this.type = type; - this.payload = payload; - } - - /** - * Creates a new form data body. The form data is an object where each key is the entry name, - * and the value is either a string or a file object. - * - * By default it sets the `application/x-www-form-urlencoded` Content-Type header, - * but you can set it to `multipart/form-data` if the Cargo feature `multipart` is enabled. - * - * Note that a file path must be allowed in the `fs` scope. - * @example - * ```typescript - * import { Body } from "@tauri-apps/plugin-http" - * const body = Body.form({ - * key: 'value', - * image: { - * file: '/path/to/file', // either a path or an array buffer of the file contents - * mime: 'image/jpeg', // optional - * fileName: 'image.jpg' // optional - * } - * }); - * - * // alternatively, use a FormData: - * const form = new FormData(); - * form.append('key', 'value'); - * form.append('image', file, 'image.png'); - * const formBody = Body.form(form); - * ``` - * - * @param data The body data. - * - * @returns The body object ready to be used on the POST and PUT requests. - * - * @since 2.0.0 - */ - static form(data: Record | FormData): Body { - const form: Record> = {}; - - const append = ( - key: string, - v: string | Uint8Array | FilePart | File - ): void => { - if (v !== null) { - let r; - if (typeof v === "string") { - r = v; - } else if (v instanceof Uint8Array || Array.isArray(v)) { - r = Array.from(v); - } else if (v instanceof File) { - r = { file: v.name, mime: v.type, fileName: v.name }; - } else if (typeof v.file === "string") { - r = { file: v.file, mime: v.mime, fileName: v.fileName }; - } else { - r = { file: Array.from(v.file), mime: v.mime, fileName: v.fileName }; - } - form[String(key)] = r; - } - }; - - if (data instanceof FormData) { - for (const [key, value] of data) { - append(key, value); - } - } else { - for (const [key, value] of Object.entries(data)) { - append(key, value); - } - } - return new Body("Form", form); - } - - /** - * Creates a new JSON body. - * @example - * ```typescript - * import { Body } from "@tauri-apps/plugin-http" - * Body.json({ - * registered: true, - * name: 'tauri' - * }); - * ``` - * - * @param data The body JSON object. - * - * @returns The body object ready to be used on the POST and PUT requests. - * - * @since 2.0.0 - */ - static json(data: Record): Body { - return new Body("Json", data); - } - - /** - * Creates a new UTF-8 string body. - * @example - * ```typescript - * import { Body } from "@tauri-apps/plugin-http" - * Body.text('The body content as a string'); - * ``` - * - * @param value The body string. - * - * @returns The body object ready to be used on the POST and PUT requests. - * - * @since 2.0.0 - */ - static text(value: string): Body { - return new Body("Text", value); - } - - /** - * Creates a new byte array body. - * @example - * ```typescript - * import { Body } from "@tauri-apps/plugin-http" - * Body.bytes(new Uint8Array([1, 2, 3])); - * ``` - * - * @param bytes The body byte array. - * - * @returns The body object ready to be used on the POST and PUT requests. - * - * @since 2.0.0 - */ - static bytes( - bytes: Iterable | ArrayLike | ArrayBuffer - ): Body { - // stringifying Uint8Array doesn't return an array of numbers, so we create one here - return new Body( - "Bytes", - Array.from(bytes instanceof ArrayBuffer ? new Uint8Array(bytes) : bytes) - ); - } -} - -/** The request HTTP verb. */ -type HttpVerb = - | "GET" - | "POST" - | "PUT" - | "DELETE" - | "PATCH" - | "HEAD" - | "OPTIONS" - | "CONNECT" - | "TRACE"; - -/** - * Options object sent to the backend. + * Fetch a resource from the network. It returns a `Promise` that resolves to the + * `Response` to that `Request`, whether it is successful or not. * - * @since 2.0.0 - */ -interface HttpOptions { - method: HttpVerb; - url: string; - headers?: Record; - query?: Record; - body?: Body; - timeout?: number | Duration; - responseType?: ResponseType; -} - -/** Request options. */ -type RequestOptions = Omit; -/** Options for the `fetch` API. */ -type FetchOptions = Omit; - -/** @ignore */ -interface IResponse { - url: string; - status: number; - headers: Record; - rawHeaders: Record; - data: T; -} - -/** - * Response object. - * - * @since 2.0.0 - * */ -class Response { - /** The request URL. */ - url: string; - /** The response status code. */ - status: number; - /** A boolean indicating whether the response was successful (status in the range 200–299) or not. */ - ok: boolean; - /** The response headers. */ - headers: Record; - /** The response raw headers. */ - rawHeaders: Record; - /** The response data. */ - data: T; - - /** @ignore */ - constructor(response: IResponse) { - this.url = response.url; - this.status = response.status; - this.ok = this.status >= 200 && this.status < 300; - this.headers = response.headers; - this.rawHeaders = response.rawHeaders; - this.data = response.data; - } -} - -/** - * @since 2.0.0 + * @example + * ```typescript + * const response = await fetch("http://my.json.host/data.json"); + * console.log(response.status); // e.g. 200 + * console.log(response.statusText); // e.g. "OK" + * const jsonData = await response.json(); + * ``` */ -class Client { - id: number; - /** @ignore */ - constructor(id: number) { - this.id = id; - } - - /** - * Drops the client instance. - * @example - * ```typescript - * import { getClient } from '@tauri-apps/plugin-http'; - * const client = await getClient(); - * await client.drop(); - * ``` - */ - async drop(): Promise { - return window.__TAURI_INVOKE__("plugin:http|drop_client", { - client: this.id, - }); - } - - /** - * Makes an HTTP request. - * @example - * ```typescript - * import { getClient } from '@tauri-apps/plugin-http'; - * const client = await getClient(); - * const response = await client.request({ - * method: 'GET', - * url: 'http://localhost:3003/users', - * }); - * ``` - */ - async request(options: HttpOptions): Promise> { - const jsonResponse = - !options.responseType || options.responseType === ResponseType.JSON; - if (jsonResponse) { - options.responseType = ResponseType.Text; - } - return window - .__TAURI_INVOKE__>("plugin:http|request", { - clientId: this.id, - options, - }) - .then((res) => { - const response = new Response(res); - if (jsonResponse) { - /* eslint-disable */ - try { - response.data = JSON.parse(response.data as string); - } catch (e) { - if (response.ok && (response.data as unknown as string) === "") { - response.data = {} as T; - } else if (response.ok) { - throw Error( - `Failed to parse response \`${response.data}\` as JSON: ${e}; - try setting the \`responseType\` option to \`ResponseType.Text\` or \`ResponseType.Binary\` if the API does not return a JSON response.` - ); - } - } - /* eslint-enable */ - return response; - } - return response; - }); - } - - /** - * Makes a GET request. - * @example - * ```typescript - * import { getClient, ResponseType } from '@tauri-apps/plugin-http'; - * const client = await getClient(); - * const response = await client.get('http://localhost:3003/users', { - * timeout: 30, - * // the expected response type - * responseType: ResponseType.JSON - * }); - * ``` - */ - async get(url: string, options?: RequestOptions): Promise> { - return this.request({ - method: "GET", - url, - ...options, - }); - } +async function fetch( + input: URL | Request | string, + init?: RequestInit +): Promise { + const req = new Request(input, init); + const buffer = await req.arrayBuffer(); + const reqData = buffer.byteLength ? Array.from(new Uint8Array(buffer)) : null; + + const rid = await window.__TAURI_INVOKE__("plugin:http|fetch", { + cmd: "fetch", + method: req.method, + url: req.url, + headers: Array.from(req.headers.entries()), + data: reqData, + }); - /** - * Makes a POST request. - * @example - * ```typescript - * import { getClient, Body, ResponseType } from '@tauri-apps/plugin-http'; - * const client = await getClient(); - * const response = await client.post('http://localhost:3003/users', { - * body: Body.json({ - * name: 'tauri', - * password: 'awesome' - * }), - * // in this case the server returns a simple string - * responseType: ResponseType.Text, - * }); - * ``` - */ - async post( - url: string, - body?: Body, - options?: RequestOptions - ): Promise> { - return this.request({ - method: "POST", - url, - body, - ...options, + req.signal.addEventListener("abort", () => { + window.__TAURI_INVOKE__("plugin:http|fetch_cancel", { + rid, }); - } + }); - /** - * Makes a PUT request. - * @example - * ```typescript - * import { getClient, Body } from '@tauri-apps/plugin-http'; - * const client = await getClient(); - * const response = await client.put('http://localhost:3003/users/1', { - * body: Body.form({ - * file: { - * file: '/home/tauri/avatar.png', - * mime: 'image/png', - * fileName: 'avatar.png' - * } - * }) - * }); - * ``` - */ - async put( - url: string, - body?: Body, - options?: RequestOptions - ): Promise> { - return this.request({ - method: "PUT", - url, - body, - ...options, - }); + interface FetchSendResponse { + status: number; + statusText: string; + headers: [[string, string]]; + data: number[]; + url: string; } - /** - * Makes a PATCH request. - * @example - * ```typescript - * import { getClient, Body } from '@tauri-apps/plugin-http'; - * const client = await getClient(); - * const response = await client.patch('http://localhost:3003/users/1', { - * body: Body.json({ email: 'contact@tauri.app' }) - * }); - * ``` - */ - async patch(url: string, options?: RequestOptions): Promise> { - return this.request({ - method: "PATCH", - url, - ...options, + const { status, statusText, url, headers, data } = + await window.__TAURI_INVOKE__("plugin:http|fetch_send", { + rid, }); - } - /** - * Makes a DELETE request. - * @example - * ```typescript - * import { getClient } from '@tauri-apps/plugin-http'; - * const client = await getClient(); - * const response = await client.delete('http://localhost:3003/users/1'); - * ``` - */ - async delete(url: string, options?: RequestOptions): Promise> { - return this.request({ - method: "DELETE", - url, - ...options, - }); - } -} - -/** - * Creates a new client using the specified options. - * @example - * ```typescript - * import { getClient } from '@tauri-apps/plugin-http'; - * const client = await getClient(); - * ``` - * - * @param options Client configuration. - * - * @returns A promise resolving to the client instance. - * - * @since 2.0.0 - */ -async function getClient(options?: ClientOptions): Promise { - return window - .__TAURI_INVOKE__("plugin:http|create_client", { - options, - }) - .then((id) => new Client(id)); -} + const res = new Response(Uint8Array.from(data), { + headers, + status, + statusText, + }); -/** @internal */ -let defaultClient: Client | null = null; + Object.defineProperty(res, "url", { value: url }); -/** - * Perform an HTTP request using the default client. - * @example - * ```typescript - * import { fetch } from '@tauri-apps/plugin-http'; - * const response = await fetch('http://localhost:3003/users/2', { - * method: 'GET', - * timeout: 30, - * }); - * ``` - */ -async function fetch( - url: string, - options?: FetchOptions -): Promise> { - if (defaultClient === null) { - defaultClient = await getClient(); - } - return defaultClient.request({ - url, - method: options?.method ?? "GET", - ...options, - }); + return res; } -export type { - Duration, - ClientOptions, - Part, - HttpVerb, - HttpOptions, - RequestOptions, - FetchOptions, -}; - -export { - getClient, - fetch, - Body, - Client, - Response, - ResponseType, - type FilePart, -}; +export { fetch }; diff --git a/plugins/http/src/api-iife.js b/plugins/http/src/api-iife.js index e99aa35b8e..722209c13d 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";var t;e.ResponseType=void 0,(t=e.ResponseType||(e.ResponseType={}))[t.JSON=1]="JSON",t[t.Text=2]="Text",t[t.Binary=3]="Binary";class r{constructor(e,t){this.type=e,this.payload=t}static form(e){const t={},s=(e,r)=>{if(null!==r){let s;s="string"==typeof r?r:r instanceof Uint8Array||Array.isArray(r)?Array.from(r):r instanceof File?{file:r.name,mime:r.type,fileName:r.name}:"string"==typeof r.file?{file:r.file,mime:r.mime,fileName:r.fileName}:{file:Array.from(r.file),mime:r.mime,fileName:r.fileName},t[String(e)]=s}};if(e instanceof FormData)for(const[t,r]of e)s(t,r);else for(const[t,r]of Object.entries(e))s(t,r);return new r("Form",t)}static json(e){return new r("Json",e)}static text(e){return new r("Text",e)}static bytes(e){return new r("Bytes",Array.from(e instanceof ArrayBuffer?new Uint8Array(e):e))}}class s{constructor(e){this.url=e.url,this.status=e.status,this.ok=this.status>=200&&this.status<300,this.headers=e.headers,this.rawHeaders=e.rawHeaders,this.data=e.data}}class n{constructor(e){this.id=e}async drop(){return window.__TAURI_INVOKE__("plugin:http|drop_client",{client:this.id})}async request(t){const r=!t.responseType||t.responseType===e.ResponseType.JSON;return r&&(t.responseType=e.ResponseType.Text),window.__TAURI_INVOKE__("plugin:http|request",{clientId:this.id,options:t}).then((e=>{const t=new s(e);if(r){try{t.data=JSON.parse(t.data)}catch(e){if(t.ok&&""===t.data)t.data={};else if(t.ok)throw Error(`Failed to parse response \`${t.data}\` as JSON: ${e};\n try setting the \`responseType\` option to \`ResponseType.Text\` or \`ResponseType.Binary\` if the API does not return a JSON response.`)}return t}return t}))}async get(e,t){return this.request({method:"GET",url:e,...t})}async post(e,t,r){return this.request({method:"POST",url:e,body:t,...r})}async put(e,t,r){return this.request({method:"PUT",url:e,body:t,...r})}async patch(e,t){return this.request({method:"PATCH",url:e,...t})}async delete(e,t){return this.request({method:"DELETE",url:e,...t})}}async function i(e){return window.__TAURI_INVOKE__("plugin:http|create_client",{options:e}).then((e=>new n(e)))}let o=null;return e.Body=r,e.Client=n,e.Response=s,e.fetch=async function(e,t){var r;return null===o&&(o=await i()),o.request({url:e,method:null!==(r=null==t?void 0:t.method)&&void 0!==r?r:"GET",...t})},e.getClient=i,e}({});Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_HTTP__})} +if("__TAURI__"in window){var __TAURI_HTTP__=function(t){"use strict";return t.fetch=async function(t,e){const r=new Request(t,e),_=await r.arrayBuffer(),n=_.byteLength?Array.from(new Uint8Array(_)):null,a=await window.__TAURI_INVOKE__("plugin:http|fetch",{cmd:"fetch",method:r.method,url:r.url,headers:Array.from(r.headers.entries()),data:n});r.signal.addEventListener("abort",(()=>{window.__TAURI_INVOKE__("plugin:http|fetch_cancel",{rid:a})}));const{status:i,statusText:s,url:d,headers:u,data:o}=await window.__TAURI_INVOKE__("plugin:http|fetch_send",{rid:a}),c=new Response(Uint8Array.from(o),{headers:u,status:i,statusText:s});return Object.defineProperty(c,"url",{value:d}),c},t}({});Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_HTTP__})} diff --git a/plugins/http/src/commands.rs b/plugins/http/src/commands.rs new file mode 100644 index 0000000000..d3b93c41fd --- /dev/null +++ b/plugins/http/src/commands.rs @@ -0,0 +1,140 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use std::collections::HashMap; + +use http::{header, HeaderName, HeaderValue, Method, StatusCode}; +use tauri::{command, AppHandle, Runtime}; + +use crate::{Error, FetchRequest, FetchResponse, HttpExt, RequestId}; + +#[command] +pub(crate) async fn fetch( + app: AppHandle, + method: String, + url: String, + headers: Vec<(String, String)>, + data: Option>, +) -> crate::Result { + let url = url::Url::parse(&url)?; + let scheme = url.scheme(); + let method = Method::from_bytes(method.as_bytes())?; + let headers: HashMap = HashMap::from_iter(headers); + + match scheme { + "http" | "https" => { + if app.http().scope.is_allowed(&url) { + let mut request = reqwest::Client::new().request(method.clone(), url); + + for (key, value) in &headers { + let name = HeaderName::from_bytes(key.as_bytes())?; + let v = HeaderValue::from_bytes(value.as_bytes())?; + if !matches!(name, header::HOST | header::CONTENT_LENGTH) { + request = request.header(name, v); + } + } + + // POST and PUT requests should always have a 0 length content-length, + // if there is no body. https://fetch.spec.whatwg.org/#http-network-or-cache-fetch + if data.is_none() && matches!(method, Method::POST | Method::PUT) { + request = request.header(header::CONTENT_LENGTH, HeaderValue::from(0)); + } + + if headers.contains_key(header::RANGE.as_str()) { + // https://fetch.spec.whatwg.org/#http-network-or-cache-fetch step 18 + // If httpRequest’s header list contains `Range`, then append (`Accept-Encoding`, `identity`) + request = request.header( + header::ACCEPT_ENCODING, + HeaderValue::from_static("identity"), + ); + } + + if !headers.contains_key(header::USER_AGENT.as_str()) { + request = request.header(header::USER_AGENT, HeaderValue::from_static("tauri")); + } + + if let Some(data) = data { + request = request.body(data); + } + + let http_state = app.http(); + let rid = http_state.next_id(); + let fut = async move { Ok(request.send().await.map_err(Into::into)) }; + let mut request_table = http_state.requests.lock().await; + request_table.insert(rid, FetchRequest::new(Box::pin(fut))); + + Ok(rid) + } else { + Err(Error::UrlNotAllowed(url)) + } + } + "data" => { + let data_url = + data_url::DataUrl::process(url.as_str()).map_err(|_| Error::DataUrlError)?; + let (body, _) = data_url + .decode_to_vec() + .map_err(|_| Error::DataUrlDecodeError)?; + + let response = http::Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, data_url.mime_type().to_string()) + .body(reqwest::Body::from(body))?; + + let http_state = app.http(); + let rid = http_state.next_id(); + let fut = async move { Ok(Ok(reqwest::Response::from(response))) }; + let mut request_table = http_state.requests.lock().await; + request_table.insert(rid, FetchRequest::new(Box::pin(fut))); + Ok(rid) + } + _ => Err(Error::SchemeNotSupport(scheme.to_string())), + } +} + +#[command] +pub(crate) 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) + .ok_or(Error::InvalidRequestId(rid))?; + *req = FetchRequest::new(Box::pin(async { Err(Error::RequestCanceled) })); + Ok(()) +} + +#[command] +pub(crate) async fn fetch_send( + app: AppHandle, + rid: RequestId, +) -> crate::Result { + let mut request_table = app.http().requests.lock().await; + let req = request_table + .remove(&rid) + .ok_or(Error::InvalidRequestId(rid))?; + + let res = match req.0.lock().await.as_mut().await { + Ok(Ok(res)) => res, + Ok(Err(e)) | Err(e) => return Err(e), + }; + + let status = res.status(); + let url = res.url().to_string(); + let mut headers = Vec::new(); + for (key, val) in res.headers().iter() { + headers.push(( + key.as_str().into(), + String::from_utf8(val.as_bytes().to_vec())?, + )); + } + + Ok(FetchResponse { + status: status.as_u16(), + status_text: status.canonical_reason().unwrap_or_default().to_string(), + headers, + url, + data: res.bytes().await?.to_vec(), + }) +} diff --git a/plugins/http/src/commands/client.rs b/plugins/http/src/commands/client.rs deleted file mode 100644 index 07614a53d2..0000000000 --- a/plugins/http/src/commands/client.rs +++ /dev/null @@ -1,341 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use std::{collections::HashMap, path::PathBuf, time::Duration}; - -use reqwest::{header, Method, Url}; -use serde::{Deserialize, Deserializer, Serialize}; -use serde_json::Value; -use serde_repr::{Deserialize_repr, Serialize_repr}; - -#[derive(Deserialize)] -#[serde(untagged)] -enum SerdeDuration { - Seconds(u64), - Duration(Duration), -} - -fn deserialize_duration<'de, D: Deserializer<'de>>( - deserializer: D, -) -> std::result::Result, D::Error> { - if let Some(duration) = Option::::deserialize(deserializer)? { - Ok(Some(match duration { - SerdeDuration::Seconds(s) => Duration::from_secs(s), - SerdeDuration::Duration(d) => d, - })) - } else { - Ok(None) - } -} - -/// The builder of [`Client`]. -#[derive(Debug, Clone, Default, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct ClientBuilder { - /// Max number of redirections to follow. - pub max_redirections: Option, - /// Connect timeout for the request. - #[serde(deserialize_with = "deserialize_duration", default)] - pub connect_timeout: Option, -} - -impl ClientBuilder { - /// Builds the Client. - pub fn build(self) -> crate::Result { - let mut client_builder = reqwest::Client::builder(); - - if let Some(max_redirections) = self.max_redirections { - client_builder = client_builder.redirect(if max_redirections == 0 { - reqwest::redirect::Policy::none() - } else { - reqwest::redirect::Policy::limited(max_redirections) - }); - } - - if let Some(connect_timeout) = self.connect_timeout { - client_builder = client_builder.connect_timeout(connect_timeout); - } - - let client = client_builder.build()?; - Ok(Client(client)) - } -} - -/// The HTTP client based on [`reqwest`]. -#[derive(Debug, Clone)] -pub struct Client(reqwest::Client); - -impl Client { - /// Executes an HTTP request - /// - /// # Examples - pub async fn send(&self, mut request: HttpRequestBuilder) -> crate::Result { - let method = Method::from_bytes(request.method.to_uppercase().as_bytes())?; - - let mut request_builder = self.0.request(method, request.url.as_str()); - - if let Some(query) = request.query { - request_builder = request_builder.query(&query); - } - - if let Some(timeout) = request.timeout { - request_builder = request_builder.timeout(timeout); - } - - if let Some(body) = request.body { - request_builder = match body { - Body::Bytes(data) => request_builder.body(bytes::Bytes::from(data)), - Body::Text(text) => request_builder.body(bytes::Bytes::from(text)), - Body::Json(json) => request_builder.json(&json), - Body::Form(form_body) => { - #[allow(unused_variables)] - fn send_form( - request_builder: reqwest::RequestBuilder, - headers: &mut Option, - form_body: FormBody, - ) -> crate::Result { - #[cfg(feature = "multipart")] - if matches!( - headers - .as_ref() - .and_then(|h| h.0.get("content-type")) - .map(|v| v.as_bytes()), - Some(b"multipart/form-data") - ) { - // the Content-Type header will be set by reqwest in the `.multipart` call - headers.as_mut().map(|h| h.0.remove("content-type")); - let mut multipart = reqwest::multipart::Form::new(); - - for (name, part) in form_body.0 { - let part = match part { - FormPart::File { - file, - mime, - file_name, - } => { - let bytes: Vec = file.try_into()?; - let mut part = reqwest::multipart::Part::bytes(bytes); - if let Some(mime) = mime { - part = part.mime_str(&mime)?; - } - if let Some(file_name) = file_name { - part = part.file_name(file_name); - } - part - } - FormPart::Text(value) => reqwest::multipart::Part::text(value), - }; - - multipart = multipart.part(name, part); - } - - return Ok(request_builder.multipart(multipart)); - } - - let mut form = Vec::new(); - for (name, part) in form_body.0 { - match part { - FormPart::File { file, .. } => { - let bytes: Vec = file.try_into()?; - form.push((name, serde_json::to_string(&bytes)?)) - } - FormPart::Text(value) => form.push((name, value)), - } - } - Ok(request_builder.form(&form)) - } - send_form(request_builder, &mut request.headers, form_body)? - } - }; - } - - if let Some(headers) = request.headers { - request_builder = request_builder.headers(headers.0); - } - - let http_request = request_builder.build()?; - - let response = self.0.execute(http_request).await?; - - Ok(Response( - request.response_type.unwrap_or(ResponseType::Json), - response, - )) - } -} - -#[derive(Serialize_repr, Deserialize_repr, Clone, Debug)] -#[repr(u16)] -#[non_exhaustive] -/// The HTTP response type. -pub enum ResponseType { - /// Read the response as JSON - Json = 1, - /// Read the response as text - Text, - /// Read the response as binary - Binary, -} - -#[derive(Debug)] -pub struct Response(ResponseType, reqwest::Response); - -impl Response { - /// Reads the response. - /// - /// Note that the body is serialized to a [`Value`]. - pub async fn read(self) -> crate::Result { - let url = self.1.url().clone(); - - let mut headers = HashMap::new(); - let mut raw_headers = HashMap::new(); - for (name, value) in self.1.headers() { - headers.insert( - name.as_str().to_string(), - String::from_utf8(value.as_bytes().to_vec())?, - ); - raw_headers.insert( - name.as_str().to_string(), - self.1 - .headers() - .get_all(name) - .into_iter() - .map(|v| String::from_utf8(v.as_bytes().to_vec()).map_err(Into::into)) - .collect::>>()?, - ); - } - let status = self.1.status().as_u16(); - - let data = match self.0 { - ResponseType::Json => self.1.json().await?, - ResponseType::Text => Value::String(self.1.text().await?), - ResponseType::Binary => serde_json::to_value(&self.1.bytes().await?)?, - }; - - Ok(ResponseData { - url, - status, - headers, - raw_headers, - data, - }) - } -} - -#[derive(Debug, Serialize)] -#[serde(rename_all = "camelCase")] -#[non_exhaustive] -pub struct ResponseData { - /// Response URL. Useful if it followed redirects. - pub url: Url, - /// Response status code. - pub status: u16, - /// Response headers. - pub headers: HashMap, - /// Response raw headers. - pub raw_headers: HashMap>, - /// Response data. - pub data: Value, -} - -/// A file path or contents. -#[derive(Debug, Clone, Deserialize)] -#[serde(untagged)] -#[non_exhaustive] -pub enum FilePart { - /// File path. - Path(PathBuf), - /// File contents. - Contents(Vec), -} - -impl TryFrom for Vec { - type Error = crate::Error; - fn try_from(file: FilePart) -> crate::Result { - let bytes = match file { - FilePart::Path(path) => std::fs::read(path)?, - FilePart::Contents(bytes) => bytes, - }; - Ok(bytes) - } -} - -#[derive(Debug, Deserialize)] -#[serde(untagged)] -#[non_exhaustive] -pub enum FormPart { - /// A string value. - Text(String), - /// A file value. - #[serde(rename_all = "camelCase")] - File { - /// File path or content. - file: FilePart, - /// Mime type of this part. - /// Only used when the `Content-Type` header is set to `multipart/form-data`. - mime: Option, - /// File name. - /// Only used when the `Content-Type` header is set to `multipart/form-data`. - file_name: Option, - }, -} - -#[derive(Debug, Deserialize)] -pub struct FormBody(pub(crate) HashMap); - -#[derive(Debug, Deserialize)] -#[serde(tag = "type", content = "payload")] -#[non_exhaustive] -pub enum Body { - Form(FormBody), - Json(Value), - Text(String), - Bytes(Vec), -} - -#[derive(Debug, Default)] -pub struct HeaderMap(header::HeaderMap); - -impl<'de> Deserialize<'de> for HeaderMap { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let map = HashMap::::deserialize(deserializer)?; - let mut headers = header::HeaderMap::default(); - for (key, value) in map { - if let (Ok(key), Ok(value)) = ( - header::HeaderName::from_bytes(key.as_bytes()), - header::HeaderValue::from_str(&value), - ) { - headers.insert(key, value); - } else { - return Err(serde::de::Error::custom(format!( - "invalid header `{key}` `{value}`" - ))); - } - } - Ok(Self(headers)) - } -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct HttpRequestBuilder { - /// The request method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT or TRACE) - pub method: String, - /// The request URL - pub url: Url, - /// The request query params - pub query: Option>, - /// The request headers - pub headers: Option, - /// The request body - pub body: Option, - /// Timeout for the whole request - #[serde(deserialize_with = "deserialize_duration", default)] - pub timeout: Option, - /// The response type (defaults to Json) - pub response_type: Option, -} diff --git a/plugins/http/src/commands/mod.rs b/plugins/http/src/commands/mod.rs deleted file mode 100644 index 94c7132489..0000000000 --- a/plugins/http/src/commands/mod.rs +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2019-2023 Tauri Programme within The Commons Conservancy -// SPDX-License-Identifier: Apache-2.0 -// SPDX-License-Identifier: MIT - -use tauri::{path::SafePathBuf, AppHandle, Runtime, State}; -use tauri_plugin_fs::FsExt; - -use crate::{ClientId, Http}; - -mod client; -use client::{Body, ClientBuilder, FilePart, FormPart, HttpRequestBuilder, ResponseData}; - -pub use client::Client; - -#[tauri::command] -pub async fn create_client( - _app: AppHandle, - http: State<'_, Http>, - options: Option, -) -> super::Result { - let client = options.unwrap_or_default().build()?; - let mut store = http.clients.lock().unwrap(); - let id = rand::random::(); - store.insert(id, client); - Ok(id) -} - -#[tauri::command] -pub async fn drop_client( - _app: AppHandle, - http: State<'_, Http>, - client: ClientId, -) -> super::Result<()> { - let mut store = http.clients.lock().unwrap(); - store.remove(&client); - Ok(()) -} - -#[tauri::command] -pub async fn request( - app: AppHandle, - http: State<'_, Http>, - client_id: ClientId, - options: Box, -) -> super::Result { - if http.scope.is_allowed(&options.url) { - let client = http - .clients - .lock() - .unwrap() - .get(&client_id) - .ok_or_else(|| crate::Error::HttpClientNotInitialized)? - .clone(); - let options = *options; - if let Some(Body::Form(form)) = &options.body { - for value in form.0.values() { - if let FormPart::File { - file: FilePart::Path(path), - .. - } = value - { - if SafePathBuf::new(path.clone()).is_err() - || !app - .try_fs_scope() - .map(|s| s.is_allowed(path)) - .unwrap_or_default() - { - return Err(crate::Error::PathNotAllowed(path.clone())); - } - } - } - } - let response = client.send(options).await?; - Ok(response.read().await?) - } else { - Err(crate::Error::UrlNotAllowed(options.url)) - } -} diff --git a/plugins/http/src/config.rs b/plugins/http/src/config.rs index 4e9d7317e2..e4ac882b35 100644 --- a/plugins/http/src/config.rs +++ b/plugins/http/src/config.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use reqwest::Url; use serde::Deserialize; #[derive(Deserialize)] @@ -15,9 +14,9 @@ pub struct Config { /// The scoped URL is matched against the request URL using a glob pattern. /// /// Examples: -/// - "https://**": allows all HTTPS urls +/// - "https://*" or "https://**" : allows all HTTPS urls /// - "https://*.github.com/tauri-apps/tauri": allows any subdomain of "github.com" with the "tauri-apps/api" path /// - "https://myapi.service.com/users/*": allows access to any URLs that begins with "https://myapi.service.com/users/" #[allow(rustdoc::bare_urls)] #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize)] -pub struct HttpAllowlistScope(pub Vec); +pub struct HttpAllowlistScope(pub Vec); diff --git a/plugins/http/src/error.rs b/plugins/http/src/error.rs index 8b49b0f7fd..457b3382f8 100644 --- a/plugins/http/src/error.rs +++ b/plugins/http/src/error.rs @@ -2,10 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::path::PathBuf; - -use reqwest::Url; use serde::{Serialize, Serializer}; +use url::Url; + +use crate::RequestId; #[derive(Debug, thiserror::Error)] pub enum Error { @@ -15,19 +15,32 @@ pub enum Error { Io(#[from] std::io::Error), #[error(transparent)] Network(#[from] reqwest::Error), + #[error(transparent)] + Http(#[from] http::Error), + #[error(transparent)] + HttpInvalidHeaderName(#[from] http::header::InvalidHeaderName), + #[error(transparent)] + HttpInvalidHeaderValue(#[from] http::header::InvalidHeaderValue), /// URL not allowed by the scope. #[error("url not allowed on the configured scope: {0}")] UrlNotAllowed(Url), - /// Path not allowed by the scope. - #[error("path not allowed on the configured scope: {0}")] - PathNotAllowed(PathBuf), - /// Client with specified ID not found. - #[error("http client dropped or not initialized")] - HttpClientNotInitialized, + #[error(transparent)] + UrlParseError(#[from] url::ParseError), /// HTTP method error. #[error(transparent)] HttpMethod(#[from] http::method::InvalidMethod), - /// Failed to serialize header value as string. + #[error("scheme {0} not supported")] + SchemeNotSupport(String), + #[error("Request canceled")] + RequestCanceled, + #[error(transparent)] + FsError(#[from] tauri_plugin_fs::Error), + #[error("failed to process data url")] + DataUrlError, + #[error("failed to decode data url into bytes")] + DataUrlDecodeError, + #[error("invalid request id: {0}")] + InvalidRequestId(RequestId), #[error(transparent)] Utf8(#[from] std::string::FromUtf8Error), } @@ -40,3 +53,5 @@ impl Serialize for Error { serializer.serialize_str(self.to_string().as_ref()) } } + +pub type Result = std::result::Result; diff --git a/plugins/http/src/lib.rs b/plugins/http/src/lib.rs index d63107cc0e..8ab67d4472 100644 --- a/plugins/http/src/lib.rs +++ b/plugins/http/src/lib.rs @@ -2,34 +2,62 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use config::{Config, HttpAllowlistScope}; -pub use reqwest as client; +use std::sync::atomic::AtomicU32; +use std::{collections::HashMap, future::Future, pin::Pin}; + +pub use reqwest; +use serde::Serialize; +use tauri::async_runtime::Mutex; use tauri::{ plugin::{Builder, TauriPlugin}, AppHandle, Manager, Runtime, }; -use std::{collections::HashMap, sync::Mutex}; +use crate::config::{Config, HttpAllowlistScope}; +pub use error::{Error, Result}; mod commands; mod config; mod error; mod scope; -pub use error::Error; -type Result = std::result::Result; -type ClientId = u32; +type RequestId = u32; +type CancelableResponseResult = Result>; +type CancelableResponseFuture = + Pin + Send + Sync>>; +struct FetchRequest(Mutex); +impl FetchRequest { + fn new(f: CancelableResponseFuture) -> Self { + Self(Mutex::new(f)) + } +} +type RequestTable = HashMap; +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct FetchResponse { + status: u16, + status_text: String, + headers: Vec<(String, String)>, + url: String, + data: Vec, +} -pub struct Http { +struct Http { #[allow(dead_code)] app: AppHandle, - pub(crate) clients: Mutex>, - pub(crate) scope: scope::Scope, + scope: scope::Scope, + current_id: AtomicU32, + requests: Mutex, } -impl Http {} +impl Http { + fn next_id(&self) -> RequestId { + self.current_id + .fetch_add(1, std::sync::atomic::Ordering::Relaxed) + } +} -pub trait HttpExt { +trait HttpExt { fn http(&self) -> &Http; } @@ -43,15 +71,16 @@ pub fn init() -> TauriPlugin> { Builder::>::new("http") .js_init_script(include_str!("api-iife.js").to_string()) .invoke_handler(tauri::generate_handler![ - commands::create_client, - commands::drop_client, - commands::request + commands::fetch, + commands::fetch_cancel, + commands::fetch_send, ]) .setup(|app, api| { let default_scope = HttpAllowlistScope::default(); app.manage(Http { app: app.clone(), - clients: Default::default(), + current_id: 0.into(), + requests: Default::default(), scope: scope::Scope::new( api.config() .as_ref() diff --git a/plugins/http/src/scope.rs b/plugins/http/src/scope.rs index 1b802ace9e..00ef7e083d 100644 --- a/plugins/http/src/scope.rs +++ b/plugins/http/src/scope.rs @@ -2,10 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use crate::config::HttpAllowlistScope; use glob::Pattern; use reqwest::Url; +use crate::config::HttpAllowlistScope; + /// Scope for filesystem access. #[derive(Debug, Clone)] pub struct Scope { @@ -20,7 +21,7 @@ impl Scope { .0 .iter() .map(|url| { - glob::Pattern::new(url.as_str()).unwrap_or_else(|_| { + glob::Pattern::new(url).unwrap_or_else(|_| { panic!("scoped URL is not a valid glob pattern: `{url}`") }) }) @@ -30,9 +31,10 @@ impl Scope { /// Determines if the given URL is allowed on this scope. pub fn is_allowed(&self, url: &Url) -> bool { - self.allowed_urls - .iter() - .any(|allowed| allowed.matches(url.as_str())) + self.allowed_urls.iter().any(|allowed| { + allowed.matches(url.as_str()) + || allowed.matches(url.as_str().strip_suffix('/').unwrap_or_default()) + }) } } @@ -79,7 +81,7 @@ mod tests { let scope = super::Scope::new(&HttpAllowlistScope(vec!["http://*".parse().unwrap()])); assert!(scope.is_allowed(&"http://something.else".parse().unwrap())); - assert!(!scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap())); + assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap())); assert!(!scope.is_allowed(&"https://something.else".parse().unwrap())); let scope = super::Scope::new(&HttpAllowlistScope(vec!["http://**".parse().unwrap()])); From a7ca25c913204a11e518017d520b2d120ec633e5 Mon Sep 17 00:00:00 2001 From: amrbashir Date: Thu, 8 Jun 2023 20:22:12 +0300 Subject: [PATCH 02/10] change file --- .changes/http-plugin-refactor.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changes/http-plugin-refactor.md diff --git a/.changes/http-plugin-refactor.md b/.changes/http-plugin-refactor.md new file mode 100644 index 0000000000..7beb4affe3 --- /dev/null +++ b/.changes/http-plugin-refactor.md @@ -0,0 +1,6 @@ +--- +"http": minor +"http-js": minor +--- + +The http plugin has been rewritten from scratch and now only exposes a `fetch` function in Javascript and Re-exports `reqwest` crate in Rust. The new `fetch` method tries to be as close and compliant to the `fetch` Web API. From 39fe49c46c2bb8ca62c20c176899b8ae01342dc0 Mon Sep 17 00:00:00 2001 From: Amr Bashir Date: Mon, 19 Jun 2023 17:10:40 +0300 Subject: [PATCH 03/10] add client options --- plugins/http/guest-js/index.ts | 34 ++++++++++++++++++++++++++++++---- plugins/http/src/api-iife.js | 2 +- plugins/http/src/commands.rs | 11 +++++++++-- 3 files changed, 40 insertions(+), 7 deletions(-) diff --git a/plugins/http/guest-js/index.ts b/plugins/http/guest-js/index.ts index dac171452c..8862213072 100644 --- a/plugins/http/guest-js/index.ts +++ b/plugins/http/guest-js/index.ts @@ -30,6 +30,21 @@ declare global { } } +/** + * Options to configure the Rust client used to make fetch requests + * + * @since 2.0.0 + */ +export interface ClientOptions { + /** + * Defines the maximum number of redirects the client should follow. + * If set to 0, no redirects will be followed. + */ + maxRedirections?: number; + /** Timeout in milliseconds */ + connectTimeout?: number; +} + /** * Fetch a resource from the network. It returns a `Promise` that resolves to the * `Response` to that `Request`, whether it is successful or not. @@ -41,11 +56,22 @@ declare global { * console.log(response.statusText); // e.g. "OK" * const jsonData = await response.json(); * ``` + * + * @since 2.0.0 */ -async function fetch( +export async function fetch( input: URL | Request | string, - init?: RequestInit + init?: RequestInit & ClientOptions ): Promise { + const maxRedirections = init?.maxRedirections; + const connectTimeout = init?.maxRedirections; + + // Remove these fields before creating the request + if (init) { + init.maxRedirections = undefined; + init.connectTimeout = undefined; + } + const req = new Request(input, init); const buffer = await req.arrayBuffer(); const reqData = buffer.byteLength ? Array.from(new Uint8Array(buffer)) : null; @@ -56,6 +82,8 @@ async function fetch( url: req.url, headers: Array.from(req.headers.entries()), data: reqData, + maxRedirections, + connectTimeout, }); req.signal.addEventListener("abort", () => { @@ -87,5 +115,3 @@ async function fetch( return res; } - -export { fetch }; diff --git a/plugins/http/src/api-iife.js b/plugins/http/src/api-iife.js index 722209c13d..0944fcd039 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(t){"use strict";return t.fetch=async function(t,e){const r=new Request(t,e),_=await r.arrayBuffer(),n=_.byteLength?Array.from(new Uint8Array(_)):null,a=await window.__TAURI_INVOKE__("plugin:http|fetch",{cmd:"fetch",method:r.method,url:r.url,headers:Array.from(r.headers.entries()),data:n});r.signal.addEventListener("abort",(()=>{window.__TAURI_INVOKE__("plugin:http|fetch_cancel",{rid:a})}));const{status:i,statusText:s,url:d,headers:u,data:o}=await window.__TAURI_INVOKE__("plugin:http|fetch_send",{rid:a}),c=new Response(Uint8Array.from(o),{headers:u,status:i,statusText:s});return Object.defineProperty(c,"url",{value:d}),c},t}({});Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_HTTP__})} +if("__TAURI__"in window){var __TAURI_HTTP__=function(t){"use strict";return t.fetch=async function(t,e){const n=null==e?void 0:e.maxRedirections,r=null==e?void 0:e.maxRedirections;e&&(e.maxRedirections=void 0,e.connectTimeout=void 0);const i=new Request(t,e),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:d,statusText:s,url:c,headers:u,data:l}=await window.__TAURI_INVOKE__("plugin:http|fetch_send",{rid:o}),h=new Response(Uint8Array.from(l),{headers:u,status:d,statusText:s});return Object.defineProperty(h,"url",{value:c}),h},t}({});Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_HTTP__})} diff --git a/plugins/http/src/commands.rs b/plugins/http/src/commands.rs index d3b93c41fd..93a3adde60 100644 --- a/plugins/http/src/commands.rs +++ b/plugins/http/src/commands.rs @@ -2,9 +2,10 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::collections::HashMap; +use std::{collections::HashMap, time::Duration}; use http::{header, HeaderName, HeaderValue, Method, StatusCode}; +use reqwest::redirect::Policy; use tauri::{command, AppHandle, Runtime}; use crate::{Error, FetchRequest, FetchResponse, HttpExt, RequestId}; @@ -16,6 +17,8 @@ pub(crate) async fn fetch( url: String, headers: Vec<(String, String)>, data: Option>, + connect_timeout: u64, + max_redirections: usize, ) -> crate::Result { let url = url::Url::parse(&url)?; let scheme = url.scheme(); @@ -25,7 +28,11 @@ pub(crate) async fn fetch( match scheme { "http" | "https" => { if app.http().scope.is_allowed(&url) { - let mut request = reqwest::Client::new().request(method.clone(), url); + let mut request = reqwest::ClientBuilder::new() + .connect_timeout(Duration::from_millis(connect_timeout)) + .redirect(Policy::limited(max_redirections)) + .build()? + .request(method.clone(), url); for (key, value) in &headers { let name = HeaderName::from_bytes(key.as_bytes())?; From 2d679112618fd627b3eb39782684b608a15ebbd9 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Wed, 2 Aug 2023 08:28:28 -0300 Subject: [PATCH 04/10] fixes --- Cargo.lock | 362 +++++++++++++++++++++++++++-- examples/api/src/views/Http.svelte | 15 +- plugins/http/guest-js/index.ts | 5 +- plugins/http/src/api-iife.js | 2 +- plugins/http/src/commands.rs | 27 ++- plugins/http/src/lib.rs | 4 +- 6 files changed, 385 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 813f90dc83..96cf5b52a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -324,6 +324,20 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-compression" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b74f44609f0f91493e3082d3734d98497e094777144380ea4db9f9905dd5b6" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", +] + [[package]] name = "async-executor" version = "1.5.1" @@ -366,7 +380,7 @@ dependencies = [ "polling", "rustix", "slab", - "socket2", + "socket2 0.4.9", "waker-fn", ] @@ -982,6 +996,34 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "cookie" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" +dependencies = [ + "percent-encoding", + "time 0.3.21", + "version_check", +] + +[[package]] +name = "cookie_store" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d606d0fba62e13cf04db20536c05cb7f13673c161cb47a47a82b9b9e7d3f1daa" +dependencies = [ + "cookie", + "idna 0.2.3", + "log", + "publicsuffix", + "serde", + "serde_derive", + "serde_json", + "time 0.3.21", + "url", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -1215,6 +1257,12 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +[[package]] +name = "data-url" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b319d1b62ffbd002e057f36bebd1f42b9f97927c9577461d855f3513c4289f" + [[package]] name = "der" version = "0.5.1" @@ -1402,6 +1450,18 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-as-inner" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9720bba047d567ffc8a3cba48bf19126600e249ab7f128e9233e6376976a116" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "enumflags2" version = "0.7.7" @@ -1565,9 +1625,9 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" dependencies = [ "percent-encoding", ] @@ -2086,6 +2146,34 @@ dependencies = [ "tracing", ] +[[package]] +name = "h3" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6de6ca43eed186fd055214af06967b0a7a68336cefec7e8a4004e96efeaccb9e" +dependencies = [ + "bytes 1.4.0", + "fastrand", + "futures-util", + "http", + "tokio", + "tracing", +] + +[[package]] +name = "h3-quinn" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d4a1a1763e4f3e82ee9f1ecf2cf862b22cc7316ebe14684e42f94532b5ec64d" +dependencies = [ + "bytes 1.4.0", + "futures", + "h3", + "quinn", + "quinn-proto", + "tokio-util", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -2179,6 +2267,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "hostname" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c731c3e10504cc8ed35cfe2f1db4c9274c3d35fa486e3b31df46f068ef3e867" +dependencies = [ + "libc", + "match_cfg", + "winapi", +] + [[package]] name = "html5ever" version = "0.25.2" @@ -2250,7 +2349,7 @@ dependencies = [ "httpdate", "itoa 1.0.6", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -2332,6 +2431,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.3.0" @@ -2342,6 +2452,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "ignore" version = "0.4.20" @@ -2514,6 +2634,18 @@ dependencies = [ "libc", ] +[[package]] +name = "ipconfig" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b58db92f96b720de98181bbbe63c831e87005ab460c1bf306eb2622b4707997f" +dependencies = [ + "socket2 0.5.3", + "widestring", + "windows-sys 0.48.0", + "winreg 0.50.0", +] + [[package]] name = "ipnet" version = "2.7.2" @@ -2792,6 +2924,12 @@ dependencies = [ "safemem", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.3.8" @@ -2833,6 +2971,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "lru-cache" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "mac" version = "0.1.1" @@ -2875,6 +3022,12 @@ dependencies = [ "tendril", ] +[[package]] +name = "match_cfg" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" + [[package]] name = "matchers" version = "0.1.0" @@ -3520,9 +3673,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "phf" @@ -3821,6 +3974,22 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psl-types" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" + +[[package]] +name = "publicsuffix" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96a8c1bda5ae1af7f99a2962e49df150414a43d62404644d98dd5c3a93d07457" +dependencies = [ + "idna 0.3.0", + "psl-types", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -3845,6 +4014,53 @@ dependencies = [ "memchr", ] +[[package]] +name = "quinn" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75" +dependencies = [ + "bytes 1.4.0", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls 0.21.1", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8c8bb234e70c863204303507d841e7fa2295e95c822b2bb4ca8ebf57f17b1cb" +dependencies = [ + "bytes 1.4.0", + "rand 0.8.5", + "ring", + "rustc-hash", + "rustls 0.21.1", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6df19e284d93757a9fb91d63672f7741b129246a669db09d1c0063071debc0c0" +dependencies = [ + "bytes 1.4.0", + "libc", + "socket2 0.5.3", + "tracing", + "windows-sys 0.48.0", +] + [[package]] name = "quote" version = "1.0.28" @@ -4019,12 +4235,18 @@ version = "0.11.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" dependencies = [ + "async-compression", "base64 0.21.2", "bytes 1.4.0", + "cookie", + "cookie_store", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2", + "h3", + "h3-quinn", "http", "http-body", "hyper", @@ -4039,7 +4261,9 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "quinn", "rustls 0.21.1", + "rustls-native-certs", "rustls-pemfile", "serde", "serde_json", @@ -4047,8 +4271,10 @@ dependencies = [ "tokio", "tokio-native-tls", "tokio-rustls 0.24.0", + "tokio-socks", "tokio-util", "tower-service", + "trust-dns-resolver", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -4058,6 +4284,16 @@ dependencies = [ "winreg 0.10.1", ] +[[package]] +name = "resolv-conf" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52e44394d2086d010551b14b53b1f24e31647570cd1deb0379e2c21b329aba00" +dependencies = [ + "hostname", + "quick-error", +] + [[package]] name = "rfd" version = "0.11.4" @@ -4136,6 +4372,12 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc_version" version = "0.4.0" @@ -4183,6 +4425,18 @@ dependencies = [ "sct", ] +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + [[package]] name = "rustls-pemfile" version = "1.0.2" @@ -4544,6 +4798,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + [[package]] name = "soup3" version = "0.3.2" @@ -5218,17 +5482,16 @@ dependencies = [ name = "tauri-plugin-http" version = "2.0.0-alpha.0" dependencies = [ - "bytes 1.4.0", + "data-url", "glob", "http", - "rand 0.8.5", "reqwest", "serde", "serde_json", - "serde_repr", "tauri", "tauri-plugin-fs", "thiserror", + "url", ] [[package]] @@ -5740,7 +6003,7 @@ dependencies = [ "mio", "num_cpus", "pin-project-lite", - "socket2", + "socket2 0.4.9", "windows-sys 0.48.0", ] @@ -5775,6 +6038,18 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-socks" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" +dependencies = [ + "either", + "futures-util", + "thiserror", + "tokio", +] + [[package]] name = "tokio-stream" version = "0.1.14" @@ -5941,6 +6216,51 @@ dependencies = [ "serde_json", ] +[[package]] +name = "trust-dns-proto" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f7f83d1e4a0e4358ac54c5c3681e5d7da5efc5a7a632c90bb6d6669ddd9bc26" +dependencies = [ + "async-trait", + "cfg-if", + "data-encoding", + "enum-as-inner", + "futures-channel", + "futures-io", + "futures-util", + "idna 0.2.3", + "ipnet", + "lazy_static", + "rand 0.8.5", + "smallvec", + "thiserror", + "tinyvec", + "tokio", + "tracing", + "url", +] + +[[package]] +name = "trust-dns-resolver" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aff21aa4dcefb0a1afbfac26deb0adc93888c7d295fb63ab273ef276ba2b7cfe" +dependencies = [ + "cfg-if", + "futures-util", + "ipconfig", + "lazy_static", + "lru-cache", + "parking_lot 0.12.1", + "resolv-conf", + "smallvec", + "thiserror", + "tokio", + "tracing", + "trust-dns-proto", +] + [[package]] name = "try-lock" version = "0.2.4" @@ -6072,12 +6392,12 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.3.1" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" dependencies = [ "form_urlencoded", - "idna", + "idna 0.4.0", "percent-encoding", "serde", ] @@ -6431,6 +6751,12 @@ dependencies = [ "web-sys", ] +[[package]] +name = "widestring" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" + [[package]] name = "win7-notifications" version = "0.3.1" @@ -6831,6 +7157,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "wry" version = "0.28.3" diff --git a/examples/api/src/views/Http.svelte b/examples/api/src/views/Http.svelte index 998392edf3..b17559e2c7 100644 --- a/examples/api/src/views/Http.svelte +++ b/examples/api/src/views/Http.svelte @@ -18,12 +18,14 @@ (httpBody.startsWith("{") && httpBody.endsWith("}")) || (httpBody.startsWith("[") && httpBody.endsWith("]")) ) { - options.body = JSON.parse(httpBody) + options.body = JSON.parse(httpBody); } else if (httpBody !== "") { - options.body = httpBody + options.body = httpBody; } - tauriFetch("http://localhost:3003", options).then(onMessage).catch(onMessage); + tauriFetch("http://localhost:3003", options) + .then(onMessage) + .catch(onMessage); } /// http form @@ -33,7 +35,7 @@ let multipart = true; async function doPost() { - result = await tauriFetch("http://localhost:3003", { + const response = await tauriFetch("http://localhost:3003", { method: "POST", body: { foo, @@ -43,6 +45,11 @@ ? { "Content-Type": "multipart/form-data" } : undefined, }); + result = { + status: response.status, + headers: JSON.parse(JSON.stringify(response.headers)), + body: response.body, + }; } diff --git a/plugins/http/guest-js/index.ts b/plugins/http/guest-js/index.ts index 8862213072..8c1ea94e5e 100644 --- a/plugins/http/guest-js/index.ts +++ b/plugins/http/guest-js/index.ts @@ -68,8 +68,8 @@ export async function fetch( // Remove these fields before creating the request if (init) { - init.maxRedirections = undefined; - init.connectTimeout = undefined; + delete init.maxRedirections; + delete init.connectTimeout; } const req = new Request(input, init); @@ -104,6 +104,7 @@ export async function fetch( await window.__TAURI_INVOKE__("plugin:http|fetch_send", { rid, }); + console.log(status, url, headers, data) const res = new Response(Uint8Array.from(data), { headers, diff --git a/plugins/http/src/api-iife.js b/plugins/http/src/api-iife.js index 0944fcd039..7a161d75b7 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(t){"use strict";return t.fetch=async function(t,e){const n=null==e?void 0:e.maxRedirections,r=null==e?void 0:e.maxRedirections;e&&(e.maxRedirections=void 0,e.connectTimeout=void 0);const i=new Request(t,e),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:d,statusText:s,url:c,headers:u,data:l}=await window.__TAURI_INVOKE__("plugin:http|fetch_send",{rid:o}),h=new Response(Uint8Array.from(l),{headers:u,status:d,statusText:s});return Object.defineProperty(h,"url",{value:c}),h},t}({});Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_HTTP__})} +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__})} diff --git a/plugins/http/src/commands.rs b/plugins/http/src/commands.rs index 93a3adde60..3bc23976ba 100644 --- a/plugins/http/src/commands.rs +++ b/plugins/http/src/commands.rs @@ -14,13 +14,12 @@ use crate::{Error, FetchRequest, FetchResponse, HttpExt, RequestId}; pub(crate) async fn fetch( app: AppHandle, method: String, - url: String, + url: url::Url, headers: Vec<(String, String)>, data: Option>, - connect_timeout: u64, - max_redirections: usize, + connect_timeout: Option, + max_redirections: Option, ) -> crate::Result { - let url = url::Url::parse(&url)?; let scheme = url.scheme(); let method = Method::from_bytes(method.as_bytes())?; let headers: HashMap = HashMap::from_iter(headers); @@ -28,11 +27,21 @@ pub(crate) async fn fetch( match scheme { "http" | "https" => { if app.http().scope.is_allowed(&url) { - let mut request = reqwest::ClientBuilder::new() - .connect_timeout(Duration::from_millis(connect_timeout)) - .redirect(Policy::limited(max_redirections)) - .build()? - .request(method.clone(), url); + let mut builder = reqwest::ClientBuilder::new(); + + if let Some(timeout) = connect_timeout { + builder = builder.connect_timeout(Duration::from_millis(timeout)); + } + + if let Some(max_redirections) = max_redirections { + builder = builder.redirect(if max_redirections == 0 { + Policy::none() + } else { + Policy::limited(max_redirections) + }); + } + + let mut request = builder.build()?.request(method.clone(), url); for (key, value) in &headers { let name = HeaderName::from_bytes(key.as_bytes())?; diff --git a/plugins/http/src/lib.rs b/plugins/http/src/lib.rs index 7e6d4456fd..d86745ccf3 100644 --- a/plugins/http/src/lib.rs +++ b/plugins/http/src/lib.rs @@ -29,13 +29,15 @@ type RequestId = u32; type CancelableResponseResult = Result>; type CancelableResponseFuture = Pin + Send + Sync>>; +type RequestTable = HashMap; + struct FetchRequest(Mutex); impl FetchRequest { fn new(f: CancelableResponseFuture) -> Self { Self(Mutex::new(f)) } } -type RequestTable = HashMap; + #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct FetchResponse { From 4bc57234f5f5840adb01f246ac979278d1632996 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Wed, 2 Aug 2023 12:05:17 -0300 Subject: [PATCH 05/10] 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 b17559e2c7..43758b5095 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 8c1ea94e5e..9c88e28082 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 7a161d75b7..a975f86963 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 3bc23976ba..adac0458d6 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 d86745ccf3..7bf9217b30 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() From 4d77e6531446eaed3257ef08b35dccda37bce382 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Wed, 2 Aug 2023 13:11:27 -0300 Subject: [PATCH 06/10] fix multipart example --- examples/api/src/views/Http.svelte | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/examples/api/src/views/Http.svelte b/examples/api/src/views/Http.svelte index 43758b5095..842816b8df 100644 --- a/examples/api/src/views/Http.svelte +++ b/examples/api/src/views/Http.svelte @@ -34,9 +34,12 @@ 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, + ok: response.ok, + headers: Object.fromEntries(response.headers.entries()), body, }); } @@ -45,23 +48,20 @@ let foo = "baz"; let bar = "qux"; let result = null; - let multipart = true; async function doPost() { + const form = new FormData(); + form.append("foo", foo); + form.append("bar", bar); const response = await tauriFetch("http://localhost:3003", { method: "POST", - body: { - foo, - bar, - }, - headers: multipart - ? { "Content-Type": "multipart/form-data" } - : undefined, + body: form, }); result = { url: response.url, status: response.status, - headers: JSON.parse(JSON.stringify(response.headers)), + ok: response.ok, + headers: Object.fromEntries(response.headers.entries()), body: await response.text(), }; } @@ -96,11 +96,6 @@
- -


From 21801022f6d319444fa4c720ad4ec18b662d6642 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Wed, 2 Aug 2023 13:14:49 -0300 Subject: [PATCH 07/10] update change files --- .changes/http-multipart-refactor.md | 5 +++++ .changes/http-plugin-refactor.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changes/http-multipart-refactor.md diff --git a/.changes/http-multipart-refactor.md b/.changes/http-multipart-refactor.md new file mode 100644 index 0000000000..562943d5eb --- /dev/null +++ b/.changes/http-multipart-refactor.md @@ -0,0 +1,5 @@ +--- +"http-js": minor +--- + +Multipart requests are now handled in JavaScript by the `Request` JavaScript class so you just need to use a `FormData` body and not set the content-type header to `multipart/form-data`. `application/x-www-form-urlencoded` requests must be done manually. diff --git a/.changes/http-plugin-refactor.md b/.changes/http-plugin-refactor.md index 7beb4affe3..ff089543f4 100644 --- a/.changes/http-plugin-refactor.md +++ b/.changes/http-plugin-refactor.md @@ -3,4 +3,4 @@ "http-js": minor --- -The http plugin has been rewritten from scratch and now only exposes a `fetch` function in Javascript and Re-exports `reqwest` crate in Rust. The new `fetch` method tries to be as close and compliant to the `fetch` Web API. +The http plugin has been rewritten from scratch and now only exposes a `fetch` function in Javascript and Re-exports `reqwest` crate in Rust. The new `fetch` method tries to be as close and compliant to the `fetch` Web API as possible. From 566d52e0ab59ab94454992d42a3299fa85a570b3 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Wed, 2 Aug 2023 13:17:44 -0300 Subject: [PATCH 08/10] fmt, remove any --- plugins/http/guest-js/index.ts | 39 +++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/plugins/http/guest-js/index.ts b/plugins/http/guest-js/index.ts index 9c88e28082..b76db724ae 100644 --- a/plugins/http/guest-js/index.ts +++ b/plugins/http/guest-js/index.ts @@ -33,45 +33,50 @@ declare global { async function readBody(rid: number, kind: "blob" | "text"): Promise { return await window.__TAURI_INVOKE__("plugin:http|fetch_read_body", { rid, - kind + kind, }); } class TauriResponse extends Response { - _rid: number = 0 + _rid: number = 0; blob(): Promise { - return readBody(this._rid, "blob").then(bytes => new Blob([bytes], { type: this.headers.get("content-type") || "application/octet-stream" })) + 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 => { + 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}` - ); + 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 this.json().then((json) => { + const form = new FormData(); + for (const [key, value] of Object.entries( + json as Record, + )) { + form.append(key, value); } - return form - }) + return form; + }); } text(): Promise { - return readBody(this._rid, "text") + return readBody(this._rid, "text"); } } @@ -106,7 +111,7 @@ export interface ClientOptions { */ export async function fetch( input: URL | Request | string, - init?: RequestInit & ClientOptions + init?: RequestInit & ClientOptions, ): Promise { const maxRedirections = init?.maxRedirections; const connectTimeout = init?.maxRedirections; From b80c169d90a4e38beafe0e515cae78c27de3eeac Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Mon, 7 Aug 2023 08:22:33 -0300 Subject: [PATCH 09/10] remove TauriResponse class --- plugins/http/guest-js/index.ts | 60 ++++------------------------------ plugins/http/src/api-iife.js | 2 +- plugins/http/src/commands.rs | 46 +++----------------------- 3 files changed, 12 insertions(+), 96 deletions(-) diff --git a/plugins/http/guest-js/index.ts b/plugins/http/guest-js/index.ts index b76db724ae..023552f089 100644 --- a/plugins/http/guest-js/index.ts +++ b/plugins/http/guest-js/index.ts @@ -30,56 +30,6 @@ 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) => { - const form = new FormData(); - for (const [key, value] of Object.entries( - json as Record, - )) { - form.append(key, value); - } - return form; - }); - } - - text(): Promise { - return readBody(this._rid, "text"); - } -} - /** * Options to configure the Rust client used to make fetch requests * @@ -112,7 +62,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; @@ -154,12 +104,16 @@ export async function fetch( rid, }); - const res = new TauriResponse(null, { + const body = await window.__TAURI_INVOKE__("plugin:http|fetch_read_body", { + rid, + }); + + const res = new Response(Uint8Array.from(body), { headers, status, statusText, }); - res._rid = rid; + // url is read only but seems like we can do this Object.defineProperty(res, "url", { value: url }); diff --git a/plugins/http/src/api-iife.js b/plugins/http/src/api-iife.js index a975f86963..18db71722c 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(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__})} +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 _=new Request(e,t),i=await _.arrayBuffer(),a=i.byteLength?Array.from(new Uint8Array(i)):null,d=await window.__TAURI_INVOKE__("plugin:http|fetch",{cmd:"fetch",method:_.method,url:_.url,headers:Array.from(_.headers.entries()),data:a,maxRedirections:n,connectTimeout:r});_.signal.addEventListener("abort",(()=>{window.__TAURI_INVOKE__("plugin:http|fetch_cancel",{rid:d})}));const{status:o,statusText:s,url:c,headers:u}=await window.__TAURI_INVOKE__("plugin:http|fetch_send",{rid:d}),l=await window.__TAURI_INVOKE__("plugin:http|fetch_read_body",{rid:d}),w=new Response(Uint8Array.from(l),{headers:u,status:o,statusText:s});return Object.defineProperty(w,"url",{value:c}),w},e}({});Object.defineProperty(window.__TAURI__,"http",{value:__TAURI_HTTP__})} diff --git a/plugins/http/src/commands.rs b/plugins/http/src/commands.rs index adac0458d6..833b4e7f04 100644 --- a/plugins/http/src/commands.rs +++ b/plugins/http/src/commands.rs @@ -2,11 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT -use std::{collections::HashMap, str::FromStr, time::Duration}; +use std::{collections::HashMap, time::Duration}; use http::{header, HeaderName, HeaderValue, Method, StatusCode}; use reqwest::redirect::Policy; -use serde::{de::Deserializer, Deserialize, Serialize}; +use serde::Serialize; use tauri::{command, AppHandle, Runtime}; use crate::{Error, FetchRequest, HttpExt, RequestId}; @@ -20,40 +20,6 @@ pub struct FetchResponse { 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 async fn fetch( app: AppHandle, @@ -202,15 +168,11 @@ pub async fn fetch_send( pub(crate) async fn fetch_read_body( app: AppHandle, rid: RequestId, - kind: BodyKind, -) -> crate::Result { +) -> 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?)), - } + Ok(res.bytes().await?.to_vec()) } From 1604a1efd23e720c9d2883fd687d57e29cd56cd3 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Mon, 7 Aug 2023 08:25:29 -0300 Subject: [PATCH 10/10] fmt [skip ci] --- plugins/http/guest-js/index.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/plugins/http/guest-js/index.ts b/plugins/http/guest-js/index.ts index 023552f089..e991076f10 100644 --- a/plugins/http/guest-js/index.ts +++ b/plugins/http/guest-js/index.ts @@ -104,9 +104,12 @@ export async function fetch( rid, }); - const body = await window.__TAURI_INVOKE__("plugin:http|fetch_read_body", { - rid, - }); + const body = await window.__TAURI_INVOKE__( + "plugin:http|fetch_read_body", + { + rid, + }, + ); const res = new Response(Uint8Array.from(body), { headers,