155 lines
6.2 KiB
TypeScript
155 lines
6.2 KiB
TypeScript
import { initSentryServer } from "./sentry.server";
|
|
import { startEntryRuntimeServices } from "./entry-runtime-bootstrap.server";
|
|
|
|
// IMPORTANT: Initialize Sentry as the very first action on the server
|
|
// entry so the SDK can hook into the Node runtime (HTTP, async hooks)
|
|
// before any other module runs. No-op when SENTRY_DSN_WEB is unset.
|
|
initSentryServer();
|
|
|
|
// Vite resolves this import to a surface-specific boot module. Core keeps its
|
|
// cache subscribers, while the Portal artifact never imports that Core graph.
|
|
startEntryRuntimeServices();
|
|
|
|
import type { AppLoadContext, EntryContext } from "react-router";
|
|
|
|
import isbot from "isbot";
|
|
|
|
import { PassThrough } from "node:stream";
|
|
import { ServerRouter } from "react-router";
|
|
import { createReadableStreamFromReadable } from "@react-router/node";
|
|
import { renderToPipeableStream } from "react-dom/server";
|
|
|
|
import { createInstance } from "i18next";
|
|
import { I18nextProvider, initReactI18next } from "react-i18next";
|
|
import { remixI18Next } from "./locale/i18next.server";
|
|
import { i18nConfig } from "./locale/i18n";
|
|
import { getServerI18nResourcesSnapshot, getServerRequestI18nLanguage, runWithServerI18nLanguage } from "./locale/i18next.global.server";
|
|
import { routes as otherRoutes } from "./other-routes.server";
|
|
import { createScopedLogger } from "./utils/logger.server";
|
|
import { shouldNoindex, X_ROBOTS_TAG_NOINDEX } from "./utils/seo/noindex.server";
|
|
import { getRequestCspNonce } from "./utils/security/contentSecurityPolicy";
|
|
|
|
const ssrLogger = createScopedLogger("ssr.entry");
|
|
|
|
// const ABORT_DELAY = 5_000;
|
|
// Reject/cancel all pending promises after 5 seconds
|
|
export const streamTimeout = 5000;
|
|
|
|
export async function runWithRequestI18nScope<T>(request: Request, callback: () => T | Promise<T>): Promise<T> {
|
|
const language = await remixI18Next.getLocale(request);
|
|
return runWithServerI18nLanguage(language, callback);
|
|
}
|
|
|
|
export default function handleRequest(
|
|
request: Request,
|
|
responseStatusCode: number,
|
|
responseHeaders: Headers,
|
|
reactRouterContext: EntryContext,
|
|
loadContext: AppLoadContext
|
|
) {
|
|
if (getServerRequestI18nLanguage()) {
|
|
return handleRequestWithinI18nScope(request, responseStatusCode, responseHeaders, reactRouterContext, loadContext);
|
|
}
|
|
|
|
// The production server wraps React Router before loaders/actions execute.
|
|
// Keep this entry-level fallback for development servers and direct tests.
|
|
return runWithRequestI18nScope(request, () => handleRequestWithinI18nScope(request, responseStatusCode, responseHeaders, reactRouterContext, loadContext));
|
|
}
|
|
|
|
async function handleRequestWithinI18nScope(
|
|
request: Request,
|
|
responseStatusCode: number,
|
|
responseHeaders: Headers,
|
|
reactRouterContext: EntryContext,
|
|
_loadContext: AppLoadContext
|
|
) {
|
|
const callbackName = isbot(request.headers.get("user-agent")) ? "onAllReady" : "onShellReady";
|
|
|
|
for (const handler of otherRoutes) {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
const otherRouteResponse = await handler(request, reactRouterContext);
|
|
if (otherRouteResponse) return otherRouteResponse;
|
|
}
|
|
|
|
// Internationalization (i18n).
|
|
const i18nInstance = createInstance();
|
|
const lng = getServerRequestI18nLanguage() ?? (await remixI18Next.getLocale(request));
|
|
const ns = remixI18Next.getRouteNamespaces(reactRouterContext);
|
|
const resources = getServerI18nResourcesSnapshot();
|
|
const cspNonce = getRequestCspNonce(request);
|
|
|
|
await i18nInstance
|
|
.use(initReactI18next) // Tell our instance to use react-i18next.
|
|
.init({
|
|
...i18nConfig, // Spread configuration.
|
|
lng, // Locale detected above.
|
|
ns, // Namespaces detected above.
|
|
resources,
|
|
// The process snapshot is already in memory; avoid scheduling another
|
|
// async initialization turn for every SSR request.
|
|
initImmediate: false,
|
|
});
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let shellRendered = false;
|
|
|
|
const { pipe, abort } = renderToPipeableStream(
|
|
<I18nextProvider i18n={i18nInstance}>
|
|
<ServerRouter context={reactRouterContext} url={request.url} nonce={cspNonce} />
|
|
</I18nextProvider>,
|
|
{
|
|
nonce: cspNonce,
|
|
[callbackName]: () => {
|
|
shellRendered = true;
|
|
const body = new PassThrough();
|
|
const stream = createReadableStreamFromReadable(body);
|
|
|
|
responseHeaders.set("Content-Type", "text/html; charset=utf-8");
|
|
// Default HTML responses to `no-store` so CDNs / browsers do not
|
|
// cache rendered documents that may contain per-user data (CSRF
|
|
// tokens, session-derived markup, feature flags). Loaders that
|
|
// serve cacheable HTML (marketing pages, public-tenant landing)
|
|
// can override by calling `headers.set("Cache-Control", ...)`
|
|
// themselves — `set()` here only runs when no value is present.
|
|
// See docs/ops/asset-cdn.md "Cache strategy".
|
|
if (!responseHeaders.has("Cache-Control")) {
|
|
responseHeaders.set("Cache-Control", "no-store");
|
|
}
|
|
|
|
// AK-INFRA1 — Staging-Härtung: jede HTML-Antwort als noindex markieren, wenn WEB_NOINDEX
|
|
// gesetzt ist (Staging). Greift zusätzlich zur robots.txt, auch ohne deren Abruf.
|
|
if (shouldNoindex() && !responseHeaders.has("X-Robots-Tag")) {
|
|
responseHeaders.set("X-Robots-Tag", X_ROBOTS_TAG_NOINDEX);
|
|
}
|
|
|
|
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) {
|
|
ssrLogger.error({ err: error }, "SSR streaming error");
|
|
}
|
|
},
|
|
}
|
|
);
|
|
|
|
// setTimeout(abort, ABORT_DELAY);
|
|
// Automatically timeout the React renderer after 6 seconds, which ensures
|
|
// React has enough time to flush down the rejected boundary contents
|
|
setTimeout(abort, streamTimeout + 1000);
|
|
});
|
|
}
|