Skip to content

fix(ssr): avoid pipeThrough(new TextEncoderStream()) in inline script to prevent Chrome iOS crash (#15486) - #15728

Closed
DoctorGuidance wants to merge 1 commit into
remix-run:mainfrom
DoctorGuidance:fix/ssr-stream-text-encoder-ios-crash
Closed

DoctorGuidance wants to merge 1 commit into
remix-run:mainfrom
DoctorGuidance:fix/ssr-stream-text-encoder-ios-crash

Conversation

@DoctorGuidance

Copy link
Copy Markdown

Closes #15486

Problem

In React Router 8's <Scripts /> component, the initial inline SSR script sets up the streaming handoff via:

window.__reactRouterContext.stream = new ReadableStream({
  start(controller) {
    window.__reactRouterContext.streamController = controller;
  }
}).pipeThrough(new TextEncoderStream());

In Chrome Mobile iOS (and other WKWebView-based apps like Google Search), Chrome injects JavaScript via WKUserScripts at documentStart that intercepts or wraps ReadableStream.prototype.pipeThrough and TextEncoderStream. This causes mutual recursion between injected handlers (Tk ↔ Rk), which overflows WKWebView's restricted JS call stack and throws RangeError: Maximum call stack size exceeded.

Because this error occurs in the first inline script of the page:

  1. window.__reactRouterContext.stream and streamController are never initialized.
  2. Subsequent inline <script> tags from <StreamTransfer> fail with TypeError: Cannot read properties of undefined (reading 'enqueue').
  3. Hydration fails completely, leaving the app non-interactive on Chrome Mobile iOS.

Solution

Replace pipeThrough(new TextEncoderStream()) with a manual TextEncoder wrapper directly inside the ReadableStream start handler. This eliminates both pipeThrough and TextEncoderStream from the inline script while preserving the exact same ReadableStream<Uint8Array> output and enqueue/close/error controller contract for single fetch streaming:

window.__reactRouterContext.stream = new ReadableStream({
  start(controller) {
    var enc = new TextEncoder();
    window.__reactRouterContext.streamController = {
      enqueue: function(c) {
        controller.enqueue(typeof c === "string" ? enc.encode(c) : c);
      },
      close: function() { controller.close(); },
      error: function(e) { controller.error(e); }
    };
  }
});

Testing

  • Added regression test in packages/react-router/__tests__/dom/ssr/components-test.tsx verifying the inline SSR script renders without pipeThrough or TextEncoderStream.
  • Updated test mock stream controller in components-test.tsx and verified all 25 SSR and hydration tests pass.
  • Verified TypeScript compilation (tsc --noEmit) passes with zero errors.

@brophdawg11

Copy link
Copy Markdown
Contributor

closing for the same reason #15487 was closed

@brophdawg11 brophdawg11 closed this Nov 3, 2026
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.

SSR: pipeThrough(new TextEncoderStream()) crashes Chrome Mobile iOS / WKWebView apps (RangeError: Maximum call stack size exceeded)

2 participants