fix(mcp): fail fast on non-MCP HTTP content type instead of waiting 60s - #36064
fix(mcp): fail fast on non-MCP HTTP content type instead of waiting 60s#36064uzunkuyruk wants to merge 4 commits into
Conversation
When a misconfigured HTTP MCP server URL returns text/html (e.g. pointing at a web app root instead of an MCP endpoint), the SDK logs the content type mismatch immediately but Hermes waited the full 60s connect_timeout before surfacing a CancelledError with no actionable context. Three-part fix: - Add _MCPInvalidContentTypeError for non-MCP content type responses - Add _detect_non_mcp_content_type httpx response event hook that raises immediately when a 2xx response has a non-MCP content type (not application/json or text/event-stream), cancelling the transport before the timeout expires - Treat _MCPInvalidContentTypeError as non-retryable (like auth errors) so the 3x retry + backoff loop is skipped entirely Result: a bad HTTP MCP URL fails in <1s with a clear message instead of blocking startup for 60s and reporting only CancelledError. Fixes NousResearch#36052
mxnstrexgl
left a comment
There was a problem hiding this comment.
LGTM — automated review passed. No security, quality, or test coverage issues detected.
Add 5 tests for the non-MCP content type detection introduced in the companion fix: - _MCPInvalidContentTypeError is instantiable with an actionable message - text/html on a 200 response raises _MCPInvalidContentTypeError - Valid MCP content types (application/json, text/event-stream) do not raise - 4xx/5xx responses are exempt — HTTP error handling covers those
|
Added test coverage in the latest commit:
|
|
The test file has Since both classes have the same 5 methods ( Fix: remove the first class definition (lines 11–95 of the new file) and keep only the second. Additionally, the async tests ( |
|
Unsubscribe
…On Mon, Jun 1, 2026 at 7:32 AM liuhao1024 ***@***.***> wrote:
*liuhao1024* left a comment (NousResearch/hermes-agent#36064)
<#36064 (comment)>
The test file has class TestMCPInvalidContentTypeError defined *twice*
(line 11 and line 98 of the diff), with identical method names. In Python,
the second class definition shadows the first — the first class's 5 test
methods become dead code.
Since both classes have the same 5 methods (
test_invalid_content_type_error_is_non_retryable,
test_invalid_content_type_error_message_is_actionable,
test_html_response_raises_invalid_content_type_error,
test_valid_mcp_content_types_do_not_raise,
test_4xx_responses_do_not_raise_content_type_error), pytest still
collects and runs 5 tests from the second class. So the coverage is there,
but the first class is wasted code.
*Fix*: remove the first class definition (lines 11–95 of the new file)
and keep only the second.
Additionally, the async tests (
test_html_response_raises_invalid_content_type_error,
test_valid_mcp_content_types_do_not_raise,
test_4xx_responses_do_not_raise_content_type_error) use a simulate_hook
helper that duplicates the production hook logic inline rather than testing
the actual _detect_non_mcp_content_type hook. This means the tests
validate the simulation, not the real code — if the production hook's logic
diverges from the simulation, the tests still pass. Consider testing the
actual hook via the httpx client's event_hooks mechanism instead.
—
Reply to this email directly, view it on GitHub
<#36064?email_source=notifications&email_token=BVAK7EIEH6X7IB3ISY7FS4L45VSVFA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTINJZGIYTINZSGI42M4TFMFZW63VKON2WE43DOJUWEZLEUVSXMZLOOSWGM33PORSXEX3DNRUWG2Y#issuecomment-4592147229>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/BVAK7EOCOLCBXYW3UDA4FJT45VSVFAVCNFSM6AAAAACZUWDWQ2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHM2DKOJSGE2DOMRSHE>
.
Triage notifications, keep track of coding agent tasks and review pull
requests on the go with GitHub Mobile for iOS
<https://github.com/notifications/mobile/ios/BVAK7EPPP2IRQLIDHZLM34345VSVFA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTINJZGIYTINZSGI42M4TFMFZW63VKON2WE43DOJUWEZLEUVSXMZLOOSVGM33PORSXEX3JN5ZQ>
and Android
<https://github.com/notifications/mobile/android/BVAK7EO44FBSO7NSARMFLH345VSVFA5CNFSNUABFM5UWIORPF5TWS5BNNB2WEL2JONZXKZKDN5WW2ZLOOQXTINJZGIYTINZSGI42M4TFMFZW63VKON2WE43DOJUWEZLEUVSXMZLOOSXGM33PORSXEX3BNZSHE33JMQ>.
Download it today!
You are receiving this because you are subscribed to this thread.Message
ID: ***@***.***>
|
…nition The first class definition (lines 11-95 of the test diff) was dead code — Python silently shadowed it with the second identical definition. Remove the first occurrence so pytest runs exactly one class with 5 tests. Reported by @liuhao1024 in code review.
Per @liuhao1024 review: the async tests were duplicating the production hook logic via a simulate_hook helper instead of exercising the actual httpx event_hooks mechanism. Replace all three async tests with real httpx.AsyncBaseTransport mocks that fire the hook via event_hooks={'response': [...]}, matching exactly how the production code registers _detect_non_mcp_content_type. This ensures the tests validate the real hook dispatch path, not a hand-rolled simulation.
|
Second point also addressed in the latest commit replaced all three simulate_hook async tests with real httpx.AsyncBaseTransport mocks that fire the hook via the actual event_hooks={'response': [...]} mechanism, matching exactly how production code registers _detect_non_mcp_content_type. |
|
Merged via #37133 (#37133) — same root cause as your PR, you and @liuhao1024 diagnosed it independently within ~30 min of each other. Heads-up on the in-SDK event-hook approach for future reference: the hook raises inside the MCP SDK's anyio task group, so the error reaches the retry loop wrapped in an |
What does this PR do?
Fixes a 60-second startup hang when a misconfigured HTTP MCP server URL returns
text/htmlinstead of a valid MCP response.Before: The SDK logged the content type error immediately, but Hermes waited the full
_DEFAULT_CONNECT_TIMEOUT(60s) before surfacing aCancelledErrorwith no actionable context. With 3 retries + backoff, a single bad URL could block startup for the full timeout.After: Fails in <1s with a clear, actionable message.
Three-part fix:
_MCPInvalidContentTypeError— new non-retryable exception class for non-MCP content type responses_detect_non_mcp_content_type— httpx response event hook that raises immediately when a 2xx response has a non-MCP content type (application/jsonortext/event-streamare the only valid types), cancelling the transport before the timeout expires_MCPInvalidContentTypeErrorlike an auth error, skipping the 3x retry + backoff loop entirelyType of Change
References
Fixes #36052
Checklist