Skip to content

Commit 2e5944d

Browse files
committed
fix(api): add request timeout and tcp keepalive
The blocking HTTP client was built via reqwest::blocking::Client::new() with no explicit configuration, which on macOS surfaces as "error sending request" when a request is in flight long enough for the OS to drop the quiet TCP connection (e.g. while the server is doing slow synchronous work like ducklake schema discovery against a remote catalog). Add an explicit overall request timeout (5 min) to bound the worst case if the server genuinely hangs, and a 30s TCP keepalive so the socket stays warm across long synchronous server work. Both values live as constants near the helper for clarity.
1 parent 20afdfa commit 2e5944d

1 file changed

Lines changed: 23 additions & 2 deletions

File tree

src/api.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,27 @@ use crate::config;
33
use crate::util;
44
use crossterm::style::Stylize;
55
use serde::de::DeserializeOwned;
6+
use std::time::Duration;
7+
8+
/// Cap on any single HTTP request. Connection create + synchronous schema
9+
/// discovery against a slow remote catalog can take well over a minute, so
10+
/// this needs to be generous; 5 minutes leaves headroom while still bounding
11+
/// the worst case if the server genuinely hangs.
12+
const HTTP_REQUEST_TIMEOUT: Duration = Duration::from_secs(300);
13+
14+
/// TCP keepalive cadence. Without this, macOS will drop a TCP connection
15+
/// that has been quiet (e.g. while the server is doing slow synchronous
16+
/// work) and reqwest surfaces it as "error sending request" even though the
17+
/// request itself completed server-side.
18+
const TCP_KEEPALIVE_INTERVAL: Duration = Duration::from_secs(30);
19+
20+
fn build_http_client() -> reqwest::blocking::Client {
21+
reqwest::blocking::Client::builder()
22+
.timeout(HTTP_REQUEST_TIMEOUT)
23+
.tcp_keepalive(TCP_KEEPALIVE_INTERVAL)
24+
.build()
25+
.expect("reqwest blocking client should always build with these defaults")
26+
}
627

728
#[derive(Clone)]
829
pub struct ApiClient {
@@ -48,7 +69,7 @@ impl ApiClient {
4869
};
4970

5071
Self {
51-
client: reqwest::blocking::Client::new(),
72+
client: build_http_client(),
5273
api_key: access_token,
5374
api_url: profile_config.api_url.to_string(),
5475
workspace_id: workspace_id.map(String::from),
@@ -66,7 +87,7 @@ impl ApiClient {
6687
#[cfg(test)]
6788
pub(crate) fn test_new(api_url: &str, api_key: &str, workspace_id: Option<&str>) -> Self {
6889
Self {
69-
client: reqwest::blocking::Client::new(),
90+
client: build_http_client(),
7091
api_key: api_key.to_string(),
7192
api_url: api_url.to_string(),
7293
workspace_id: workspace_id.map(String::from),

0 commit comments

Comments
 (0)