import { PassThrough } from "node:stream"; import type { AppLoadContext, EntryContext } from "@remix-run/node"; import { createReadableStreamFromReadable } from "@remix-run/node"; import { RemixServer } from "@remix-run/react"; import * as isbotModule from "isbot"; import { renderToPipeableStream } from "react-dom/server"; const ABORT_DELAY = 5_000; export default function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext, loadContext: AppLoadContext ) { return isBotRequest(request.headers.get("user-agent")) ? handleBotRequest( request, responseStatusCode, responseHeaders, remixContext ) : handleBrowserRequest( request, responseStatusCode, responseHeaders, remixContext ); } // We have some Remix apps in the wild already running with isbot@3 so we need // to maintain backwards compatibility even though we want new apps to use // isbot@4. That way, we can ship this as a minor Semver update to @remix-run/dev. function isBotRequest(userAgent: string | null) { if (!userAgent) { return false; } // isbot >= 3.8.0, >4 if ("isbot" in isbotModule && typeof isbotModule.isbot === "function") { return isbotModule.isbot(userAgent); } // isbot < 3.8.0 if ("default" in isbotModule && typeof isbotModule.default === "function") { return isbotModule.default(userAgent); } return false; } function handleBotRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext ) { return new Promise((resolve, reject) => { let shellRendered = false; const { pipe, abort } = renderToPipeableStream( , { onAllReady() { shellRendered = true; const body = new PassThrough(); const stream = createReadableStreamFromReadable(body); responseHeaders.set("Content-Type", "text/html"); resolve( new Response(stream, { headers: responseHeaders, status: responseStatusCode, }) ); pipe(body); }, onShellError(error: unknown) { reject(error); }, onError(error: unknown) { responseStatusCode = 500; // Log streaming rendering errors from inside the shell. Don't log // errors encountered during initial shell rendering since they'll // reject and get logged in handleDocumentRequest. if (shellRendered) { console.error(error); } }, } ); setTimeout(abort, ABORT_DELAY); }); } function handleBrowserRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, remixContext: EntryContext ) { return new Promise((resolve, reject) => { let shellRendered = false; const { pipe, abort } = renderToPipeableStream( , { onShellReady() { shellRendered = true; const body = new PassThrough(); const stream = createReadableStreamFromReadable(body); responseHeaders.set("Content-Type", "text/html"); resolve( new Response(stream, { headers: responseHeaders, status: responseStatusCode, }) ); pipe(body); }, onShellError(error: unknown) { reject(error); }, onError(error: unknown) { responseStatusCode = 500; // Log streaming rendering errors from inside the shell. Don't log // errors encountered during initial shell rendering since they'll // reject and get logged in handleDocumentRequest. if (shellRendered) { console.error(error); } }, } ); setTimeout(abort, ABORT_DELAY); }); }