simple-web-app

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

commit e337c44c1a8928d8072b34cfd4c0b9e4299e6afe
parent 5017be83a04bca5a146a3d5ddf4640661748e1f7
Author: Silas Brack <silasbrack@gmail.com>
Date:   Fri, 12 Jun 2026 19:49:16 +0200

perf: share reqwest connection pool across all TrailBase clients

Previously, every request created a new trailbase_client::Client
which internally created a new reqwest::Client, meaning no TCP
connection reuse to TrailBase. Under load, this caused hundreds of
simultaneous TCP connection setups to localhost, capping throughput
at ~220 rps despite TrailBase handling 1400+ rps.

Implements a SharedTransport that wraps a single reqwest::Client
(shared via Arc internally) and passes it to all TrailBase client
instances via ClientOptions.transport. Also bumps reqwest to 0.13
to match trailbase-client's version.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Diffstat:
MCargo.lock | 215+++----------------------------------------------------------------------------
MCargo.toml | 4+++-
Msrc/handlers/auth.rs | 8++++----
Msrc/handlers/comment.rs | 6+++---
Msrc/handlers/feed.rs | 4++--
Msrc/handlers/story.rs | 16++++++++--------
Msrc/handlers/tag.rs | 4++--
Msrc/handlers/user.rs | 4++--
Msrc/handlers/vote.rs | 4++--
Msrc/trailbase.rs | 70+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
10 files changed, 96 insertions(+), 239 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -517,16 +517,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] name = "eventsource-stream" version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -572,21 +562,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - -[[package]] name = "form_urlencoded" version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -858,22 +833,6 @@ dependencies = [ ] [[package]] -name = "hyper-tls" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" -dependencies = [ - "bytes", - "http-body-util", - "hyper", - "hyper-util", - "native-tls", - "tokio", - "tokio-native-tls", - "tower-service", -] - -[[package]] name = "hyper-util" version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1166,12 +1125,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" [[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] name = "litemap" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1244,23 +1197,6 @@ dependencies = [ ] [[package]] -name = "native-tls" -version = "0.2.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" -dependencies = [ - "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", -] - -[[package]] name = "nom" version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1329,49 +1265,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] -name = "openssl" -version = "0.10.80" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a45fa2aa886c42762255da344f0a0d313e254066c46aad76f300c3d3da62d967" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] name = "openssl-probe" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] -name = "openssl-sys" -version = "0.9.116" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28a22dc7140cda5f096e5e7724a6962ca81a7f8bfd2979f9b18c11af56318c4" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] name = "p256" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1467,12 +1366,6 @@ dependencies = [ ] [[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] name = "potential_utf" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1655,49 +1548,6 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.12.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" -dependencies = [ - "base64", - "bytes", - "encoding_rs", - "futures-core", - "futures-util", - "h2", - "http", - "http-body", - "http-body-util", - "hyper", - "hyper-rustls", - "hyper-tls", - "hyper-util", - "js-sys", - "log", - "mime", - "native-tls", - "percent-encoding", - "pin-project-lite", - "rustls-pki-types", - "serde", - "serde_json", - "serde_urlencoded", - "sync_wrapper", - "tokio", - "tokio-native-tls", - "tokio-util", - "tower", - "tower-http", - "tower-service", - "url", - "wasm-bindgen", - "wasm-bindgen-futures", - "wasm-streams 0.4.2", - "web-sys", -] - -[[package]] -name = "reqwest" version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" @@ -1723,6 +1573,8 @@ dependencies = [ "rustls", "rustls-pki-types", "rustls-platform-verifier", + "serde", + "serde_json", "sync_wrapper", "tokio", "tokio-rustls", @@ -1733,7 +1585,7 @@ dependencies = [ "url", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-streams 0.5.0", + "wasm-streams", "web-sys", ] @@ -1797,19 +1649,6 @@ dependencies = [ ] [[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] name = "rustls" version = "0.23.39" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2072,10 +1911,12 @@ name = "simple-web-app" version = "0.1.0" dependencies = [ "askama", + "async-trait", "axum", "axum-extra", "chrono", - "reqwest 0.12.28", + "http", + "reqwest", "serde", "serde_json", "thiserror 2.0.18", @@ -2189,19 +2030,6 @@ dependencies = [ ] [[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] name = "thiserror" version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2324,16 +2152,6 @@ dependencies = [ ] [[package]] -name = "tokio-native-tls" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" -dependencies = [ - "native-tls", - "tokio", -] - -[[package]] name = "tokio-rustls" version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2460,7 +2278,7 @@ dependencies = [ "http-body-util", "jsonwebtoken", "parking_lot", - "reqwest 0.13.2", + "reqwest", "serde", "serde_json", "serde_repr", @@ -2518,12 +2336,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - -[[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -2620,19 +2432,6 @@ dependencies = [ [[package]] name = "wasm-streams" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65" -dependencies = [ - "futures-util", - "js-sys", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "wasm-streams" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" diff --git a/Cargo.toml b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" axum = "0.8" axum-extra = { version = "0.10", features = ["cookie"] } askama = "0.12" +async-trait = "0.1" chrono = { version = "0.4", features = ["serde"] } tokio = { version = "1", features = ["macros", "rt-multi-thread", "net"] } tokio-stream = { version = "0.1", features = ["sync"] } @@ -14,6 +15,7 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" thiserror = "2" tracing = "0.1" -reqwest = { version = "0.12", features = ["json", "stream"] } +http = "1" +reqwest = { version = "0.13", features = ["json", "stream"] } trailbase-client = "0.8" url = "2" diff --git a/src/handlers/auth.rs b/src/handlers/auth.rs @@ -64,14 +64,14 @@ pub async fn login( jar: CookieJar, Form(form): Form<LoginForm>, ) -> Result<(CookieJar, Redirect), Html<String>> { - let client = trailbase::new_client(&state.trailbase_url).map_err(|e| { + let client = trailbase::new_client(&state.http_client, &state.trailbase_url).map_err(|e| { render_login_error(&format!("Client error: {}", e)) })?; match trailbase::login(&client, &form.email, &form.password).await { Ok(tokens) => { // Ensure user has a profile, create one if missing - if let Ok(auth_client) = trailbase::client_with_tokens(&state.trailbase_url, &tokens) { + if let Ok(auth_client) = trailbase::client_with_tokens(&state.http_client, &state.trailbase_url, &tokens) { if let Some(user_id) = trailbase::extract_user_id_from_token(&tokens.auth_token) { match trailbase::get_user_profile_by_id(&auth_client, &user_id).await { Ok(Some(_)) => {}, @@ -135,7 +135,7 @@ pub async fn register( return Err(render_register_error(&form, "Passwords do not match")); } - let client = trailbase::new_client(&state.trailbase_url).map_err(|e| { + let client = trailbase::new_client(&state.http_client, &state.trailbase_url).map_err(|e| { render_register_error(&form, &format!("Client error: {}", e)) })?; @@ -148,7 +148,7 @@ pub async fn register( match trailbase::register(&client, &state.http_client, &form.email, &form.password).await { Ok(tokens) => { // Create authenticated client and user profile - if let Ok(auth_client) = trailbase::client_with_tokens(&state.trailbase_url, &tokens) { + if let Ok(auth_client) = trailbase::client_with_tokens(&state.http_client, &state.trailbase_url, &tokens) { if let Err(e) = trailbase::create_user_profile(&auth_client, &form.username).await { tracing::warn!("Failed to create user profile: {}", e); } diff --git a/src/handlers/comment.rs b/src/handlers/comment.rs @@ -29,7 +29,7 @@ pub async fn create_comment( Html(r#"<meta http-equiv="refresh" content="0;url=/login">"#.to_string()) })?; - let client = trailbase::client_with_tokens(&state.trailbase_url, &tokens) + let client = trailbase::client_with_tokens(&state.http_client, &state.trailbase_url, &tokens) .map_err(|e| Html(format!("Client error: {}", e)))?; if form.text.trim().is_empty() { @@ -72,7 +72,7 @@ pub async fn show_reply_form( let tokens = get_tokens_from_cookies(&jar) .ok_or_else(|| AppError::Unauthorized("Login required".to_string()))?; - let client = trailbase::client_with_tokens(&state.trailbase_url, &tokens)?; + let client = trailbase::client_with_tokens(&state.http_client, &state.trailbase_url, &tokens)?; // Get the parent comment to show what we're replying to let args = trailbase_client::ListArguments::new() @@ -131,7 +131,7 @@ pub async fn submit_reply( Html(r#"<meta http-equiv="refresh" content="0;url=/login">"#.to_string()) })?; - let client = trailbase::client_with_tokens(&state.trailbase_url, &tokens) + let client = trailbase::client_with_tokens(&state.http_client, &state.trailbase_url, &tokens) .map_err(|e| Html(format!("Client error: {}", e)))?; if form.text.trim().is_empty() { diff --git a/src/handlers/feed.rs b/src/handlers/feed.rs @@ -47,8 +47,8 @@ async fn fetch_feed( // Create client (with auth if available) let tokens = get_tokens_from_cookies(jar); let client = match &tokens { - Some(t) => trailbase::client_with_tokens(&state.trailbase_url, t)?, - None => trailbase::new_client(&state.trailbase_url)?, + Some(t) => trailbase::client_with_tokens(&state.http_client, &state.trailbase_url, t)?, + None => trailbase::new_client(&state.http_client, &state.trailbase_url)?, }; // Fetch categories/tags diff --git a/src/handlers/story.rs b/src/handlers/story.rs @@ -79,8 +79,8 @@ pub async fn show_story( let request_start = std::time::Instant::now(); let tokens = get_tokens_from_cookies(&jar); let client = match &tokens { - Some(t) => trailbase::client_with_tokens(&state.trailbase_url, t)?, - None => trailbase::new_client(&state.trailbase_url)?, + Some(t) => trailbase::client_with_tokens(&state.http_client, &state.trailbase_url, t)?, + None => trailbase::new_client(&state.http_client, &state.trailbase_url)?, }; // Fetch the story @@ -171,7 +171,7 @@ pub async fn show_submit( )); } - let client = trailbase::new_client(&state.trailbase_url)?; + let client = trailbase::new_client(&state.http_client, &state.trailbase_url)?; let all_tags = trailbase::get_categories(&client, 50).await.unwrap_or_default(); let submit = SubmitTemplate { @@ -211,7 +211,7 @@ pub async fn submit_story( Html(r#"<meta http-equiv="refresh" content="0;url=/login">"#.to_string()) })?; - let client = trailbase::client_with_tokens(&state.trailbase_url, &tokens) + let client = trailbase::client_with_tokens(&state.http_client, &state.trailbase_url, &tokens) .map_err(|e| render_submit_error(&form, &[], &format!("Client error: {}", e)))?; let all_tags = trailbase::get_categories(&client, 50).await.unwrap_or_default(); @@ -293,7 +293,7 @@ pub async fn show_edit( let tokens = get_tokens_from_cookies(&jar) .ok_or_else(|| AppError::Unauthorized("Login required".to_string()))?; - let client = trailbase::client_with_tokens(&state.trailbase_url, &tokens)?; + let client = trailbase::client_with_tokens(&state.http_client, &state.trailbase_url, &tokens)?; let story = trailbase::get_story(&client, id) .await? @@ -345,7 +345,7 @@ pub async fn edit_story( Html(r#"<meta http-equiv="refresh" content="0;url=/login">"#.to_string()) })?; - let client = trailbase::client_with_tokens(&state.trailbase_url, &tokens) + let client = trailbase::client_with_tokens(&state.http_client, &state.trailbase_url, &tokens) .map_err(|e| render_edit_error(id, &form, &[], &format!("Client error: {}", e)))?; // Check ownership @@ -437,8 +437,8 @@ pub async fn comments_stream( // Helper to build a TrailBase client let make_client = || -> Option<trailbase_client::Client> { match &tokens { - Some(t) => trailbase::client_with_tokens(&state.trailbase_url, t).ok(), - None => trailbase::new_client(&state.trailbase_url).ok(), + Some(t) => trailbase::client_with_tokens(&state.http_client, &state.trailbase_url, t).ok(), + None => trailbase::new_client(&state.http_client, &state.trailbase_url).ok(), } }; diff --git a/src/handlers/tag.rs b/src/handlers/tag.rs @@ -29,8 +29,8 @@ pub async fn show_tag( ) -> Result<Html<String>, AppError> { let tokens = get_tokens_from_cookies(&jar); let client = match &tokens { - Some(t) => trailbase::client_with_tokens(&state.trailbase_url, t)?, - None => trailbase::new_client(&state.trailbase_url)?, + Some(t) => trailbase::client_with_tokens(&state.http_client, &state.trailbase_url, t)?, + None => trailbase::new_client(&state.http_client, &state.trailbase_url)?, }; // Find the category by name diff --git a/src/handlers/user.rs b/src/handlers/user.rs @@ -31,8 +31,8 @@ pub async fn show_user( ) -> Result<Html<String>, AppError> { let tokens = get_tokens_from_cookies(&jar); let client = match &tokens { - Some(t) => trailbase::client_with_tokens(&state.trailbase_url, t)?, - None => trailbase::new_client(&state.trailbase_url)?, + Some(t) => trailbase::client_with_tokens(&state.http_client, &state.trailbase_url, t)?, + None => trailbase::new_client(&state.http_client, &state.trailbase_url)?, }; // Get user profile diff --git a/src/handlers/vote.rs b/src/handlers/vote.rs @@ -55,7 +55,7 @@ pub async fn vote_story( let tokens = get_tokens_from_cookies(&jar) .ok_or_else(|| AppError::Unauthorized("Login required to vote".to_string()))?; - let client = trailbase::client_with_tokens(&state.trailbase_url, &tokens)?; + let client = trailbase::client_with_tokens(&state.http_client, &state.trailbase_url, &tokens)?; let voted = toggle_vote(&client, TARGET_TYPE_STORY, story_id).await?; let new_score = trailbase::get_story(&client, story_id) @@ -75,7 +75,7 @@ pub async fn vote_comment( let tokens = get_tokens_from_cookies(&jar) .ok_or_else(|| AppError::Unauthorized("Login required to vote".to_string()))?; - let client = trailbase::client_with_tokens(&state.trailbase_url, &tokens)?; + let client = trailbase::client_with_tokens(&state.http_client, &state.trailbase_url, &tokens)?; let voted = toggle_vote(&client, TARGET_TYPE_COMMENT, comment_id).await?; let args = trailbase_client::ListArguments::new() diff --git a/src/trailbase.rs b/src/trailbase.rs @@ -1,5 +1,9 @@ +use std::borrow::Cow; + +use reqwest::header::HeaderMap; +use reqwest::Method; use serde::{Deserialize, Serialize}; -use trailbase_client::{Client, ClientOptions, CompareOp, Filter, ListArguments, Pagination, Tokens}; +use trailbase_client::{Client, ClientOptions, CompareOp, Filter, ListArguments, Pagination, Tokens, Transport}; pub use crate::trailbase_types::{Category, Story, Comment, Vote, UserProfile, StoryCategory}; @@ -7,6 +11,49 @@ pub use crate::trailbase_types::{Category, Story, Comment, Vote, UserProfile, St pub const TARGET_TYPE_STORY: i64 = 1; pub const TARGET_TYPE_COMMENT: i64 = 2; +/// Transport that shares a single reqwest::Client connection pool across +/// all TrailBase clients, avoiding per-request TCP connection setup. +struct SharedTransport { + client: reqwest::Client, + url: url::Url, +} + +impl SharedTransport { + fn new(client: reqwest::Client, url: url::Url) -> Self { + Self { client, url } + } +} + +#[async_trait::async_trait] +impl Transport for SharedTransport { + async fn fetch( + &self, + path: &str, + headers: HeaderMap, + method: Method, + body: Option<Vec<u8>>, + query_params: Option<&[(Cow<'static, str>, Cow<'static, str>)]>, + ) -> Result<http::Response<reqwest::Body>, trailbase_client::Error> { + let mut url = self.url.clone(); + url.set_path(path); + + if let Some(query_params) = query_params { + let mut params = url.query_pairs_mut(); + for (key, value) in query_params { + params.append_pair(key, value); + } + } + + let mut builder = self.client.request(method, url).headers(headers); + if let Some(body) = body { + builder = builder.body(body); + } + let request = builder.build()?; + + Ok(self.client.execute(request).await?.into()) + } +} + #[derive(Debug, thiserror::Error)] pub enum ClientError { #[error("TrailBase error: {0}")] @@ -156,20 +203,29 @@ fn base64_decode(input: &str) -> Option<Vec<u8>> { Some(output) } -/// Create a new unauthenticated client -pub fn new_client(base_url: &str) -> Result<Client, ClientError> { - Ok(Client::new(base_url, None)?) +fn make_transport(http_client: &reqwest::Client, base_url: &str) -> Result<Box<dyn Transport + Send + Sync>, ClientError> { + let url: url::Url = base_url.parse().map_err(|e| ClientError::Auth(format!("Invalid URL: {}", e)))?; + Ok(Box::new(SharedTransport::new(http_client.clone(), url))) +} + +/// Create a new unauthenticated client using the shared connection pool +pub fn new_client(http_client: &reqwest::Client, base_url: &str) -> Result<Client, ClientError> { + let opts = ClientOptions { + tokens: None, + transport: Some(make_transport(http_client, base_url)?), + }; + Ok(Client::new(base_url, Some(opts))?) } -/// Create a client with existing tokens (from cookies) -pub fn client_with_tokens(base_url: &str, tokens: &StoredTokens) -> Result<Client, ClientError> { +/// Create a client with existing tokens using the shared connection pool +pub fn client_with_tokens(http_client: &reqwest::Client, base_url: &str, tokens: &StoredTokens) -> Result<Client, ClientError> { let opts = ClientOptions { tokens: Some(Tokens { auth_token: tokens.auth_token.clone(), refresh_token: tokens.refresh_token.clone(), csrf_token: None, }), - transport: None, + transport: Some(make_transport(http_client, base_url)?), }; Ok(Client::new(base_url, Some(opts))?) }