Skip to content

Commit 9f4fe67

Browse files
pimfeltkampclaude
andcommitted
Bound body read by per-request timeout (same class as Dart/Node fix)
reqwest's `Client::timeout` covers only the connect + initial-response phase. The response body stream that backs `resp.text()` is not covered — a slow or stalled body would otherwise hang the SDK call indefinitely. Same bug class as the Node SDK fix in ca0de42 and the Dart SDK fix in 0e528c3. The Rust variant gets the same total-deadline treatment: store the configured timeout on Transport and wrap `resp.text()` in `tokio::time::timeout` so the body read is also bounded by it. Verified locally: 25/25 unit + 2/2 doctests passing; clippy clean under `-D warnings`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent e9c6d9e commit 9f4fe67

1 file changed

Lines changed: 21 additions & 4 deletions

File tree

src/client.rs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ impl ClientBuilder {
176176
base_url,
177177
user_agent,
178178
max_retries: self.max_retries.unwrap_or(DEFAULT_MAX_RETRIES),
179+
timeout,
179180
http,
180181
});
181182

@@ -211,6 +212,11 @@ pub(crate) struct Transport {
211212
base_url: String,
212213
user_agent: String,
213214
max_retries: u32,
215+
// Per-request total deadline. reqwest's `Client::timeout` covers the
216+
// connect + initial-response phase; the response body stream that
217+
// backs `resp.text()` is *not* covered. We re-apply the same total
218+
// timeout to the body read so a slow/stalled body can't hang the call.
219+
timeout: Duration,
214220
http: reqwest::Client,
215221
}
216222

@@ -301,10 +307,21 @@ impl Transport {
301307
.and_then(|v| v.to_str().ok())
302308
.and_then(parse_retry_after);
303309

304-
let text = resp
305-
.text()
306-
.await
307-
.map_err(|e| Error::network(format!("failed to read body: {e}")))?;
310+
// Bound the body read by the configured per-request timeout —
311+
// reqwest's Client::timeout doesn't cover the body stream once
312+
// .send() has resolved, so a slow body would otherwise hang.
313+
let text = match tokio::time::timeout(self.timeout, resp.text()).await {
314+
Ok(Ok(t)) => t,
315+
Ok(Err(e)) => {
316+
return Err(Error::network(format!("failed to read body: {e}")))
317+
}
318+
Err(_) => {
319+
return Err(Error::timeout(format!(
320+
"response body read timed out after {}s",
321+
self.timeout.as_secs()
322+
)))
323+
}
324+
};
308325
let parsed: Option<Value> = if text.is_empty() {
309326
None
310327
} else {

0 commit comments

Comments
 (0)