Skip to content

Commit

Permalink
refactor(core): use attohttpc by default (#1861)
Browse files Browse the repository at this point in the history
  • Loading branch information
lucasfernog authored May 19, 2021
1 parent f237435 commit 17c7c43
Show file tree
Hide file tree
Showing 12 changed files with 310 additions and 125 deletions.
5 changes: 5 additions & 0 deletions .changes/attohttpc-default-client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"tauri": patch
---

Use `attohttpc` on the HTTP API by default for bundle size optimization. `reqwest` is implemented behind the `reqwest-client` feature flag.
5 changes: 5 additions & 0 deletions .changes/core-features.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"cli.rs": patch
---

Properly keep all `tauri` features that are not managed by the CLI.
13 changes: 10 additions & 3 deletions core/tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ targets = [
"x86_64-apple-darwin"
]

[package.metadata.cargo-udeps.ignore]
normal = ["attohttpc"] # we ignore attohttpc because we can't remove it based on `not(feature = "reqwest-client")`

[dependencies]
serde_json = { version = "1.0", features = [ "raw_value" ] }
serde = { version = "1.0", features = [ "derive" ] }
Expand All @@ -41,7 +44,6 @@ tauri-macros = { version = "1.0.0-beta.1", path = "../tauri-macros" }
tauri-utils = { version = "1.0.0-beta.0", path = "../tauri-utils" }
tauri-runtime-wry = { version = "0.1.1", path = "../tauri-runtime-wry", optional = true }
rand = "0.8"
reqwest = { version = "0.11", features = [ "json", "multipart" ] }
tempfile = "3"
semver = "0.11"
serde_repr = "0.1"
Expand All @@ -53,7 +55,6 @@ tar = "0.4"
flate2 = "1.0"
rfd = "0.3.0"
tinyfiledialogs = "3.3"
bytes = { version = "1", features = [ "serde" ] }
http = "0.2"
clap = { version = "=3.0.0-beta.2", optional = true }
notify-rust = { version = "4.5.0", optional = true }
Expand All @@ -65,6 +66,11 @@ minisign-verify = "0.1.8"
state = "0.4"
bincode = "1.3"

# HTTP
reqwest = { version = "0.11", features = [ "json", "multipart" ], optional = true }
bytes = { version = "1", features = [ "serde" ], optional = true }
attohttpc = { version = "0.17", features = [ "json", "form" ] }

[build-dependencies]
cfg_aliases = "0.1.1"

Expand All @@ -85,9 +91,10 @@ wry = [ "tauri-runtime-wry" ]
cli = [ "clap" ]
custom-protocol = [ "tauri-macros/custom-protocol" ]
api-all = [ "notification-all", "global-shortcut-all", "updater" ]
updater = [ "reqwest/default-tls" ]
updater = [ ]
menu = [ "tauri-runtime/menu", "tauri-runtime-wry/menu" ]
system-tray = [ "tauri-runtime/system-tray", "tauri-runtime-wry/system-tray" ]
reqwest-client = [ "reqwest", "bytes" ]
fs-all = [ ]
fs-read-text-file = [ ]
fs-read-binary-file = [ ]
Expand Down
9 changes: 7 additions & 2 deletions core/tauri/src/api/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,22 @@ pub enum Error {
#[error("user cancelled the dialog")]
DialogCancelled,
/// The network error.
#[cfg(not(feature = "reqwest-client"))]
#[error("Network Error: {0}")]
Network(#[from] attohttpc::Error),
/// The network error.
#[cfg(feature = "reqwest-client")]
#[error("Network Error: {0}")]
Network(#[from] reqwest::Error),
/// HTTP method error.
#[error("{0}")]
HttpMethod(#[from] http::method::InvalidMethod),
/// Invalid HTTO header.
#[error("{0}")]
HttpHeader(#[from] reqwest::header::InvalidHeaderName),
HttpHeader(#[from] http::header::InvalidHeaderName),
/// Failed to serialize header value as string.
#[error("failed to convert response header value to string")]
HttpHeaderToString(#[from] reqwest::header::ToStrError),
HttpHeaderToString(#[from] http::header::ToStrError),
/// HTTP form to must be an object.
#[error("http form must be an object")]
InvalidHttpForm,
Expand Down
144 changes: 132 additions & 12 deletions core/tauri/src/api/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,15 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT

use bytes::Bytes;
use reqwest::{header::HeaderName, redirect::Policy, Method};
use http::{header::HeaderName, Method};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use serde_repr::{Deserialize_repr, Serialize_repr};

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

/// Client builder.
#[derive(Default, Deserialize)]
#[derive(Clone, Default, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClientBuilder {
/// Max number of redirections to follow
Expand All @@ -38,12 +37,19 @@ impl ClientBuilder {
self
}

/// Builds the ClientOptions.
/// Builds the Client.
#[cfg(not(feature = "reqwest-client"))]
pub fn build(self) -> crate::api::Result<Client> {
Ok(Client(self))
}

/// Builds the Client.
#[cfg(feature = "reqwest-client")]
pub fn build(self) -> crate::api::Result<Client> {
let mut client_builder = reqwest::Client::builder();

if let Some(max_redirections) = self.max_redirections {
client_builder = client_builder.redirect(Policy::limited(max_redirections))
client_builder = client_builder.redirect(reqwest::redirect::Policy::limited(max_redirections))
}

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

/// The HTTP client.
#[cfg(feature = "reqwest-client")]
#[derive(Clone)]
pub struct Client(reqwest::Client);

/// The HTTP client.
#[cfg(not(feature = "reqwest-client"))]
#[derive(Clone)]
pub struct Client(ClientBuilder);

#[cfg(not(feature = "reqwest-client"))]
impl Client {
/// Executes an HTTP request
///
/// The response will be transformed to String,
/// If reading the response as binary, the byte array will be serialized using serde_json.
pub async fn send(&self, request: HttpRequestBuilder) -> crate::api::Result<Response> {
let method = Method::from_bytes(request.method.to_uppercase().as_bytes())?;

let mut request_builder = attohttpc::RequestBuilder::try_new(method, &request.url)?;

if let Some(query) = request.query {
request_builder = request_builder.params(&query);
}

if let Some(headers) = request.headers {
for (header, header_value) in headers.iter() {
request_builder =
request_builder.header(HeaderName::from_bytes(header.as_bytes())?, header_value);
}
}

if let Some(timeout) = request.timeout {
request_builder = request_builder.timeout(Duration::from_secs(timeout));
}

let response = if let Some(body) = request.body {
match body {
Body::Bytes(data) => request_builder.body(attohttpc::body::Bytes(data)).send()?,
Body::Text(text) => request_builder.body(attohttpc::body::Bytes(text)).send()?,
Body::Json(json) => request_builder.json(&json)?.send()?,
Body::Form(form_body) => {
let mut form = Vec::new();
for (name, part) in form_body.0 {
match part {
FormPart::Bytes(bytes) => form.push((name, serde_json::to_string(&bytes)?)),
FormPart::File(file_path) => form.push((name, serde_json::to_string(&file_path)?)),
FormPart::Text(text) => form.push((name, text)),
}
}
request_builder.form(&form)?.send()?
}
}
} else {
request_builder.send()?
};

let response = response.error_for_status()?;
Ok(Response(
request.response_type.unwrap_or(ResponseType::Json),
response,
request.url,
))
}
}

#[cfg(feature = "reqwest-client")]
impl Client {
/// Executes an HTTP request
///
/// The response will be transformed to String,
/// If reading the response as binary, the byte array will be serialized using serde_json
/// If reading the response as binary, the byte array will be serialized using serde_json.
pub async fn send(&self, request: HttpRequestBuilder) -> crate::api::Result<Response> {
let method = Method::from_bytes(request.method.to_uppercase().as_bytes())?;

let mut request_builder = self.0.request(method, &request.url);

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

let response = if let Some(body) = request.body {
match body {
Body::Bytes(data) => request_builder.body(Bytes::from(data)).send().await?,
Body::Text(text) => request_builder.body(Bytes::from(text)).send().await?,
Body::Bytes(data) => {
request_builder
.body(bytes::Bytes::from(data))
.send()
.await?
}
Body::Text(text) => {
request_builder
.body(bytes::Bytes::from(text))
.send()
.await?
}
Body::Json(json) => request_builder.json(&json).send().await?,
Body::Form(form_body) => {
let mut form = Vec::new();
Expand Down Expand Up @@ -249,24 +329,50 @@ impl HttpRequestBuilder {
}

/// The HTTP response.
#[cfg(feature = "reqwest-client")]
pub struct Response(ResponseType, reqwest::Response);
/// The HTTP response.
#[cfg(not(feature = "reqwest-client"))]
pub struct Response(ResponseType, attohttpc::Response, String);

impl Response {
/// Reads the response as raw bytes.
pub async fn bytes(self) -> crate::api::Result<RawResponse> {
let status = self.1.status().as_u16();
#[cfg(feature = "reqwest-client")]
let data = self.1.bytes().await?.to_vec();
#[cfg(not(feature = "reqwest-client"))]
let data = self.1.bytes()?;
Ok(RawResponse { status, data })
}

/// Reads the response and returns its info.
pub async fn read(self) -> crate::api::Result<ResponseData> {
#[cfg(feature = "reqwest-client")]
let url = self.1.url().to_string();
#[cfg(not(feature = "reqwest-client"))]
let url = self.2;

let mut headers = HashMap::new();
for (name, value) in self.1.headers() {
headers.insert(name.as_str().to_string(), value.to_str()?.to_string());
}
let status = self.1.status().as_u16();

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

#[cfg(not(feature = "reqwest-client"))]
let data = match self.0 {
ResponseType::Json => self.1.json()?,
ResponseType::Text => Value::String(self.1.text()?),
ResponseType::Binary => Value::String(serde_json::to_string(&self.1.bytes()?)?),
};

Ok(ResponseData {
url,
status,
Expand All @@ -276,12 +382,26 @@ impl Response {
}
}

/// A response with raw bytes.
#[non_exhaustive]
pub struct RawResponse {
/// Response status code.
pub status: u16,
/// Response bytes.
pub data: Vec<u8>,
}

/// The response type.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ResponseData {
url: String,
status: u16,
headers: HashMap<String, String>,
data: Value,
/// Response URL. Useful if it followed redirects.
pub url: String,
/// Response status code.
pub status: u16,
/// Response headers.
pub headers: HashMap<String, String>,
/// Response data.
pub data: Value,
}
Loading

0 comments on commit 17c7c43

Please sign in to comment.