Repository navigation
fix: send Last-Event-ID header as UTF-8 - #367
Merged
rexxars merged 1 commit intoOct 13, 2026
Merged
Conversation
The event ID was passed to `fetch()` as a plain string header value, but header values are byte strings. An ID with a character above U+00FF made `fetch()` throw a `TypeError` on every reconnect attempt, so the client never reconnected. An ID with characters in U+0080 to U+00FF was sent as single Latin-1 bytes instead of UTF-8. Encode the ID as UTF-8 and pass those bytes through as a byte string, as the specification requires.
rexxars
approved these changes
Oct 13, 2026
Member
|
Thank you for your contribution! |
Member
|
Mistakenly didn't run the CI workflows on this; this crashes on Safari and workerd. I'm working on finding a solution. |
Member
|
Could not find a proper workaround for Safari, unfortunately: https://bugs.webkit.org/show_bug.cgi?id=327451 |
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.
What was wrong
Last-Event-IDwas set to the raw event ID string. Header values passed tofetch()are byte strings, so:€) makesfetch()throw aTypeErroron every reconnect attempt.#onFetchErrorretries with the same value forever, so the client never reconnects andreadyStatestaysCONNECTING.é) is sent as a single Latin-1 byte rather than as UTF-8.The HTML specification (server-sent events, "reconnection" step of the processing model) says the
Last-Event-IDheader is the last event ID string, encoded as UTF-8.The fix
Encode the ID as UTF-8 and pass those bytes through as a byte string (one character per byte), so
fetch()puts exactly the UTF-8 bytes on the wire. This is the only place the ID is used as a header value. A plain loop is used for the conversion rather than spreading the bytes intoString.fromCharCode, so long IDs cannot hit argument limits.Before and after
Reproduction on Node v24.19.0: a local server sends
id: café-€1and closes the response, then logs theLast-Event-IDbytes it receives (decoded as UTF-8 for readability).Before (
main):After:
With an ID in the Latin-1 range only (
id: café), the server received the bytes636166e9before, and receives636166c3a9(UTF-8) after.Tests
Added a
test.eachintest/client.test.tswith three IDs: Latin-1 range (café), beyond Latin-1 (café-€1) and outside the BMP (id-😀). It uses a new/echo-last-event-idroute intest/helpers/server.ts, which sends the ID on the first connection and echoes theLast-Event-IDheader, decoded as UTF-8, on the reconnect.src/EventSource.tsand confirmed all three new tests fail (two of them withTimeout waiting for onMessage call count of 2), then restored it and confirmed they pass.npm test(Node, vitest): 55 passed, 1 skipped.npm run lint(oxlint plus bothtsc --noEmitruns) andoxfmt --checkpass.npm run test:types,npm run test:bun(55 passed, 1 skipped) andnpm run test:happy-dom(52 passed, 4 expected failures, as documented in CONTRIBUTING) pass as well.A patch changeset is included, as
src/is touched.