Repository navigation
Conversation
The connection header is connection-specific, which HTTP/2 forbids (RFC 9113 section 8.2.2). A conforming HTTP/2 client treats an SSE response carrying it as malformed and resets the stream, so live SSE through an h2c reverse proxy never reaches its first event.
This branch has not been deployed
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.
We run Electric with TanStack DB in production, in web apps and in Expo apps on iOS and Android, and I'm sending upstream fixes for a few server bugs we found in that setup. This one we hit directly: our API proxies shape requests to Electric over h2c, and we currently have to send live SSE requests over a separate HTTP/1.1 connection to get around it.
Problem
Live SSE responses, and OPTIONS responses, always set
connection: keep-alive:electric/packages/sync-service/lib/electric/shapes/api/response.ex
Lines 452 to 456 in 3f9809f
connectionis a connection-specific header, and HTTP/2 forbids it (RFC 9113, section 8.2.2): "An endpoint MUST NOT generate an HTTP/2 message containing connection-specific header fields ... Any message containing connection-specific header fields MUST be treated as malformed." Bandit passes the header through on HTTP/2, so when Electric is served over HTTP/2 (for example behind a reverse proxy that talks h2c to it) a conforming client rejects every SSE response.Long polls on the same connection work, because only the SSE and OPTIONS paths set the header.
Repro
Electric over h2c, with the handle and offset from an earlier request:
curl --http2-prior-knowledge -N \ 'http://localhost:3000/v1/shape?table=items&offset=<offset>&handle=<handle>&live=true&live_sse=true'curl reports
Invalid HTTP header field was received: frame type: 1, stream: 1, name: [connection], value: [keep-alive]and the stream is reset. Node'snode:http2client resets it withPROTOCOL_ERRORbefore any headers reach the caller, and Electric logsError while streaming response: :protocol_error.Fix
Response.put_keep_alive_header/1adds the header only whenPlug.Conn.get_http_protocol/1is HTTP/1.x. The SSE path andElectric.Shapes.Api.Optionsboth use it, so OPTIONS responses follow the same rule. HTTP/1.x responses are unchanged, and HTTP/2 connections stay open without the header.Tests
serve_shape_plug_test.exs: "omits the connection header from an SSE response on HTTP/2"options_shape_plug_test.exs: "omits the connection header on HTTP/2"Both set the protocol with
Plug.Test.put_http_protocol/2and fail on main. The existing HTTP/1.1 tests that assertconnection: keep-alivestill pass unchanged.