react-router
Version:
Declarative routing for React
216 lines (215 loc) • 8.04 kB
JavaScript
/**
* 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, isRouteErrorResponse, stripBasename } from "../router/utils.js";
import { isRedirectStatusCode, isResponse } from "../router/router.js";
import { encode } from "../../vendor/turbo-stream-v2/turbo-stream.js";
import { NO_BODY_STATUS_CODES, SingleFetchRedirectSymbol } from "../dom/ssr/single-fetch.js";
import "./mode.js";
import { sanitizeError, sanitizeErrors } from "./errors.js";
import { getDocumentHeaders } from "./headers.js";
import { throwIfPotentialCSRFAttack } from "../actions.js";
import { getNormalizedPath } from "./urls.js";
//#region lib/server-runtime/single-fetch.ts
const SERVER_NO_BODY_STATUS_CODES = new Set([...NO_BODY_STATUS_CODES, 304]);
async function singleFetchAction(build, serverMode, staticHandler, request, loadContext, handleError) {
try {
try {
throwIfPotentialCSRFAttack(request, Array.isArray(build.allowedActionOrigins) ? build.allowedActionOrigins : []);
} catch (e) {
return handleQueryError(/* @__PURE__ */ new Error("Bad Request"), 400);
}
return handleQueryResult(await staticHandler.query(request, {
requestContext: loadContext,
skipLoaderErrorBubbling: true,
skipRevalidation: true,
generateMiddlewareResponse: async (query) => {
try {
return handleQueryResult(await query(request));
} catch (error) {
return handleQueryError(error);
}
},
normalizePath: (r) => getNormalizedPath(r)
}));
} catch (error) {
return handleQueryError(error);
}
function handleQueryResult(result) {
return isResponse(result) ? result : staticContextToResponse(result);
}
function handleQueryError(error, status = 500) {
handleError(error);
return generateSingleFetchResponse(request, build, serverMode, {
result: { error },
headers: new Headers(),
status
});
}
function staticContextToResponse(context) {
let headers = getDocumentHeaders(context, build);
if (isRedirectStatusCode(context.statusCode) && headers.has("Location")) 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 singleFetchResult;
if (context.errors) singleFetchResult = { error: Object.values(context.errors)[0] };
else singleFetchResult = { data: Object.values(context.actionData || {})[0] };
return generateSingleFetchResponse(request, build, serverMode, {
result: singleFetchResult,
headers,
status: context.statusCode
});
}
}
async function singleFetchLoaders(build, serverMode, staticHandler, request, loadContext, handleError) {
let routesParam = new URL(request.url).searchParams.get("_routes");
let loadRouteIds = routesParam ? new Set(routesParam.split(",")) : null;
try {
return handleQueryResult(await staticHandler.query(request, {
requestContext: loadContext,
filterMatchesToLoad: (m) => !loadRouteIds || loadRouteIds.has(m.route.id),
skipLoaderErrorBubbling: true,
generateMiddlewareResponse: async (query) => {
try {
return handleQueryResult(await query(request));
} catch (error) {
return handleQueryError(error);
}
},
normalizePath: (r) => getNormalizedPath(r)
}));
} catch (error) {
return handleQueryError(error);
}
function handleQueryResult(result) {
return isResponse(result) ? result : staticContextToResponse(result);
}
function handleQueryError(error) {
handleError(error);
return generateSingleFetchResponse(request, build, serverMode, {
result: { error },
headers: new Headers(),
status: 500
});
}
function staticContextToResponse(context) {
let headers = getDocumentHeaders(context, build);
if (isRedirectStatusCode(context.statusCode) && headers.has("Location")) 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 results = {};
let loadedMatches = new Set(context.matches.filter((m) => loadRouteIds ? loadRouteIds.has(m.route.id) : m.route.loader != null).map((m) => m.route.id));
if (context.errors) for (let [id, error] of Object.entries(context.errors)) results[id] = { error };
for (let [id, data] of Object.entries(context.loaderData)) if (!(id in results) && loadedMatches.has(id)) results[id] = { data };
return generateSingleFetchResponse(request, build, serverMode, {
result: results,
headers,
status: context.statusCode
});
}
}
function generateSingleFetchResponse(request, build, serverMode, { result, headers, status }) {
let resultHeaders = new Headers(headers);
resultHeaders.set("X-Remix-Response", "yes");
if (SERVER_NO_BODY_STATUS_CODES.has(status)) return new Response(null, {
status,
headers: resultHeaders
});
resultHeaders.set("Content-Type", "text/x-script");
resultHeaders.delete("Content-Length");
return new Response(encodeViaTurboStream(result, request.signal, build.entry.module.streamTimeout, serverMode), {
status: status || 200,
headers: resultHeaders
});
}
function generateSingleFetchRedirectResponse(redirectResponse, request, build, serverMode) {
let redirect = getSingleFetchRedirect(redirectResponse.status, redirectResponse.headers, build.basename);
let headers = new Headers(redirectResponse.headers);
headers.delete("Location");
headers.set("Content-Type", "text/x-script");
return generateSingleFetchResponse(request, build, serverMode, {
result: request.method === "GET" ? { [SingleFetchRedirectSymbol]: redirect } : redirect,
headers,
status: 202
});
}
function getSingleFetchRedirect(status, headers, basename) {
let redirect = headers.get("Location");
if (basename) redirect = stripBasename(redirect, basename) || redirect;
return {
redirect,
status,
revalidate: headers.has("X-Remix-Revalidate") || headers.has("Set-Cookie"),
reload: headers.has("X-Remix-Reload-Document"),
replace: headers.has("X-Remix-Replace")
};
}
function encodeViaTurboStream(data, requestSignal, streamTimeout, serverMode) {
let controller = new AbortController();
let timeoutId = setTimeout(() => {
controller.abort(/* @__PURE__ */ new Error("Server Timeout"));
cleanupCallbacks();
}, typeof streamTimeout === "number" ? streamTimeout : 4950);
let abortControllerOnRequestAbort = () => {
controller.abort(requestSignal.reason);
cleanupCallbacks();
};
requestSignal.addEventListener("abort", abortControllerOnRequestAbort);
let cleanupCallbacks = () => {
clearTimeout(timeoutId);
requestSignal.removeEventListener("abort", abortControllerOnRequestAbort);
};
return encode(data, {
signal: controller.signal,
onComplete: cleanupCallbacks,
plugins: [(value) => {
if (value instanceof Error) {
let { name, message, stack } = serverMode === "production" ? sanitizeError(value, serverMode) : value;
return [
"SanitizedError",
name,
message,
stack
];
}
if (value instanceof ErrorResponseImpl) {
let { data, status, statusText } = value;
return [
"ErrorResponse",
data,
status,
statusText
];
}
if (value && typeof value === "object" && SingleFetchRedirectSymbol in value) return ["SingleFetchRedirect", value[SingleFetchRedirectSymbol]];
}],
postPlugins: [(value) => {
if (!value) return;
if (typeof value !== "object") return;
return ["SingleFetchClassInstance", Object.fromEntries(Object.entries(value))];
}, () => ["SingleFetchFallback"]]
});
}
//#endregion
export { SERVER_NO_BODY_STATUS_CODES, encodeViaTurboStream, generateSingleFetchRedirectResponse, singleFetchAction, singleFetchLoaders };