| 1 | |
| 2 | |
| 3 | |
| 4 | |
| 5 | |
| 6 | |
| 7 | |
| 8 | |
| 9 | |
| 10 |
|
| 11 | import { ErrorResponseImpl, isRouteErrorResponse, stripBasename } from "../router/utils.js";
|
| 12 | import { isRedirectStatusCode, isResponse } from "../router/router.js";
|
| 13 | import { encode } from "../../vendor/turbo-stream-v2/turbo-stream.js";
|
| 14 | import { NO_BODY_STATUS_CODES, SingleFetchRedirectSymbol } from "../dom/ssr/single-fetch.js";
|
| 15 | import "./mode.js";
|
| 16 | import { sanitizeError, sanitizeErrors } from "./errors.js";
|
| 17 | import { getDocumentHeaders } from "./headers.js";
|
| 18 | import { throwIfPotentialCSRFAttack } from "../actions.js";
|
| 19 | import { getNormalizedPath } from "./urls.js";
|
| 20 |
|
| 21 | const SERVER_NO_BODY_STATUS_CODES = new Set([...NO_BODY_STATUS_CODES, 304]);
|
| 22 | async function singleFetchAction(build, serverMode, staticHandler, request, loadContext, handleError) {
|
| 23 | try {
|
| 24 | try {
|
| 25 | throwIfPotentialCSRFAttack(request, Array.isArray(build.allowedActionOrigins) ? build.allowedActionOrigins : []);
|
| 26 | } catch (e) {
|
| 27 | return handleQueryError( new Error("Bad Request"), 400);
|
| 28 | }
|
| 29 | return handleQueryResult(await staticHandler.query(request, {
|
| 30 | requestContext: loadContext,
|
| 31 | skipLoaderErrorBubbling: true,
|
| 32 | skipRevalidation: true,
|
| 33 | generateMiddlewareResponse: async (query) => {
|
| 34 | try {
|
| 35 | return handleQueryResult(await query(request));
|
| 36 | } catch (error) {
|
| 37 | return handleQueryError(error);
|
| 38 | }
|
| 39 | },
|
| 40 | normalizePath: (r) => getNormalizedPath(r)
|
| 41 | }));
|
| 42 | } catch (error) {
|
| 43 | return handleQueryError(error);
|
| 44 | }
|
| 45 | function handleQueryResult(result) {
|
| 46 | return isResponse(result) ? result : staticContextToResponse(result);
|
| 47 | }
|
| 48 | function handleQueryError(error, status = 500) {
|
| 49 | handleError(error);
|
| 50 | return generateSingleFetchResponse(request, build, serverMode, {
|
| 51 | result: { error },
|
| 52 | headers: new Headers(),
|
| 53 | status
|
| 54 | });
|
| 55 | }
|
| 56 | function staticContextToResponse(context) {
|
| 57 | let headers = getDocumentHeaders(context, build);
|
| 58 | if (isRedirectStatusCode(context.statusCode) && headers.has("Location")) return new Response(null, {
|
| 59 | status: context.statusCode,
|
| 60 | headers
|
| 61 | });
|
| 62 | if (context.errors) {
|
| 63 | Object.values(context.errors).forEach((err) => {
|
| 64 | if (!isRouteErrorResponse(err) || err.error) handleError(err);
|
| 65 | });
|
| 66 | context.errors = sanitizeErrors(context.errors, serverMode);
|
| 67 | }
|
| 68 | let singleFetchResult;
|
| 69 | if (context.errors) singleFetchResult = { error: Object.values(context.errors)[0] };
|
| 70 | else singleFetchResult = { data: Object.values(context.actionData || {})[0] };
|
| 71 | return generateSingleFetchResponse(request, build, serverMode, {
|
| 72 | result: singleFetchResult,
|
| 73 | headers,
|
| 74 | status: context.statusCode
|
| 75 | });
|
| 76 | }
|
| 77 | }
|
| 78 | async function singleFetchLoaders(build, serverMode, staticHandler, request, loadContext, handleError) {
|
| 79 | let routesParam = new URL(request.url).searchParams.get("_routes");
|
| 80 | let loadRouteIds = routesParam ? new Set(routesParam.split(",")) : null;
|
| 81 | try {
|
| 82 | return handleQueryResult(await staticHandler.query(request, {
|
| 83 | requestContext: loadContext,
|
| 84 | filterMatchesToLoad: (m) => !loadRouteIds || loadRouteIds.has(m.route.id),
|
| 85 | skipLoaderErrorBubbling: true,
|
| 86 | generateMiddlewareResponse: async (query) => {
|
| 87 | try {
|
| 88 | return handleQueryResult(await query(request));
|
| 89 | } catch (error) {
|
| 90 | return handleQueryError(error);
|
| 91 | }
|
| 92 | },
|
| 93 | normalizePath: (r) => getNormalizedPath(r)
|
| 94 | }));
|
| 95 | } catch (error) {
|
| 96 | return handleQueryError(error);
|
| 97 | }
|
| 98 | function handleQueryResult(result) {
|
| 99 | return isResponse(result) ? result : staticContextToResponse(result);
|
| 100 | }
|
| 101 | function handleQueryError(error) {
|
| 102 | handleError(error);
|
| 103 | return generateSingleFetchResponse(request, build, serverMode, {
|
| 104 | result: { error },
|
| 105 | headers: new Headers(),
|
| 106 | status: 500
|
| 107 | });
|
| 108 | }
|
| 109 | function staticContextToResponse(context) {
|
| 110 | let headers = getDocumentHeaders(context, build);
|
| 111 | if (isRedirectStatusCode(context.statusCode) && headers.has("Location")) return new Response(null, {
|
| 112 | status: context.statusCode,
|
| 113 | headers
|
| 114 | });
|
| 115 | if (context.errors) {
|
| 116 | Object.values(context.errors).forEach((err) => {
|
| 117 | if (!isRouteErrorResponse(err) || err.error) handleError(err);
|
| 118 | });
|
| 119 | context.errors = sanitizeErrors(context.errors, serverMode);
|
| 120 | }
|
| 121 | let results = {};
|
| 122 | let loadedMatches = new Set(context.matches.filter((m) => loadRouteIds ? loadRouteIds.has(m.route.id) : m.route.loader != null).map((m) => m.route.id));
|
| 123 | if (context.errors) for (let [id, error] of Object.entries(context.errors)) results[id] = { error };
|
| 124 | for (let [id, data] of Object.entries(context.loaderData)) if (!(id in results) && loadedMatches.has(id)) results[id] = { data };
|
| 125 | return generateSingleFetchResponse(request, build, serverMode, {
|
| 126 | result: results,
|
| 127 | headers,
|
| 128 | status: context.statusCode
|
| 129 | });
|
| 130 | }
|
| 131 | }
|
| 132 | function generateSingleFetchResponse(request, build, serverMode, { result, headers, status }) {
|
| 133 | let resultHeaders = new Headers(headers);
|
| 134 | resultHeaders.set("X-Remix-Response", "yes");
|
| 135 | if (SERVER_NO_BODY_STATUS_CODES.has(status)) return new Response(null, {
|
| 136 | status,
|
| 137 | headers: resultHeaders
|
| 138 | });
|
| 139 | resultHeaders.set("Content-Type", "text/x-script");
|
| 140 | resultHeaders.delete("Content-Length");
|
| 141 | return new Response(encodeViaTurboStream(result, request.signal, build.entry.module.streamTimeout, serverMode), {
|
| 142 | status: status || 200,
|
| 143 | headers: resultHeaders
|
| 144 | });
|
| 145 | }
|
| 146 | function generateSingleFetchRedirectResponse(redirectResponse, request, build, serverMode) {
|
| 147 | let redirect = getSingleFetchRedirect(redirectResponse.status, redirectResponse.headers, build.basename);
|
| 148 | let headers = new Headers(redirectResponse.headers);
|
| 149 | headers.delete("Location");
|
| 150 | headers.set("Content-Type", "text/x-script");
|
| 151 | return generateSingleFetchResponse(request, build, serverMode, {
|
| 152 | result: request.method === "GET" ? { [SingleFetchRedirectSymbol]: redirect } : redirect,
|
| 153 | headers,
|
| 154 | status: 202
|
| 155 | });
|
| 156 | }
|
| 157 | function getSingleFetchRedirect(status, headers, basename) {
|
| 158 | let redirect = headers.get("Location");
|
| 159 | if (basename) redirect = stripBasename(redirect, basename) || redirect;
|
| 160 | return {
|
| 161 | redirect,
|
| 162 | status,
|
| 163 | revalidate: headers.has("X-Remix-Revalidate") || headers.has("Set-Cookie"),
|
| 164 | reload: headers.has("X-Remix-Reload-Document"),
|
| 165 | replace: headers.has("X-Remix-Replace")
|
| 166 | };
|
| 167 | }
|
| 168 | function encodeViaTurboStream(data, requestSignal, streamTimeout, serverMode) {
|
| 169 | let controller = new AbortController();
|
| 170 | let timeoutId = setTimeout(() => {
|
| 171 | controller.abort( new Error("Server Timeout"));
|
| 172 | cleanupCallbacks();
|
| 173 | }, typeof streamTimeout === "number" ? streamTimeout : 4950);
|
| 174 | let abortControllerOnRequestAbort = () => {
|
| 175 | controller.abort(requestSignal.reason);
|
| 176 | cleanupCallbacks();
|
| 177 | };
|
| 178 | requestSignal.addEventListener("abort", abortControllerOnRequestAbort);
|
| 179 | let cleanupCallbacks = () => {
|
| 180 | clearTimeout(timeoutId);
|
| 181 | requestSignal.removeEventListener("abort", abortControllerOnRequestAbort);
|
| 182 | };
|
| 183 | return encode(data, {
|
| 184 | signal: controller.signal,
|
| 185 | onComplete: cleanupCallbacks,
|
| 186 | plugins: [(value) => {
|
| 187 | if (value instanceof Error) {
|
| 188 | let { name, message, stack } = serverMode === "production" ? sanitizeError(value, serverMode) : value;
|
| 189 | return [
|
| 190 | "SanitizedError",
|
| 191 | name,
|
| 192 | message,
|
| 193 | stack
|
| 194 | ];
|
| 195 | }
|
| 196 | if (value instanceof ErrorResponseImpl) {
|
| 197 | let { data, status, statusText } = value;
|
| 198 | return [
|
| 199 | "ErrorResponse",
|
| 200 | data,
|
| 201 | status,
|
| 202 | statusText
|
| 203 | ];
|
| 204 | }
|
| 205 | if (value && typeof value === "object" && SingleFetchRedirectSymbol in value) return ["SingleFetchRedirect", value[SingleFetchRedirectSymbol]];
|
| 206 | }],
|
| 207 | postPlugins: [(value) => {
|
| 208 | if (!value) return;
|
| 209 | if (typeof value !== "object") return;
|
| 210 | return ["SingleFetchClassInstance", Object.fromEntries(Object.entries(value))];
|
| 211 | }, () => ["SingleFetchFallback"]]
|
| 212 | });
|
| 213 | }
|
| 214 |
|
| 215 | export { SERVER_NO_BODY_STATUS_CODES, encodeViaTurboStream, generateSingleFetchRedirectResponse, singleFetchAction, singleFetchLoaders };
|