Skip to content

fix(react-router): Carry multi-byte UTF-8 across SSR stream chunk boundaries - #23307

Open
logaretm wants to merge 2 commits into
developfrom
awad/react-router-utf8-stream-decode
Open

fix(react-router): Carry multi-byte UTF-8 across SSR stream chunk boundaries#23307
logaretm wants to merge 2 commits into
developfrom
awad/react-router-utf8-stream-decode

Conversation

@logaretm

@logaretm logaretm commented Aug 11, 2026

Copy link
Copy Markdown
Member

Fixes the UTF-8 corruption in #23305: a multi-byte character split across an SSR stream chunk boundary got replaced by two U+FFFD, breaking React hydration (#418) and wiping the injected trace meta tags on re-render.

Both injectors decoded each chunk with a fresh, non-streaming decoder, so a split character's trailing bytes were flushed as U+FFFD instead of carried over. Fixed by using one hoisted TextDecoder with { stream: true }, matching what the Astro and SolidStart middleware already do.

Tests are deterministic, since the bug only fires when a char lands exactly on a chunk boundary and an e2e sweep would just flake.

closes #23305

…unks

Adds regression tests for `injectTraceMetaTags` (Cloudflare) and
`getMetaTagTransformer` (Node) that split a multi-byte character across
stream chunk boundaries. These fail against the current per-chunk,
non-streaming decode, which flushes the split character as U+FFFD.
…ndaries

`injectTraceMetaTags` and `getMetaTagTransformer` decoded each stream
chunk with a fresh, non-streaming decoder. A multi-byte character split
across a chunk boundary was flushed as two U+FFFD and written into the
response, breaking React hydration (#418) and wiping the injected trace
meta tags on re-render.

Use a single hoisted `TextDecoder` with `{ stream: true }` so an
incomplete trailing sequence is carried into the next chunk, matching the
pattern already used in the Astro and SolidStart middleware.
@logaretm

Copy link
Copy Markdown
Member Author

bugbot run

@github-actions

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size % Change Change
@sentry/browser 30.29 kB - -
@sentry/browser - with treeshaking flags 28.47 kB - -
@sentry/browser - with treeshaking flags tracing without tracing 26.8 kB - -
@sentry/browser (incl. Tracing) 48.54 kB - -
@sentry/browser (incl. Tracing + Span Streaming) 48.56 kB - -
@sentry/browser (incl. Tracing, Profiling) 51.44 kB - -
@sentry/browser (incl. Tracing, Replay) 87.95 kB - -
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 77.36 kB - -
@sentry/browser (incl. Tracing, Replay with Canvas) 92.66 kB - -
@sentry/browser (incl. Tracing, Replay, Feedback) 105.36 kB - -
@sentry/browser (incl. Feedback) 47.61 kB - -
@sentry/browser (incl. sendFeedback) 35.11 kB - -
@sentry/browser (incl. FeedbackAsync) 40.26 kB - -
@sentry/browser (incl. Metrics) 31.28 kB - -
@sentry/browser (incl. Logs) 31.53 kB - -
@sentry/browser (incl. Metrics & Logs) 32.2 kB - -
@sentry/react 32.09 kB - -
@sentry/react (incl. Tracing) 50.73 kB - -
@sentry/vue 35.34 kB - -
@sentry/vue (incl. Tracing) 50.5 kB - -
@sentry/svelte 30.31 kB - -
CDN Bundle 31.59 kB - -
CDN Bundle (incl. Tracing) 48.87 kB - -
CDN Bundle (incl. Logs, Metrics) 33.8 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) 50.83 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) 74.34 kB - -
CDN Bundle (incl. Tracing, Replay) 86.46 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 88.33 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) 92.17 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 94.14 kB - -
CDN Bundle - uncompressed 93.77 kB - -
CDN Bundle (incl. Tracing) - uncompressed 146.66 kB - -
CDN Bundle (incl. Logs, Metrics) - uncompressed 100.17 kB - -
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 152.46 kB - -
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 229.1 kB - -
CDN Bundle (incl. Tracing, Replay) - uncompressed 265.92 kB - -
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 271.7 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 279.61 kB - -
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 285.38 kB - -
@sentry/nextjs (client) 53.29 kB - -
@sentry/sveltekit (client) 48.96 kB - -
@sentry/core/server 65.44 kB - -
@sentry/core/browser 51.8 kB - -
@sentry/node 117.98 kB +0.01% +1 B 🔺
@sentry/node/import (ESM hook with diagnostics-channel injection) 0 B added added
@sentry/node - without tracing 82.09 kB +0.01% +1 B 🔺
@sentry/aws-serverless 91.5 kB +0.01% +1 B 🔺
@sentry/cloudflare (withSentry) - minified 214.1 kB - -
@sentry/cloudflare (withSentry) 528.64 kB - -

View base workflow run

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit e920ab3. Configure here.

@logaretm
logaretm marked this pull request as ready for review August 11, 2026 18:30
@logaretm
logaretm requested a review from a team as a code owner August 11, 2026 18:30
@logaretm
logaretm requested review from chargome and s1gr1d and removed request for a team August 11, 2026 18:30
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.

injectTraceMetaTags corrupts multi-byte UTF-8 split across SSR stream chunks (React hydration error #418)

1 participant