Files
sharper.ly/apps/web/app/entry.server.tsx
T

123 lines
4.3 KiB
TypeScript
Raw Normal View History

2026-09-11 02:46:56 +02:00
/* ============================================================================
SERVER ENTRY
----------------------------------------------------------------------------
The stock React Router streaming entry plus two SHARP-ER additions:
- `Content-Language` is stamped from the URL's locale prefix, so the
header agrees with <html lang> that root.tsx renders. `Vary` is NOT set
globally: every page URL carries its own locale, so only the bare "/"
redirect varies on Accept-Language and it sets the header itself.
- `handleError` logs server errors without the noise of client aborts.
========================================================================= */
import { PassThrough } from "node:stream";
import type { EntryContext, RouterContextProvider } from "react-router";
import { createReadableStreamFromReadable } from "@react-router/node";
import { ServerRouter } from "react-router";
import { isbot } from "isbot";
import type { RenderToPipeableStreamOptions } from "react-dom/server";
import { renderToPipeableStream } from "react-dom/server";
import { DEFAULT_LOCALE, getLocaleFromPathname } from "~/i18n/config";
export const streamTimeout = 5_000;
/** The locale a URL addresses, for the `Content-Language` response header. */
function contentLanguage(request: Request): string {
try {
return getLocaleFromPathname(new URL(request.url).pathname) ?? DEFAULT_LOCALE;
} catch {
return DEFAULT_LOCALE;
}
}
export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
routerContext: EntryContext,
loadContext: RouterContextProvider,
) {
// https://httpwg.org/specs/rfc9110.html#HEAD
if (request.method.toUpperCase() === "HEAD") {
return new Response(null, {
status: responseStatusCode,
headers: responseHeaders,
});
}
return new Promise((resolve, reject) => {
let shellRendered = false;
let userAgent = request.headers.get("user-agent");
// Ensure requests from bots and SPA Mode renders wait for all content to load before responding
// https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation
let readyOption: keyof RenderToPipeableStreamOptions =
(userAgent && isbot(userAgent)) || routerContext.isSpaMode
? "onAllReady"
: "onShellReady";
// Abort the rendering stream after the `streamTimeout` so it has time to
// flush down the rejected boundaries
let timeoutId: ReturnType<typeof setTimeout> | undefined = setTimeout(
() => abort(),
streamTimeout + 1000,
);
const { pipe, abort } = renderToPipeableStream(
<ServerRouter context={routerContext} url={request.url} />,
{
[readyOption]() {
shellRendered = true;
const body = new PassThrough({
final(callback) {
// Clear the timeout to prevent retaining the closure and memory leak
clearTimeout(timeoutId);
timeoutId = undefined;
callback();
},
});
const stream = createReadableStreamFromReadable(body);
responseHeaders.set("Content-Type", "text/html");
responseHeaders.set("Content-Language", contentLanguage(request));
pipe(body);
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
}),
);
},
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);
}
},
},
);
});
}
/**
* Central server-side error logging. A request the client walked away from is
* not an application error, so it is not logged as one.
*/
export function handleError(
error: unknown,
{ request }: { request: Request },
): void {
if (request.signal.aborted) return;
console.error(`[sharp-er] ${request.method} ${request.url}`, error);
}