import { PassThrough } from "node:stream"; import { createReadableStreamFromReadable } from "@react-router/node"; import { isbot } from "isbot"; import { renderToPipeableStream } from "react-dom/server"; import type { ActionFunctionArgs, AppLoadContext, EntryContext, LoaderFunctionArgs, } from "react-router"; import { isRouteErrorResponse, ServerRouter } from "react-router"; import { notifyError } from "./lib/errors"; export const streamTimeout = 60 * 20 * 1_000; export default function handleRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, reactRouterContext: EntryContext, // This is ignored so we can keep it in the template for visibility. Feel // free to delete this parameter in your app if you're not using it! // eslint-disable-next-line @typescript-eslint/no-unused-vars loadContext: AppLoadContext ) { return isbot(request.headers.get("user-agent") || "") ? handleBotRequest( request, responseStatusCode, responseHeaders, reactRouterContext ) : handleBrowserRequest( request, responseStatusCode, responseHeaders, reactRouterContext ); } function handleBotRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, reactRouterContext: EntryContext ) { return new Promise((resolve, reject) => { let shellRendered = false; let timeoutId: NodeJS.Timeout; const { pipe, abort } = renderToPipeableStream( , { onAllReady() { shellRendered = true; const body = new PassThrough(); const stream = createReadableStreamFromReadable(body); responseHeaders.set("Content-Type", "text/html"); clearTimeout(timeoutId); resolve( new Response(stream, { headers: responseHeaders, status: responseStatusCode, }) ); pipe(body); }, onShellError(error: unknown) { clearTimeout(timeoutId); 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); } }, } ); timeoutId = setTimeout(abort, streamTimeout); }); } function handleBrowserRequest( request: Request, responseStatusCode: number, responseHeaders: Headers, reactRouterContext: EntryContext ) { return new Promise((resolve, reject) => { let shellRendered = false; let timeoutId: NodeJS.Timeout; const { pipe, abort } = renderToPipeableStream( , { onShellReady() { shellRendered = true; const body = new PassThrough(); const stream = createReadableStreamFromReadable(body); responseHeaders.set("Content-Type", "text/html"); clearTimeout(timeoutId); resolve( new Response(stream, { headers: responseHeaders, status: responseStatusCode, }) ); pipe(body); }, onShellError(error: unknown) { clearTimeout(timeoutId); 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); } }, } ); timeoutId = setTimeout(abort, streamTimeout); }); } export function handleError( error: any, { request, params, context }: LoaderFunctionArgs | ActionFunctionArgs ) { // https://github.com/remix-run/remix/discussions/8933 if (request.signal.aborted || isRouteErrorResponse(error)) { return; } if (error?.["status"] === 404) { return; } if (error instanceof Error) { notifyError(error, "unhandled remix error"); } else { console.error("error", error); } }