123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152 |
- 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(
- <RemixServer
- context={remixContext}
- url={request.url}
- abortDelay={ABORT_DELAY}
- />,
- {
- 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(
- <RemixServer
- context={remixContext}
- url={request.url}
- abortDelay={ABORT_DELAY}
- />,
- {
- 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);
- });
- }
|