Skip to content

Commit 17c7c43

Browse files
authored
refactor(core): use attohttpc by default (#1861)
1 parent f237435 commit 17c7c43

File tree

12 files changed

+310
-125
lines changed

12 files changed

+310
-125
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"tauri": patch
3+
---
4+
5+
Use `attohttpc` on the HTTP API by default for bundle size optimization. `reqwest` is implemented behind the `reqwest-client` feature flag.

.changes/core-features.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"cli.rs": patch
3+
---
4+
5+
Properly keep all `tauri` features that are not managed by the CLI.

core/tauri/Cargo.toml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,9 @@ targets = [
2727
"x86_64-apple-darwin"
2828
]
2929

30+
[package.metadata.cargo-udeps.ignore]
31+
normal = ["attohttpc"] # we ignore attohttpc because we can't remove it based on `not(feature = "reqwest-client")`
32+
3033
[dependencies]
3134
serde_json = { version = "1.0", features = [ "raw_value" ] }
3235
serde = { version = "1.0", features = [ "derive" ] }
@@ -41,7 +44,6 @@ tauri-macros = { version = "1.0.0-beta.1", path = "../tauri-macros" }
4144
tauri-utils = { version = "1.0.0-beta.0", path = "../tauri-utils" }
4245
tauri-runtime-wry = { version = "0.1.1", path = "../tauri-runtime-wry", optional = true }
4346
rand = "0.8"
44-
reqwest = { version = "0.11", features = [ "json", "multipart" ] }
4547
tempfile = "3"
4648
semver = "0.11"
4749
serde_repr = "0.1"
@@ -53,7 +55,6 @@ tar = "0.4"
5355
flate2 = "1.0"
5456
rfd = "0.3.0"
5557
tinyfiledialogs = "3.3"
56-
bytes = { version = "1", features = [ "serde" ] }
5758
http = "0.2"
5859
clap = { version = "=3.0.0-beta.2", optional = true }
5960
notify-rust = { version = "4.5.0", optional = true }
@@ -65,6 +66,11 @@ minisign-verify = "0.1.8"
6566
state = "0.4"
6667
bincode = "1.3"
6768

69+
# HTTP
70+
reqwest = { version = "0.11", features = [ "json", "multipart" ], optional = true }
71+
bytes = { version = "1", features = [ "serde" ], optional = true }
72+
attohttpc = { version = "0.17", features = [ "json", "form" ] }
73+
6874
[build-dependencies]
6975
cfg_aliases = "0.1.1"
7076

@@ -85,9 +91,10 @@ wry = [ "tauri-runtime-wry" ]
8591
cli = [ "clap" ]
8692
custom-protocol = [ "tauri-macros/custom-protocol" ]
8793
api-all = [ "notification-all", "global-shortcut-all", "updater" ]
88-
updater = [ "reqwest/default-tls" ]
94+
updater = [ ]
8995
menu = [ "tauri-runtime/menu", "tauri-runtime-wry/menu" ]
9096
system-tray = [ "tauri-runtime/system-tray", "tauri-runtime-wry/system-tray" ]
97+
reqwest-client = [ "reqwest", "bytes" ]
9198
fs-all = [ ]
9299
fs-read-text-file = [ ]
93100
fs-read-binary-file = [ ]

core/tauri/src/api/error.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,22 @@ pub enum Error {
2525
#[error("user cancelled the dialog")]
2626
DialogCancelled,
2727
/// The network error.
28+
#[cfg(not(feature = "reqwest-client"))]
29+
#[error("Network Error: {0}")]
30+
Network(#[from] attohttpc::Error),
31+
/// The network error.
32+
#[cfg(feature = "reqwest-client")]
2833
#[error("Network Error: {0}")]
2934
Network(#[from] reqwest::Error),
3035
/// HTTP method error.
3136
#[error("{0}")]
3237
HttpMethod(#[from] http::method::InvalidMethod),
3338
/// Invalid HTTO header.
3439
#[error("{0}")]
35-
HttpHeader(#[from] reqwest::header::InvalidHeaderName),
40+
HttpHeader(#[from] http::header::InvalidHeaderName),
3641
/// Failed to serialize header value as string.
3742
#[error("failed to convert response header value to string")]
38-
HttpHeaderToString(#[from] reqwest::header::ToStrError),
43+
HttpHeaderToString(#[from] http::header::ToStrError),
3944
/// HTTP form to must be an object.
4045
#[error("http form must be an object")]
4146
InvalidHttpForm,

core/tauri/src/api/http.rs

Lines changed: 132 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,15 @@
22
// SPDX-License-Identifier: Apache-2.0
33
// SPDX-License-Identifier: MIT
44

5-
use bytes::Bytes;
6-
use reqwest::{header::HeaderName, redirect::Policy, Method};
5+
use http::{header::HeaderName, Method};
76
use serde::{Deserialize, Serialize};
87
use serde_json::Value;
98
use serde_repr::{Deserialize_repr, Serialize_repr};
109

1110
use std::{collections::HashMap, path::PathBuf, time::Duration};
1211

1312
/// Client builder.
14-
#[derive(Default, Deserialize)]
13+
#[derive(Clone, Default, Deserialize)]
1514
#[serde(rename_all = "camelCase")]
1615
pub struct ClientBuilder {
1716
/// Max number of redirections to follow
@@ -38,12 +37,19 @@ impl ClientBuilder {
3837
self
3938
}
4039

41-
/// Builds the ClientOptions.
40+
/// Builds the Client.
41+
#[cfg(not(feature = "reqwest-client"))]
42+
pub fn build(self) -> crate::api::Result<Client> {
43+
Ok(Client(self))
44+
}
45+
46+
/// Builds the Client.
47+
#[cfg(feature = "reqwest-client")]
4248
pub fn build(self) -> crate::api::Result<Client> {
4349
let mut client_builder = reqwest::Client::builder();
4450

4551
if let Some(max_redirections) = self.max_redirections {
46-
client_builder = client_builder.redirect(Policy::limited(max_redirections))
52+
client_builder = client_builder.redirect(reqwest::redirect::Policy::limited(max_redirections))
4753
}
4854

4955
if let Some(connect_timeout) = self.connect_timeout {
@@ -56,16 +62,80 @@ impl ClientBuilder {
5662
}
5763

5864
/// The HTTP client.
65+
#[cfg(feature = "reqwest-client")]
5966
#[derive(Clone)]
6067
pub struct Client(reqwest::Client);
6168

69+
/// The HTTP client.
70+
#[cfg(not(feature = "reqwest-client"))]
71+
#[derive(Clone)]
72+
pub struct Client(ClientBuilder);
73+
74+
#[cfg(not(feature = "reqwest-client"))]
75+
impl Client {
76+
/// Executes an HTTP request
77+
///
78+
/// The response will be transformed to String,
79+
/// If reading the response as binary, the byte array will be serialized using serde_json.
80+
pub async fn send(&self, request: HttpRequestBuilder) -> crate::api::Result<Response> {
81+
let method = Method::from_bytes(request.method.to_uppercase().as_bytes())?;
82+
83+
let mut request_builder = attohttpc::RequestBuilder::try_new(method, &request.url)?;
84+
85+
if let Some(query) = request.query {
86+
request_builder = request_builder.params(&query);
87+
}
88+
89+
if let Some(headers) = request.headers {
90+
for (header, header_value) in headers.iter() {
91+
request_builder =
92+
request_builder.header(HeaderName::from_bytes(header.as_bytes())?, header_value);
93+
}
94+
}
95+
96+
if let Some(timeout) = request.timeout {
97+
request_builder = request_builder.timeout(Duration::from_secs(timeout));
98+
}
99+
100+
let response = if let Some(body) = request.body {
101+
match body {
102+
Body::Bytes(data) => request_builder.body(attohttpc::body::Bytes(data)).send()?,
103+
Body::Text(text) => request_builder.body(attohttpc::body::Bytes(text)).send()?,
104+
Body::Json(json) => request_builder.json(&json)?.send()?,
105+
Body::Form(form_body) => {
106+
let mut form = Vec::new();
107+
for (name, part) in form_body.0 {
108+
match part {
109+
FormPart::Bytes(bytes) => form.push((name, serde_json::to_string(&bytes)?)),
110+
FormPart::File(file_path) => form.push((name, serde_json::to_string(&file_path)?)),
111+
FormPart::Text(text) => form.push((name, text)),
112+
}
113+
}
114+
request_builder.form(&form)?.send()?
115+
}
116+
}
117+
} else {
118+
request_builder.send()?
119+
};
120+
121+
let response = response.error_for_status()?;
122+
Ok(Response(
123+
request.response_type.unwrap_or(ResponseType::Json),
124+
response,
125+
request.url,
126+
))
127+
}
128+
}
129+
130+
#[cfg(feature = "reqwest-client")]
62131
impl Client {
63132
/// Executes an HTTP request
64133
///
65134
/// The response will be transformed to String,
66-
/// If reading the response as binary, the byte array will be serialized using serde_json
135+
/// If reading the response as binary, the byte array will be serialized using serde_json.
67136
pub async fn send(&self, request: HttpRequestBuilder) -> crate::api::Result<Response> {
68137
let method = Method::from_bytes(request.method.to_uppercase().as_bytes())?;
138+
69139
let mut request_builder = self.0.request(method, &request.url);
70140

71141
if let Some(query) = request.query {
@@ -85,8 +155,18 @@ impl Client {
85155

86156
let response = if let Some(body) = request.body {
87157
match body {
88-
Body::Bytes(data) => request_builder.body(Bytes::from(data)).send().await?,
89-
Body::Text(text) => request_builder.body(Bytes::from(text)).send().await?,
158+
Body::Bytes(data) => {
159+
request_builder
160+
.body(bytes::Bytes::from(data))
161+
.send()
162+
.await?
163+
}
164+
Body::Text(text) => {
165+
request_builder
166+
.body(bytes::Bytes::from(text))
167+
.send()
168+
.await?
169+
}
90170
Body::Json(json) => request_builder.json(&json).send().await?,
91171
Body::Form(form_body) => {
92172
let mut form = Vec::new();
@@ -249,24 +329,50 @@ impl HttpRequestBuilder {
249329
}
250330

251331
/// The HTTP response.
332+
#[cfg(feature = "reqwest-client")]
252333
pub struct Response(ResponseType, reqwest::Response);
334+
/// The HTTP response.
335+
#[cfg(not(feature = "reqwest-client"))]
336+
pub struct Response(ResponseType, attohttpc::Response, String);
253337

254338
impl Response {
339+
/// Reads the response as raw bytes.
340+
pub async fn bytes(self) -> crate::api::Result<RawResponse> {
341+
let status = self.1.status().as_u16();
342+
#[cfg(feature = "reqwest-client")]
343+
let data = self.1.bytes().await?.to_vec();
344+
#[cfg(not(feature = "reqwest-client"))]
345+
let data = self.1.bytes()?;
346+
Ok(RawResponse { status, data })
347+
}
348+
255349
/// Reads the response and returns its info.
256350
pub async fn read(self) -> crate::api::Result<ResponseData> {
351+
#[cfg(feature = "reqwest-client")]
257352
let url = self.1.url().to_string();
353+
#[cfg(not(feature = "reqwest-client"))]
354+
let url = self.2;
355+
258356
let mut headers = HashMap::new();
259357
for (name, value) in self.1.headers() {
260358
headers.insert(name.as_str().to_string(), value.to_str()?.to_string());
261359
}
262360
let status = self.1.status().as_u16();
263361

362+
#[cfg(feature = "reqwest-client")]
264363
let data = match self.0 {
265364
ResponseType::Json => self.1.json().await?,
266365
ResponseType::Text => Value::String(self.1.text().await?),
267366
ResponseType::Binary => Value::String(serde_json::to_string(&self.1.bytes().await?)?),
268367
};
269368

369+
#[cfg(not(feature = "reqwest-client"))]
370+
let data = match self.0 {
371+
ResponseType::Json => self.1.json()?,
372+
ResponseType::Text => Value::String(self.1.text()?),
373+
ResponseType::Binary => Value::String(serde_json::to_string(&self.1.bytes()?)?),
374+
};
375+
270376
Ok(ResponseData {
271377
url,
272378
status,
@@ -276,12 +382,26 @@ impl Response {
276382
}
277383
}
278384

385+
/// A response with raw bytes.
386+
#[non_exhaustive]
387+
pub struct RawResponse {
388+
/// Response status code.
389+
pub status: u16,
390+
/// Response bytes.
391+
pub data: Vec<u8>,
392+
}
393+
279394
/// The response type.
280395
#[derive(Serialize)]
281396
#[serde(rename_all = "camelCase")]
397+
#[non_exhaustive]
282398
pub struct ResponseData {
283-
url: String,
284-
status: u16,
285-
headers: HashMap<String, String>,
286-
data: Value,
399+
/// Response URL. Useful if it followed redirects.
400+
pub url: String,
401+
/// Response status code.
402+
pub status: u16,
403+
/// Response headers.
404+
pub headers: HashMap<String, String>,
405+
/// Response data.
406+
pub data: Value,
287407
}

0 commit comments

Comments
 (0)