UNPKG

react-router

Version:
346 lines (345 loc) • 14.6 kB
/** * react-router v8.0.0 * * Copyright (c) Remix Software Inc. * * This source code is licensed under the MIT license found in the * LICENSE.md file in the root directory of this source tree. * * @license MIT */ import { ErrorResponseImpl, RouterContextProvider, defaultMapRouteProperties, isRouteErrorResponse, removeTrailingSlash, stripBasename } from "../router/utils.js"; import { instrumentHandler } from "../router/instrumentation.js"; import { createStaticHandler, getStaticContextFromError, isMutationMethod, isRedirectResponse, isResponse } from "../router/router.js"; import { getManifestPath } from "../dom/ssr/fog-of-war.js"; import { createEntryRouteModules } from "./entry.js"; import { isServerMode } from "./mode.js"; import { sanitizeErrors, serializeError } from "./errors.js"; import { matchServerRoutes } from "./routeMatching.js"; import { getBuildTimeHeader, getDevServerHooks } from "./dev.js"; import { createStaticHandlerDataRoutes } from "./routes.js"; import { createServerHandoffString } from "./serverHandoff.js"; import { getDocumentHeaders } from "./headers.js"; import { throwIfPotentialCSRFAttack } from "../actions.js"; import { getNormalizedPath } from "./urls.js"; import { SERVER_NO_BODY_STATUS_CODES, encodeViaTurboStream, generateSingleFetchRedirectResponse, singleFetchAction, singleFetchLoaders } from "./single-fetch.js"; //#region lib/server-runtime/server.ts function derive(build, mode) { let dataRoutes = createStaticHandlerDataRoutes(build.routes); let serverMode = isServerMode(mode) ? mode : "production"; let staticHandler = createStaticHandler(dataRoutes, { basename: build.basename, mapRouteProperties: defaultMapRouteProperties, instrumentations: build.entry.module.instrumentations, future: build.future }); let errorHandler = build.entry.module.handleError || ((error, { request }) => { if (serverMode !== "test" && !request.signal.aborted) console.error(isRouteErrorResponse(error) && error.error ? error.error : error); }); let requestHandler = async (request, initialContext) => { let params = {}; let loadContext; let handleError = (error) => { if (mode === "development") getDevServerHooks()?.processRequestError?.(error); errorHandler(error, { context: loadContext, params, request }); }; if (initialContext && !(initialContext instanceof RouterContextProvider)) { let error = /* @__PURE__ */ new Error("Invalid `context` value provided to `handleRequest`. You must return an instance of `RouterContextProvider` from your `getLoadContext` function."); handleError(error); return returnLastResortErrorResponse(error, serverMode); } loadContext = initialContext || new RouterContextProvider(); let requestUrl = new URL(request.url); let normalizedPathname = getNormalizedPath(request).pathname; let isSpaMode = getBuildTimeHeader(request, "X-React-Router-SPA-Mode") === "yes"; if (!build.ssr) { let decodedPath = decodeURI(normalizedPathname); if (build.basename && build.basename !== "/") { let strippedPath = stripBasename(decodedPath, build.basename); if (strippedPath == null) { errorHandler(new ErrorResponseImpl(404, "Not Found", `Refusing to prerender the \`${decodedPath}\` path because it does not start with the basename \`${build.basename}\``), { context: loadContext, params, request }); return new Response("Not Found", { status: 404, statusText: "Not Found" }); } decodedPath = strippedPath; } if (build.prerender.length === 0) isSpaMode = true; else if (!build.prerender.some((p) => removeTrailingSlash(p) === removeTrailingSlash(decodedPath))) if (requestUrl.pathname.endsWith(".data")) { errorHandler(new ErrorResponseImpl(404, "Not Found", `Refusing to SSR the path \`${decodedPath}\` because \`ssr:false\` is set and the path is not included in the \`prerender\` config, so in production the path will be a 404.`), { context: loadContext, params, request }); return new Response("Not Found", { status: 404, statusText: "Not Found" }); } else isSpaMode = true; } let manifestUrl = getManifestPath(build.routeDiscovery.manifestPath, build.basename); if (build.routeDiscovery.mode === "lazy" && requestUrl.pathname === manifestUrl) try { return await handleManifestRequest(build, staticHandler.dataRoutes, staticHandler._internalRouteBranches, requestUrl); } catch (e) { handleError(e); return new Response("Unknown Server Error", { status: 500 }); } let matches = matchServerRoutes(build.routes, staticHandler.dataRoutes, staticHandler._internalRouteBranches, normalizedPathname, build.basename); if (matches && matches.length > 0) Object.assign(params, matches[0].params); let response; if (requestUrl.pathname.endsWith(".data")) { response = await handleSingleFetchRequest(serverMode, build, staticHandler, request, loadContext, handleError); if (isRedirectResponse(response)) response = generateSingleFetchRedirectResponse(response, request, build, serverMode); if (build.entry.module.handleDataRequest) { response = await build.entry.module.handleDataRequest(response, { context: loadContext, params: matches ? matches[0].params : {}, request }); if (isRedirectResponse(response)) response = generateSingleFetchRedirectResponse(response, request, build, serverMode); } } else if (!isSpaMode && matches && matches[matches.length - 1].route.module.default == null && matches[matches.length - 1].route.module.ErrorBoundary == null) response = await handleResourceRequest(serverMode, build, staticHandler, matches.slice(-1)[0].route.id, request, loadContext, handleError); else { let { pathname } = requestUrl; let criticalCss = void 0; if (build.unstable_getCriticalCss) criticalCss = await build.unstable_getCriticalCss({ pathname }); else if (mode === "development" && getDevServerHooks()?.getCriticalCss) criticalCss = await getDevServerHooks()?.getCriticalCss?.(pathname); response = await handleDocumentRequest(serverMode, build, staticHandler, request, loadContext, handleError, isSpaMode, criticalCss); } if (request.method === "HEAD") return new Response(null, { headers: response.headers, status: response.status, statusText: response.statusText }); return response; }; if (build.entry.module.instrumentations) requestHandler = instrumentHandler(requestHandler, build.entry.module.instrumentations.map((i) => i.handler).filter(Boolean)); return { serverMode, staticHandler, errorHandler, requestHandler }; } const createRequestHandler = (build, mode) => { let _build; let serverMode; let staticHandler; let errorHandler; let _requestHandler; return async function requestHandler(request, initialContext) { _build = typeof build === "function" ? await build() : build; if (typeof build === "function") { let derived = derive(_build, mode); serverMode = derived.serverMode; staticHandler = derived.staticHandler; errorHandler = derived.errorHandler; _requestHandler = derived.requestHandler; } else if (!serverMode || !staticHandler || !errorHandler || !_requestHandler) { let derived = derive(_build, mode); serverMode = derived.serverMode; staticHandler = derived.staticHandler; errorHandler = derived.errorHandler; _requestHandler = derived.requestHandler; } return _requestHandler(request, initialContext); }; }; async function handleManifestRequest(build, dataRoutes, branches, url) { if (url.toString().length > 7680) return new Response(null, { statusText: "Bad Request", status: 400 }); if (build.assets.version !== url.searchParams.get("version")) return new Response(null, { status: 204, headers: { "X-Remix-Reload-Document": "true" } }); let patches = {}; if (url.searchParams.has("paths")) { let pathParam = url.searchParams.get("paths") || ""; let paths = new Set(pathParam.split(",").filter(Boolean)); for (let path of paths) { if (!path.startsWith("/")) path = `/${path}`; let matches = matchServerRoutes(build.routes, dataRoutes, branches, path, build.basename); if (matches) for (let match of matches) { let routeId = match.route.id; let route = build.assets.routes[routeId]; if (route) patches[routeId] = route; } } return Response.json(patches, { headers: { "Cache-Control": "public, max-age=31536000, immutable" } }); } return new Response("Invalid Request", { status: 400 }); } async function handleSingleFetchRequest(serverMode, build, staticHandler, request, loadContext, handleError) { return isMutationMethod(request.method) ? await singleFetchAction(build, serverMode, staticHandler, request, loadContext, handleError) : await singleFetchLoaders(build, serverMode, staticHandler, request, loadContext, handleError); } async function handleDocumentRequest(serverMode, build, staticHandler, request, loadContext, handleError, isSpaMode, criticalCss) { try { if (isMutationMethod(request.method)) try { throwIfPotentialCSRFAttack(request, Array.isArray(build.allowedActionOrigins) ? build.allowedActionOrigins : []); } catch (e) { handleError(e); return new Response("Bad Request", { status: 400 }); } let result = await staticHandler.query(request, { requestContext: loadContext, generateMiddlewareResponse: async (query) => { try { let innerResult = await query(request); if (!isResponse(innerResult)) innerResult = await renderHtml(innerResult, isSpaMode); return innerResult; } catch (error) { handleError(error); return new Response(null, { status: 500 }); } }, normalizePath: (r) => getNormalizedPath(r) }); if (!isResponse(result)) result = await renderHtml(result, isSpaMode); return result; } catch (error) { handleError(error); return new Response(null, { status: 500 }); } async function renderHtml(context, isSpaMode) { let headers = getDocumentHeaders(context, build); if (SERVER_NO_BODY_STATUS_CODES.has(context.statusCode)) return new Response(null, { status: context.statusCode, headers }); if (context.errors) { Object.values(context.errors).forEach((err) => { if (!isRouteErrorResponse(err) || err.error) handleError(err); }); context.errors = sanitizeErrors(context.errors, serverMode); } let state = { loaderData: context.loaderData, actionData: context.actionData, errors: context.errors }; let baseServerHandoff = { basename: build.basename, future: build.future, routeDiscovery: build.routeDiscovery, ssr: build.ssr, isSpaMode }; let entryContext = { manifest: build.assets, branches: staticHandler._internalRouteBranches, routeModules: createEntryRouteModules(build.routes), staticHandlerContext: context, criticalCss, serverHandoffString: createServerHandoffString({ ...baseServerHandoff, criticalCss }), serverHandoffStream: encodeViaTurboStream(state, request.signal, build.entry.module.streamTimeout, serverMode), renderMeta: {}, future: build.future, ssr: build.ssr, routeDiscovery: build.routeDiscovery, isSpaMode, serializeError: (err) => serializeError(err, serverMode) }; let handleDocumentRequestFunction = build.entry.module.default; try { return await handleDocumentRequestFunction(request, context.statusCode, headers, entryContext, loadContext); } catch (error) { handleError(error); let errorForSecondRender = error; if (isResponse(error)) try { let data = await unwrapResponse(error); errorForSecondRender = new ErrorResponseImpl(error.status, error.statusText, data); } catch (e) {} context = getStaticContextFromError(staticHandler.dataRoutes, context, errorForSecondRender); if (context.errors) context.errors = sanitizeErrors(context.errors, serverMode); let state = { loaderData: context.loaderData, actionData: context.actionData, errors: context.errors }; entryContext = { ...entryContext, staticHandlerContext: context, serverHandoffString: createServerHandoffString(baseServerHandoff), serverHandoffStream: encodeViaTurboStream(state, request.signal, build.entry.module.streamTimeout, serverMode), renderMeta: {} }; try { return await handleDocumentRequestFunction(request, context.statusCode, headers, entryContext, loadContext); } catch (error) { handleError(error); return returnLastResortErrorResponse(error, serverMode); } } } } async function handleResourceRequest(serverMode, build, staticHandler, routeId, request, loadContext, handleError) { try { return handleQueryRouteResult(await staticHandler.queryRoute(request, { routeId, requestContext: loadContext, generateMiddlewareResponse: async (queryRoute) => { try { return handleQueryRouteResult(await queryRoute(request)); } catch (error) { return handleQueryRouteError(error); } }, normalizePath: (r) => getNormalizedPath(r) })); } catch (error) { return handleQueryRouteError(error); } function handleQueryRouteResult(result) { if (isResponse(result)) return result; if (typeof result === "string") return new Response(result); return Response.json(result); } function handleQueryRouteError(error) { if (isResponse(error)) return error; if (isRouteErrorResponse(error)) { handleError(error); return errorResponseToJson(error, serverMode); } if (error instanceof Error && error.message === "Expected a response from queryRoute") { let newError = /* @__PURE__ */ new Error("Expected a Response to be returned from resource route handler"); handleError(newError); return returnLastResortErrorResponse(newError, serverMode); } handleError(error); return returnLastResortErrorResponse(error, serverMode); } } function errorResponseToJson(errorResponse, serverMode) { return Response.json(serializeError(errorResponse.error || /* @__PURE__ */ new Error("Unexpected Server Error"), serverMode), { status: errorResponse.status, statusText: errorResponse.statusText }); } function returnLastResortErrorResponse(error, serverMode) { let message = "Unexpected Server Error"; if (serverMode !== "production") message += `\n\n${String(error)}`; return new Response(message, { status: 500, headers: { "Content-Type": "text/plain" } }); } function unwrapResponse(response) { let contentType = response.headers.get("Content-Type"); return contentType && /\bapplication\/json\b/.test(contentType) ? response.body == null ? null : response.json() : response.text(); } //#endregion export { createRequestHandler };