Skip to content

fix(extension_manager): apply configured timeout to StreamableHttp reqwest client - #9101

Closed
clouatre wants to merge 3 commits into
aaif-goose:mainfrom
clouatre:fix/streamable-http-timeout
Closed

fix(extension_manager): apply configured timeout to StreamableHttp reqwest client#9101
clouatre wants to merge 3 commits into
aaif-goose:mainfrom
clouatre:fix/streamable-http-timeout

Conversation

@clouatre

@clouatre clouatre commented May 8, 2026

Copy link
Copy Markdown
Contributor

Summary

create_streamable_http_client built its reqwest::Client without a connection timeout, so the extension timeout from config.yaml was correctly resolved by resolve_timeout() but silently discarded at the HTTP transport layer. The OS TCP default (~30s on Linux) fired instead, regardless of the configured value.

This is the root cause of #9022 (streamable HTTP calls reset after ~30s on Linux). The session persistence path (EnabledExtensionsState) is correct; the bug was exclusively in HTTP client construction.

Changes

  • crates/goose/src/agents/extension_manager.rs: compute timeout_duration via resolve_timeout() once before both reqwest::Client::builder() calls; add .connect_timeout(timeout_duration) to the primary client and the OAuth fallback client. .connect_timeout() bounds TCP connection establishment without capping long-lived SSE streams (using .timeout() would disconnect SSE streams every 300s).
  • crates/goose/tests/extension_timeout_test.rs (new): serde roundtrip test for ExtensionConfig::StreamableHttp timeout field (public API only).
  • crates/goose/src/agents/extension_manager.rs #[cfg(test)] module: two unit tests for resolve_timeout with a per-extension value and with None (default fallback). Placed here since resolve_timeout is private.

The fix follows the existing pattern used in download_manager.rs (which also uses .connect_timeout()). Note: .tcp_user_timeout() suggested in #9022 is not available in reqwest 0.13.

Test plan

  • Tests pass (cargo test -p goose)
  • Linter clean (cargo clippy --all-targets -- -D warnings)
  • Security scan clean (no findings from aptu scan_security)

Fixes #9022

…qwest client

Issues aaif-goose#9100 and aaif-goose#9022: create_streamable_http_client built its reqwest::Client
without .timeout(), so the extension timeout from config.yaml was resolved but
silently discarded at the HTTP transport layer. The OS TCP default (~30s on Linux)
fired instead.

- Compute timeout_duration via resolve_timeout() before both reqwest::Client builds
- Add .timeout(timeout_duration) to primary and OAuth fallback HTTP clients
- Make resolve_timeout pub(crate) for test access
- Add unit tests for resolve_timeout behavior and ExtensionConfig serde roundtrip

Fixes aaif-goose#9100
Fixes aaif-goose#9022

Signed-off-by: Hugues Clouâtre <hugues@linux.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6845efbf37

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/goose/tests/extension_timeout_test.rs Outdated
… serde test external

The two resolve_timeout unit tests require access to a private function.
Move them into the existing #[cfg(test)] module in extension_manager.rs
where they can call it directly. Keep streamable_http_config_timeout_roundtrips
in tests/ as it only uses public API (ExtensionConfig + serde_json).

Reverts pub(crate) visibility added in previous commit; resolve_timeout
remains private.

Fixes inline review comment on PR aaif-goose#9101.

Signed-off-by: Hugues Clouâtre <hugues@linux.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e7df084c85

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread crates/goose/src/agents/extension_manager.rs Outdated
… HTTP clients

Using .timeout() on the shared reqwest::Client caps all requests including
the long-lived SSE GET stream, causing periodic disconnects for streamable
HTTP extensions that run longer than the configured timeout (default 300s).

Switch to .connect_timeout() which bounds only TCP connection establishment,
leaving streaming responses unaffected. The MCP-layer timeout passed to
McpClient::connect() already governs RPC call duration.

Addresses review comment on PR aaif-goose#9101.

Signed-off-by: Hugues Clouâtre <hugues@linux.com>
@DOsinga

DOsinga commented May 12, 2026

Copy link
Copy Markdown
Collaborator

Thanks for looking into this, but this PR doesn't actually fix the bug described in #9022.

The issue is that reqwest defaults tcp_user_timeout to 30 seconds on Linux (see reqwest 0.13.2 source, line 303). This kills connections when the server takes >30s to acknowledge transmitted data — which is exactly what happens with long-running MCP tool calls.

.connect_timeout() only bounds the TCP handshake (SYN → SYN-ACK). It has no effect on connections that are already established and waiting for a server response. The correct fix is .tcp_user_timeout(timeout_duration), which is available in reqwest 0.13.2 (the version this project uses) — contrary to what the PR description states.

The tests are also trivial (testing Option::unwrap_or and serde derive) and don't verify the actual behavior.

If you'd like to take another pass at this, the fix is straightforward: add .tcp_user_timeout(timeout_duration) to both client builders. You may also want to keep .connect_timeout() as a reasonable default, but it's not what solves the reported issue.

@clouatre

Copy link
Copy Markdown
Contributor Author

@DOsinga addressed your review in #9207. Applied .tcp_user_timeout(Some(timeout_duration)) to both client builders, gated to Linux where the bug manifests and the socket option exists.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streamable HTTP MCP calls on Linux may reset after ~30s despite larger extension timeout

2 participants