Skip to content

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

Merged
logaretm merged 1 commit into
v10from
awad/v10-backport-react-router-utf8-ssr-chunks
Aug 13, 2026
Merged

fix(v10/react-router): Carry multi-byte UTF-8 across SSR stream chunk boundaries#23421
logaretm merged 1 commit into
v10from
awad/v10-backport-react-router-utf8-ssr-chunks

Conversation

@logaretm

Copy link
Copy Markdown
Member

Backport of: #23307

@logaretm
logaretm marked this pull request as ready for review August 13, 2026 16:02
@logaretm
logaretm requested a review from a team as a code ownerAugust 13, 2026 16:02
@logaretm
logaretm requested review from chargome and s1gr1d and removed request for a teamAugust 13, 2026 16:02

@cursorcursorBot 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.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit e20ed86. Configure here.

}

const encoder = new TextEncoder();
const html = value instanceof Uint8Array ? new TextDecoder().decode(value) : String(value);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Streaming decoder not flushed at end

Low Severity

A streaming TextDecoder with { stream: true } holds incomplete UTF-8 until a later chunk or a final flush. injectTraceMetaTags closes on done without a final decoder.decode(), and getMetaTagTransformer has no Transform flush, so leftover bytes at end-of-stream are dropped instead of emitted.

Additional Locations (1)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit e20ed86. Configure here.

Comment on lines 18 to +26
const htmlMetaTagTransformer = new Transform({
transform(chunk, _encoding, callback) {
const html = Buffer.isBuffer(chunk) ? chunk.toString() : String(chunk);
const html = Buffer.isBuffer(chunk) ? decoder.decode(chunk, { stream: true }) : String(chunk);
if (html.includes(headClosingTag)) {
const modifiedHtml = html.replace(headClosingTag, `${getTraceMetaTags()}${headClosingTag}`);
callback(null, modifiedHtml);
return;
}
callback(null, chunk);
callback(null, html);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Bug: The streaming TextDecoder is not flushed when the stream ends, which can cause data loss if the stream terminates with an incomplete multi-byte character.
Severity: LOW

Suggested Fix

In getMetaTagTransformer.ts, add a flush(callback) method to the Transform stream options and call callback(null, decoder.decode()) within it. In cloudflare/index.ts, before calling controller.close(), enqueue the result of the final decoder.decode() call to flush any remaining buffered data.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.
Location: packages/react-router/src/server/getMetaTagTransformer.ts#L18-L26
Potential issue: When using `TextDecoder` with the `{ stream: true }` option, a final
call to `decoder.decode()` without arguments is required to flush any buffered bytes,
such as those from an incomplete multi-byte character at the end of the stream. This
final flushing call is missing in two locations. In `getMetaTagTransformer.ts`, the
Node.js `Transform` stream is missing a `flush` callback to handle the end of the
stream. In `cloudflare/index.ts`, the stream is closed via `controller.close()` without
a final decode. This can lead to data loss if an input stream happens to end with an
incomplete UTF-8 character sequence, which is rare but possible.

Also affects:

  • packages/react-router/src/cloudflare/index.ts:28~31

Did we get this right? 👍 / 👎 to inform future reviews.

@github-actions

Copy link
Copy Markdown
Contributor

size-limit report 📦

PathSize% ChangeChange
@sentry/browser27.75 kBaddedadded
@sentry/browser - with treeshaking flags26.19 kBaddedadded
@sentry/browser (incl. Tracing)46.59 kBaddedadded
@sentry/browser (incl. Tracing + Span Streaming)48.37 kBaddedadded
@sentry/browser (incl. Tracing, Profiling)51.34 kBaddedadded
@sentry/browser (incl. Tracing, Replay)85.83 kBaddedadded
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags75.47 kBaddedadded
@sentry/browser (incl. Tracing, Replay with Canvas)90.55 kBaddedadded
@sentry/browser (incl. Tracing, Replay, Feedback)103.19 kBaddedadded
@sentry/browser (incl. Feedback)44.92 kBaddedadded
@sentry/browser (incl. sendFeedback)32.54 kBaddedadded
@sentry/browser (incl. FeedbackAsync)37.67 kBaddedadded
@sentry/browser (incl. Metrics)28.83 kBaddedadded
@sentry/browser (incl. Logs)29.06 kBaddedadded
@sentry/browser (incl. Metrics & Logs)29.75 kBaddedadded
@sentry/react29.55 kBaddedadded
@sentry/react (incl. Tracing)48.85 kBaddedadded
@sentry/vue33.17 kBaddedadded
@sentry/vue (incl. Tracing)48.55 kBaddedadded
@sentry/svelte27.77 kBaddedadded
CDN Bundle30.14 kBaddedadded
CDN Bundle (incl. Tracing)48.55 kBaddedadded
CDN Bundle (incl. Logs, Metrics)31.72 kBaddedadded
CDN Bundle (incl. Tracing, Logs, Metrics)49.86 kBaddedadded
CDN Bundle (incl. Replay, Logs, Metrics)70.98 kBaddedadded
CDN Bundle (incl. Tracing, Replay)86.05 kBaddedadded
CDN Bundle (incl. Tracing, Replay, Logs, Metrics)87.36 kBaddedadded
CDN Bundle (incl. Tracing, Replay, Feedback)91.86 kBaddedadded
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics)93.12 kBaddedadded
CDN Bundle - uncompressed89.86 kBaddedadded
CDN Bundle (incl. Tracing) - uncompressed146.74 kBaddedadded
CDN Bundle (incl. Logs, Metrics) - uncompressed94.58 kBaddedadded
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed150.72 kBaddedadded
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed219.34 kBaddedadded
CDN Bundle (incl. Tracing, Replay) - uncompressed265.98 kBaddedadded
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed269.94 kBaddedadded
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed279.68 kBaddedadded
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed283.64 kBaddedadded
@sentry/nextjs (client)51.39 kBaddedadded
@sentry/sveltekit (client)47.01 kBaddedadded
@sentry/core/server80.6 kBaddedadded
@sentry/core/browser66.83 kBaddedadded
@sentry/node-core63.24 kBaddedadded
@sentry/node125.88 kBaddedadded
@sentry/node (incl. diagnostics channel injection)151.32 kBaddedadded
@sentry/node/import (ESM hook with diagnostics-channel injection)70.03 kBaddedadded
@sentry/node/light51.4 kBaddedadded
@sentry/node - without tracing74.93 kBaddedadded
@sentry/aws-serverless84.2 kBaddedadded
@sentry/cloudflare (withSentry) - minified202 kBaddedadded
@sentry/cloudflare (withSentry)496.62 kBaddedadded

@logaretm
logaretm merged commit 245d807 into v10Aug 13, 2026
50 checks passed
@logaretm
logaretm deleted the awad/v10-backport-react-router-utf8-ssr-chunks branch August 13, 2026 16:28
Sign up for freeto 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.

2 participants

@logaretm@nicohrubec