fix(proxy): implement session-aware streamable-http upstream client#56
Merged
Conversation
…pe error Route primeAnnotationCache through forwardInternal (duck-typed) when available so session-enforcing upstreams receive the warm-up request on the managed session. Replace the opaque 'unexpected shape' fallback with classifyPrimeFailure, which distinguishes HTTP error status, JSON-RPC error payloads, and missing result.tools.
The internal initialize handshake previously drained response bodies unread, so an HTTP 200 carrying a JSON-RPC error could cache a poisoned internal session, and the offered protocol version was used even when the upstream negotiated a different one. Parse and validate the JSON-RPC envelope for both internal handshake steps (initialize and notifications/initialized), failing closed on JSON-RPC errors over both application/json and text/event-stream bodies. Capture the upstream-negotiated protocolVersion from the initialize result and use it for the initialized notification and all managed-session requests, falling back to the offered constant when the upstream does not state one. Scan notifications/initialized SSE responses incrementally with a read timeout and a byte cap instead of buffering whole bodies, so a never-closing or oversized stream cannot stall handshake error classification. Accept SSE field lines with and without a space after the colon in the shared parser. Preserve an already-present mcp-protocol-version request header in direct forwarder usage instead of overwriting it. Add McpForwarderWithInternal to type the optional forwardInternal contract and use it in the prime path instead of an ad-hoc cast. Wire the session-enforcing integration test through the production createForwarderFromConfig and GovernedForwarder path.
UpstreamForwarder was the stateless JSON-RPC POST client that the session-aware StreamableHttpForwarder replaces. Keeping two divergent implementations invites behavior drift: the old class still rejected text/event-stream responses outright. Turn UpstreamForwarder into a deprecated subclass alias of StreamableHttpForwarder so existing imports keep working with the fixed behavior, and mark it with @deprecated JSDoc pointing at the replacement. Migrate the integration and e2e suites to construct StreamableHttpForwarder directly, keeping forwarder.test.ts as compatibility coverage for the alias. Suppress the deprecation lint rule only on the deliberate public re-exports and the public-API surface test, and assert the StreamableHttpForwarder export alongside the alias. Record the deprecation in the changelog.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Implements fixes from the recent Windows beta test - the highest-severity item:
transport: streamable-httpwas a stateless JSON-RPC POST client, so spec-compliant session-enforcing servers (e.g. stock FastMCP, the official MCP SDK servers) rejected Helio's sessionless startup prime with HTTP 400 (infinite "unexpected shape" fail-closed loop) and upstreamtext/event-streamresponses were rejected outright.StreamableHttpForwarderper the two-regime session model: downstream traffic is transparent passthrough (each client'sinitializeforwarded verbatim, upstreamMcp-Session-Idrelayed back, raw 404 passthrough so the client owns re-init), while Helio-internal sessionless traffic (the startup annotation prime) uses a lazily-initialized managed internal session with strictnotifications/initialized, inflight coalescing, and a single re-init retry on HTTP 404. No global upstream session is ever shared across clients.application/jsonandtext/event-streamPOST responses via a shared SSE parser (CRLF andfield:value-without-space tolerant); sendMCP-Protocol-Versionon non-initialize upstream requests.result.tools) so operators get actionable retry logs.UpstreamForwarderto a deprecated compatibility alias ofStreamableHttpForwarder; correct the docs claims and add CHANGELOG entries.Closes #34
Closes #37
Closes #38
Closes #39
Closes #40
Closes #41
Closes #42
Closes #43
Type of Change
Packages Affected
packages/proxypackages/dashboardpackages/python-sdkdocs/examples/Checklist
anytypes or@ts-ignorewithout justificationpnpm secrets:scan,pnpm docs:check:ci,pnpm audit --audit-level=high,pnpm build,pnpm lint,pnpm format:check,pnpm typecheck,pnpm test)feat:,fix:,docs:)How to Test
mcp.run(transport="http")) or any MCP SDK server in stateful mode, pointupstream.urlat it withtransport: streamable-http, runnpx @gethelio/proxy start, and confirm the annotation cache primes ("N tools cached") instead of looping on HTTP 400 / "unexpected shape".initializereturns the upstreamMcp-Session-Id, and subsequenttools/list/tools/callsucceed over SSE responses.initialize.pnpm --filter @gethelio/proxy test— full suite (1384 tests) passes, including the session-enforcing SSE integration suite (integration-streamable-http-session.test.ts).pnpm --filter @gethelio/proxy benchmark— p99 within the <5ms target.Additional Context