Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

90 linesCodeBlameRaw
1import type { EntryContext, RouterContextProvider, ServerInstrumentation } from "react-router";
2import { ServerRouter } from "react-router";
3import { isbot } from "isbot";
4import { renderToReadableStream } from "react-dom/server";
5
6import { addresses } from "./lib/addresses.server";
7import { NonceContext } from "./lib/nonce";
8import { makeNonce, pagePolicy } from "./lib/page-headers";
9import { recordHandler } from "./lib/perf.server";
10import { agentPage } from "./lib/agent-pages";
11
12// React Router's own server entry, plus the timing of every loader and
13// action for the Server-Timing header (lib/perf.server.ts).
14
15export const streamTimeout = 5_000;
16
17export const instrumentations: ServerInstrumentation[] = [
18 {
19 route(route) {
20 route.instrument({
21 async loader(handle) {
22 const started = Date.now();
23 await handle();
24 recordHandler(route.id, "loader", Date.now() - started);
25 },
26 async action(handle) {
27 const started = Date.now();
28 await handle();
29 recordHandler(route.id, "action", Date.now() - started);
30 },
31 });
32 },
33 },
34];
35
36export default async function handleRequest(
37 request: Request,
38 responseStatusCode: number,
39 responseHeaders: Headers,
40 routerContext: EntryContext,
41 _loadContext: RouterContextProvider,
42) {
43 const agent = agentPage(request, responseStatusCode, responseHeaders);
44 if (agent) return agent;
45 // https://httpwg.org/specs/rfc9110.html#HEAD
46 if (request.method.toUpperCase() === "HEAD") {
47 responseHeaders.set("Content-Type", "text/html");
48 return new Response(null, {
49 status: responseStatusCode,
50 headers: responseHeaders,
51 });
52 }
53
54 let shellRendered = false;
55 const userAgent = request.headers.get("user-agent");
56
57 // The page's inline scripts carry this nonce, and its policy allows only
58 // them (lib/page-headers.ts). Not in development, where Vite adds its own.
59 const nonce = import.meta.env.DEV ? undefined : makeNonce();
60 const body = await renderToReadableStream(
61 <NonceContext value={nonce}>
62 <ServerRouter context={routerContext} url={request.url} nonce={nonce} />
63 </NonceContext>,
64 {
65 nonce,
66 signal: AbortSignal.timeout(streamTimeout + 1000),
67 onError(error: unknown) {
68 responseStatusCode = 500;
69 // Errors while streaming after the shell; those in the shell reject
70 // and are logged by React Router.
71 if (shellRendered) {
72 console.error(error);
73 }
74 },
75 },
76 );
77 shellRendered = true;
78
79 // Crawlers get the whole page at once, deferred panels included.
80 if ((userAgent && isbot(userAgent)) || routerContext.isSpaMode) {
81 await body.allReady;
82 }
83
84 responseHeaders.set("Content-Type", "text/html");
85 if (nonce) responseHeaders.set("Content-Security-Policy", pagePolicy(nonce, addresses().usercontent));
86 return new Response(body, {
87 headers: responseHeaders,
88 status: responseStatusCode,
89 });
90}