diff --git a/.changes/http-multipart-refactor.md b/.changes/http-multipart-refactor.md
new file mode 100644
index 00000000..562943d5
--- /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
new file mode 100644
index 00000000..ff089543
--- /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 as possible.
diff --git a/Cargo.lock b/Cargo.lock
index 92f85d3d..60b0d73c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -325,6 +325,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"
@@ -367,7 +381,7 @@ dependencies = [
"polling",
"rustix",
"slab",
- "socket2",
+ "socket2 0.4.9",
"waker-fn",
]
@@ -986,6 +1000,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"
@@ -1209,6 +1251,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.7.7"
@@ -1400,6 +1448,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"
@@ -1580,9 +1640,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",
]
@@ -2101,6 +2161,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"
@@ -2209,6 +2297,17 @@ dependencies = [
"windows-sys 0.48.0",
]
+[[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"
@@ -2280,7 +2379,7 @@ dependencies = [
"httpdate",
"itoa 1.0.6",
"pin-project-lite",
- "socket2",
+ "socket2 0.4.9",
"tokio",
"tower-service",
"tracing",
@@ -2362,6 +2461,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"
@@ -2372,6 +2482,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"
@@ -2554,6 +2674,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"
@@ -2832,6 +2964,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"
@@ -2873,6 +3011,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"
@@ -2915,6 +3062,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"
@@ -3525,9 +3678,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"
@@ -3825,6 +3978,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"
@@ -3849,6 +4018,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",
+ "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",
+ "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"
@@ -4023,12 +4239,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",
@@ -4043,7 +4265,9 @@ dependencies = [
"once_cell",
"percent-encoding",
"pin-project-lite",
+ "quinn",
"rustls",
+ "rustls-native-certs",
"rustls-pemfile",
"serde",
"serde_json",
@@ -4051,8 +4275,10 @@ dependencies = [
"tokio",
"tokio-native-tls",
"tokio-rustls",
+ "tokio-socks",
"tokio-util",
"tower-service",
+ "trust-dns-resolver",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
@@ -4062,6 +4288,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"
@@ -4142,6 +4378,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"
@@ -4177,6 +4419,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"
@@ -4548,6 +4802,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"
@@ -5322,17 +5586,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]]
@@ -5844,7 +6107,7 @@ dependencies = [
"mio",
"num_cpus",
"pin-project-lite",
- "socket2",
+ "socket2 0.4.9",
"windows-sys 0.48.0",
]
@@ -5868,6 +6131,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"
@@ -6035,6 +6310,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",
+ "resolv-conf",
+ "smallvec",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "trust-dns-proto",
+]
+
[[package]]
name = "try-lock"
version = "0.2.4"
@@ -6166,12 +6486,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",
]
@@ -6521,6 +6841,12 @@ version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c70234412ca409cc04e864e89523cb0fc37f5e1344ebed5a3ebf4192b6b9f68"
+[[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"
@@ -6921,6 +7247,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 5a1d3032..842816b8 100644
--- a/examples/api/src/views/Http.svelte
+++ b/examples/api/src/views/Http.svelte
@@ -1,5 +1,5 @@
@@ -87,11 +96,6 @@
-
-
diff --git a/plugins/http/Cargo.toml b/plugins/http/Cargo.toml
index f1fb6082..95f74bfe 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 ecc17453..e991076f 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,94 @@ declare global {
}
/**
+ * Options to configure the Rust client used to make fetch requests
+ *
* @since 2.0.0
*/
-interface Duration {
- secs: number;
- nanos: number;
-}
-
-/**
- * @since 2.0.0
- */
-interface ClientOptions {
+export 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;
+ /** Timeout in milliseconds */
+ connectTimeout?: number;
}
/**
- * @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.
+ * 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
- */
-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.
+ * @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();
+ * ```
*
* @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
- */
-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,
- });
- }
-
- /**
- * 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,
- });
- }
+export async function fetch(
+ input: URL | Request | string,
+ init?: RequestInit & ClientOptions,
+): Promise {
+ const maxRedirections = init?.maxRedirections;
+ const connectTimeout = init?.maxRedirections;
+
+ // Remove these fields before creating the request
+ if (init) {
+ delete init.maxRedirections;
+ delete init.connectTimeout;
+ }
+
+ 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,
+ maxRedirections,
+ connectTimeout,
+ });
- /**
- * 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,
+ req.signal.addEventListener("abort", () => {
+ window.__TAURI_INVOKE__("plugin:http|fetch_cancel", {
+ rid,
});
- }
+ });
- /**
- * 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,
- });
+ interface FetchSendResponse {
+ status: number;
+ statusText: string;
+ headers: [[string, string]];
+ url: string;
}
- /**
- * 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,
+ const { status, statusText, url, headers } =
+ await window.__TAURI_INVOKE__("plugin:http|fetch_send", {
+ rid,
});
- }
-}
-
-/**
- * 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));
-}
-
-/** @internal */
-let defaultClient: Client | null = null;
-/**
- * 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,
+ const body = await window.__TAURI_INVOKE__(
+ "plugin:http|fetch_read_body",
+ {
+ rid,
+ },
+ );
+
+ const res = new Response(Uint8Array.from(body), {
+ headers,
+ status,
+ statusText,
});
-}
-export type {
- Duration,
- ClientOptions,
- Part,
- HttpVerb,
- HttpOptions,
- RequestOptions,
- FetchOptions,
-};
+ // url is read only but seems like we can do this
+ Object.defineProperty(res, "url", { value: url });
-export {
- getClient,
- fetch,
- Body,
- Client,
- Response,
- ResponseType,
- type FilePart,
-};
+ return res;
+}
diff --git a/plugins/http/src/api-iife.js b/plugins/http/src/api-iife.js
index e99aa35b..18db7172 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(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
new file mode 100644
index 00000000..833b4e7f
--- /dev/null
+++ b/plugins/http/src/commands.rs
@@ -0,0 +1,178 @@
+// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
+// SPDX-License-Identifier: Apache-2.0
+// SPDX-License-Identifier: MIT
+
+use std::{collections::HashMap, time::Duration};
+
+use http::{header, HeaderName, HeaderValue, Method, StatusCode};
+use reqwest::redirect::Policy;
+use serde::Serialize;
+use tauri::{command, AppHandle, Runtime};
+
+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,
+}
+
+#[command]
+pub async fn fetch(
+ app: AppHandle,
+ method: String,
+ url: url::Url,
+ headers: Vec<(String, String)>,
+ data: Option>,
+ connect_timeout: Option,
+ max_redirections: Option,
+) -> crate::Result {
+ 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 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())?;
+ 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 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 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())?,
+ ));
+ }
+
+ 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,
+ })
+}
+
+// TODO: change return value to tauri::ipc::Response on next alpha
+#[command]
+pub(crate) async fn fetch_read_body(
+ app: AppHandle,
+ rid: RequestId,
+) -> crate::Result> {
+ let mut response_table = app.http().responses.lock().await;
+ let res = response_table
+ .remove(&rid)
+ .ok_or(Error::InvalidRequestId(rid))?;
+
+ Ok(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 07614a53..00000000
--- 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