| | 0 | 1 | | import { PassThrough } from "node:stream"; |
| | | 2 | | |
| | | 3 | | import type { AppLoadContext, EntryContext } from "react-router"; |
| | | 4 | | import { createReadableStreamFromReadable } from "@react-router/node"; |
| | | 5 | | import { ServerRouter } from "react-router"; |
| | | 6 | | import { isbot } from "isbot"; |
| | | 7 | | import type { RenderToPipeableStreamOptions } from "react-dom/server"; |
| | | 8 | | import { renderToPipeableStream } from "react-dom/server"; |
| | | 9 | | |
| | | 10 | | import { startNodeOpenTelemetry } from "~/otel/server"; |
| | | 11 | | import { withSsrRequestSpan } from "~/otel/ssr-request"; |
| | | 12 | | |
| | 0 | 13 | | startNodeOpenTelemetry(); |
| | | 14 | | |
| | 0 | 15 | | export const streamTimeout = 5_000; |
| | | 16 | | |
| | 0 | 17 | | export default function handleRequest( |
| | | 18 | | request: Request, |
| | | 19 | | responseStatusCode: number, |
| | | 20 | | responseHeaders: Headers, |
| | | 21 | | routerContext: EntryContext, |
| | | 22 | | loadContext: AppLoadContext, |
| | | 23 | | // If you have middleware enabled: |
| | | 24 | | // loadContext: RouterContextProvider |
| | | 25 | | ) { |
| | | 26 | | // https://httpwg.org/specs/rfc9110.html#HEAD |
| | 0 | 27 | | if (request.method.toUpperCase() === "HEAD") { |
| | 0 | 28 | | return new Response(null, { |
| | | 29 | | status: responseStatusCode, |
| | | 30 | | headers: responseHeaders, |
| | | 31 | | }); |
| | | 32 | | } |
| | | 33 | | |
| | 0 | 34 | | return withSsrRequestSpan(request, () => renderHtmlResponse( |
| | | 35 | | request, |
| | | 36 | | responseStatusCode, |
| | | 37 | | responseHeaders, |
| | | 38 | | routerContext, |
| | | 39 | | loadContext, |
| | | 40 | | )); |
| | | 41 | | } |
| | | 42 | | |
| | 0 | 43 | | async function renderHtmlResponse( |
| | | 44 | | request: Request, |
| | | 45 | | responseStatusCode: number, |
| | | 46 | | responseHeaders: Headers, |
| | | 47 | | routerContext: EntryContext, |
| | | 48 | | _loadContext: AppLoadContext, |
| | | 49 | | ): Promise<Response> { |
| | 0 | 50 | | return new Promise((resolve, reject) => { |
| | 0 | 51 | | let shellRendered = false; |
| | 0 | 52 | | const userAgent = request.headers.get("user-agent"); |
| | | 53 | | |
| | | 54 | | // Ensure requests from bots and SPA Mode renders wait for all content to load before responding |
| | | 55 | | // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers- |
| | | 56 | | const readyOption: keyof RenderToPipeableStreamOptions = |
| | 0 | 57 | | (userAgent && isbot(userAgent)) || routerContext.isSpaMode |
| | | 58 | | ? "onAllReady" |
| | | 59 | | : "onShellReady"; |
| | | 60 | | |
| | | 61 | | // Abort the rendering stream after the `streamTimeout` so it has time to |
| | | 62 | | // flush down the rejected boundaries |
| | 0 | 63 | | let timeoutId: ReturnType<typeof setTimeout> | undefined = setTimeout( |
| | 0 | 64 | | () => abort(), |
| | | 65 | | streamTimeout + 1000, |
| | | 66 | | ); |
| | | 67 | | |
| | 0 | 68 | | const { pipe, abort } = renderToPipeableStream( |
| | | 69 | | <ServerRouter context={routerContext} url={request.url} />, |
| | | 70 | | { |
| | 0 | 71 | | [readyOption]() { |
| | 0 | 72 | | shellRendered = true; |
| | 0 | 73 | | const body = new PassThrough({ |
| | 0 | 74 | | final(callback) { |
| | | 75 | | // Clear the timeout to prevent retaining the closure and memory leak |
| | 0 | 76 | | clearTimeout(timeoutId); |
| | 0 | 77 | | timeoutId = undefined; |
| | 0 | 78 | | callback(); |
| | | 79 | | }, |
| | | 80 | | }); |
| | 0 | 81 | | const stream = createReadableStreamFromReadable(body); |
| | | 82 | | |
| | 0 | 83 | | responseHeaders.set("Content-Type", "text/html"); |
| | | 84 | | |
| | 0 | 85 | | pipe(body); |
| | | 86 | | |
| | 0 | 87 | | resolve( |
| | | 88 | | new Response(stream, { |
| | | 89 | | headers: responseHeaders, |
| | | 90 | | status: responseStatusCode, |
| | | 91 | | }), |
| | | 92 | | ); |
| | | 93 | | }, |
| | 0 | 94 | | onShellError(error: unknown) { |
| | 0 | 95 | | reject(error); |
| | | 96 | | }, |
| | 0 | 97 | | onError(error: unknown) { |
| | 0 | 98 | | responseStatusCode = 500; |
| | | 99 | | // Log streaming rendering errors from inside the shell. Don't log |
| | | 100 | | // errors encountered during initial shell rendering since they'll |
| | | 101 | | // reject and get logged in handleDocumentRequest. |
| | 0 | 102 | | if (shellRendered) { |
| | 0 | 103 | | console.error(error); |
| | | 104 | | } |
| | | 105 | | }, |
| | | 106 | | }, |
| | | 107 | | ); |
| | | 108 | | }); |
| | | 109 | | } |
| | | 110 | | |