Skip to content

Stop a session stream's poll loop when its client hangs up behind HTTP/2 - #207

Closed
jinxi97 wants to merge 1 commit into
mainfrom
jinxi97/api-h2c-stream-abort
Closed

jinxi97 wants to merge 1 commit into
mainfrom
jinxi97/api-h2c-stream-abort

Conversation

@jinxi97

@jinxi97 jinxi97 commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

GET /v1/sessions/:id/stream has no server-side EOF. Its loop polls the store until stream.aborted, which needs the hang-up to reach the process. Behind a front end that speaks HTTP/1.1 to the container (Cloud Run's default), it doesn't: the request stays open until the platform's timeout (up to an hour). The loop keeps polling every FUNKY_STREAM_POLL_MS for nobody, and holds one of the instance's concurrency slots the whole time.

Seen on Cloud Run on 2026-10-26: reconnecting clients filled all 240 request slots (3 instances × 80) with orphaned streams. Every request then hung or got 429 Rate exceeded. until a fresh revision was rolled. Request logs showed stream requests completing after >1000s, though their clients had left after 2s.

Fix

  • apps/api/src/listen.ts: HTTP/1.1 and h2c on one port, routed by each connection's first bytes (the h2c preface → node:http2, anything else → node:http). A deployment can now turn on end-to-end HTTP/2, where a hang-up arrives as an RST_STREAM. Health probes, curl, compose and the SDKs keep HTTP/1.1; nothing changes for a deployment that never sends h2c.
  • Stream route: drops the Transfer-Encoding and Connection headers streamSSE sets. HTTP/2 forbids them: the first fails the response outright, and the second is dropped with a warning. Node sets both itself over HTTP/1.1, so the wire is unchanged there (checked with curl -i --http1.1).

Tests

apps/api/test/listen.test.ts runs the real buildApp over real sockets:

  • both protocols answer /health;
  • a client hanging up stops the stream's poll loop, via a store that counts polls, over h2c (RST_STREAM) and over HTTP/1.1 (closed connection).

The h2c case fails without the header change (ERR_HTTP2_INVALID_CONNECTION_HEADERS). No @hono/node-server bump is needed: the 2.0 line cancels a started response body on an h2 reset, and a stream's response has always started (checked on 2.0.8). api: 109/109 tests pass; pnpm typecheck and prettier --check are clean.

Deploying behind Cloud Run

Name the container port h2c (Terraform: ports { name = "h2c" }) for end-to-end HTTP/2. Cloud Run's HTTP probes stay on HTTP/1, which this serves on the same port. funkyhq/cloud does this for its gateway in funkyhq/cloud#95 and will set it for the api once this lands.

🤖 Generated with Claude Code

A session stream ends only when its client hangs up. Behind a front end
that speaks HTTP/1.1 to the container, such as Cloud Run's default, the
hang-up never arrives: the request stays open until the platform's
timeout (up to an hour), and the loop polls the store for nobody while
holding a slot of the instance's request concurrency. Reconnecting
clients fill every slot, and the api stops answering. This happened on a
Cloud Run deployment: 3 instances x 80 slots filled with orphaned streams.

- listen.ts serves HTTP/1.1 and h2c on one port, routed by each
  connection's first bytes, so a deployment can turn on end-to-end
  HTTP/2 (where a hang-up arrives as an RST_STREAM) while probes, curl,
  compose and the SDKs keep HTTP/1.1.
- The stream route drops the Transfer-Encoding and Connection headers
  streamSSE sets: HTTP/2 forbids them, and Node sets both itself over
  HTTP/1.1.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@jinxi97
jinxi97 force-pushed the jinxi97/api-h2c-stream-abort branch from 3676ac8 to 82b3f7d Compare October 26, 2026 22:16
@jinxi97

jinxi97 commented Oct 26, 2026

Copy link
Copy Markdown
Collaborator Author

Closing: Funky is a batch engine for deferrable tasks, so we're removing /v1/sessions/:id/stream instead of hardening it (clients poll /entries?after=). Replaced by the stream-removal PR.

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.

1 participant