297 lines
8.0 KiB
JavaScript
297 lines
8.0 KiB
JavaScript
import { parse as devalueParse, stringify as devalueStringify } from "devalue";
|
|
import { REDIRECT_STATUS_CODES } from "../../../core/constants.js";
|
|
import { AstroError } from "../../../core/errors/errors.js";
|
|
import {
|
|
ActionCalledFromServerError,
|
|
ActionsReturnedInvalidDataError
|
|
} from "../../../core/errors/errors-data.js";
|
|
import { appendForwardSlash as _appendForwardSlash } from "../../../core/path.js";
|
|
import { ACTION_QUERY_PARAMS as _ACTION_QUERY_PARAMS } from "../../consts.js";
|
|
const ACTION_QUERY_PARAMS = _ACTION_QUERY_PARAMS;
|
|
const appendForwardSlash = _appendForwardSlash;
|
|
const ACTION_ERROR_CODES = [
|
|
"BAD_REQUEST",
|
|
"UNAUTHORIZED",
|
|
"PAYMENT_REQUIRED",
|
|
"FORBIDDEN",
|
|
"NOT_FOUND",
|
|
"METHOD_NOT_ALLOWED",
|
|
"NOT_ACCEPTABLE",
|
|
"PROXY_AUTHENTICATION_REQUIRED",
|
|
"REQUEST_TIMEOUT",
|
|
"CONFLICT",
|
|
"GONE",
|
|
"LENGTH_REQUIRED",
|
|
"PRECONDITION_FAILED",
|
|
"CONTENT_TOO_LARGE",
|
|
"URI_TOO_LONG",
|
|
"UNSUPPORTED_MEDIA_TYPE",
|
|
"RANGE_NOT_SATISFIABLE",
|
|
"EXPECTATION_FAILED",
|
|
"MISDIRECTED_REQUEST",
|
|
"UNPROCESSABLE_CONTENT",
|
|
"LOCKED",
|
|
"FAILED_DEPENDENCY",
|
|
"TOO_EARLY",
|
|
"UPGRADE_REQUIRED",
|
|
"PRECONDITION_REQUIRED",
|
|
"TOO_MANY_REQUESTS",
|
|
"REQUEST_HEADER_FIELDS_TOO_LARGE",
|
|
"UNAVAILABLE_FOR_LEGAL_REASONS",
|
|
"INTERNAL_SERVER_ERROR",
|
|
"NOT_IMPLEMENTED",
|
|
"BAD_GATEWAY",
|
|
"SERVICE_UNAVAILABLE",
|
|
"GATEWAY_TIMEOUT",
|
|
"HTTP_VERSION_NOT_SUPPORTED",
|
|
"VARIANT_ALSO_NEGOTIATES",
|
|
"INSUFFICIENT_STORAGE",
|
|
"LOOP_DETECTED",
|
|
"NETWORK_AUTHENTICATION_REQUIRED"
|
|
];
|
|
const codeToStatusMap = {
|
|
// Implemented from IANA HTTP Status Code Registry
|
|
// https://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
|
|
BAD_REQUEST: 400,
|
|
UNAUTHORIZED: 401,
|
|
PAYMENT_REQUIRED: 402,
|
|
FORBIDDEN: 403,
|
|
NOT_FOUND: 404,
|
|
METHOD_NOT_ALLOWED: 405,
|
|
NOT_ACCEPTABLE: 406,
|
|
PROXY_AUTHENTICATION_REQUIRED: 407,
|
|
REQUEST_TIMEOUT: 408,
|
|
CONFLICT: 409,
|
|
GONE: 410,
|
|
LENGTH_REQUIRED: 411,
|
|
PRECONDITION_FAILED: 412,
|
|
CONTENT_TOO_LARGE: 413,
|
|
URI_TOO_LONG: 414,
|
|
UNSUPPORTED_MEDIA_TYPE: 415,
|
|
RANGE_NOT_SATISFIABLE: 416,
|
|
EXPECTATION_FAILED: 417,
|
|
MISDIRECTED_REQUEST: 421,
|
|
UNPROCESSABLE_CONTENT: 422,
|
|
LOCKED: 423,
|
|
FAILED_DEPENDENCY: 424,
|
|
TOO_EARLY: 425,
|
|
UPGRADE_REQUIRED: 426,
|
|
PRECONDITION_REQUIRED: 428,
|
|
TOO_MANY_REQUESTS: 429,
|
|
REQUEST_HEADER_FIELDS_TOO_LARGE: 431,
|
|
UNAVAILABLE_FOR_LEGAL_REASONS: 451,
|
|
INTERNAL_SERVER_ERROR: 500,
|
|
NOT_IMPLEMENTED: 501,
|
|
BAD_GATEWAY: 502,
|
|
SERVICE_UNAVAILABLE: 503,
|
|
GATEWAY_TIMEOUT: 504,
|
|
HTTP_VERSION_NOT_SUPPORTED: 505,
|
|
VARIANT_ALSO_NEGOTIATES: 506,
|
|
INSUFFICIENT_STORAGE: 507,
|
|
LOOP_DETECTED: 508,
|
|
NETWORK_AUTHENTICATION_REQUIRED: 511
|
|
};
|
|
const statusToCodeMap = Object.entries(codeToStatusMap).reduce(
|
|
// reverse the key-value pairs
|
|
(acc, [key, value]) => ({ ...acc, [value]: key }),
|
|
{}
|
|
);
|
|
class ActionError extends Error {
|
|
type = "AstroActionError";
|
|
code = "INTERNAL_SERVER_ERROR";
|
|
status = 500;
|
|
constructor(params) {
|
|
super(params.message);
|
|
this.code = params.code;
|
|
this.status = ActionError.codeToStatus(params.code);
|
|
if (params.stack) {
|
|
this.stack = params.stack;
|
|
}
|
|
}
|
|
static codeToStatus(code) {
|
|
return codeToStatusMap[code];
|
|
}
|
|
static statusToCode(status) {
|
|
return statusToCodeMap[status] ?? "INTERNAL_SERVER_ERROR";
|
|
}
|
|
static fromJson(body) {
|
|
if (isInputError(body)) {
|
|
return new ActionInputError(body.issues);
|
|
}
|
|
if (isActionError(body)) {
|
|
return new ActionError(body);
|
|
}
|
|
return new ActionError({
|
|
code: "INTERNAL_SERVER_ERROR"
|
|
});
|
|
}
|
|
}
|
|
function isActionError(error) {
|
|
return typeof error === "object" && error != null && "type" in error && error.type === "AstroActionError";
|
|
}
|
|
function isInputError(error) {
|
|
return typeof error === "object" && error != null && "type" in error && error.type === "AstroActionInputError" && "issues" in error && Array.isArray(error.issues);
|
|
}
|
|
class ActionInputError extends ActionError {
|
|
type = "AstroActionInputError";
|
|
// We don't expose all ZodError properties.
|
|
// Not all properties will serialize from server to client,
|
|
// and we don't want to import the full ZodError object into the client.
|
|
issues;
|
|
fields;
|
|
constructor(issues) {
|
|
super({
|
|
message: `Failed to validate: ${JSON.stringify(issues, null, 2)}`,
|
|
code: "BAD_REQUEST"
|
|
});
|
|
this.issues = issues;
|
|
this.fields = {};
|
|
for (const issue of issues) {
|
|
if (issue.path.length > 0) {
|
|
const key = issue.path[0].toString();
|
|
this.fields[key] ??= [];
|
|
this.fields[key]?.push(issue.message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
async function callSafely(handler) {
|
|
try {
|
|
const data = await handler();
|
|
return { data, error: void 0 };
|
|
} catch (e) {
|
|
if (e instanceof ActionError) {
|
|
return { data: void 0, error: e };
|
|
}
|
|
return {
|
|
data: void 0,
|
|
error: new ActionError({
|
|
message: e instanceof Error ? e.message : "Unknown error",
|
|
code: "INTERNAL_SERVER_ERROR"
|
|
})
|
|
};
|
|
}
|
|
}
|
|
function getActionQueryString(name) {
|
|
const searchParams = new URLSearchParams({ [_ACTION_QUERY_PARAMS.actionName]: name });
|
|
return `?${searchParams.toString()}`;
|
|
}
|
|
function serializeActionResult(res) {
|
|
if (res.error) {
|
|
if (import.meta.env?.DEV) {
|
|
actionResultErrorStack.set(res.error.stack);
|
|
}
|
|
let body2;
|
|
if (res.error instanceof ActionInputError) {
|
|
body2 = {
|
|
type: res.error.type,
|
|
issues: res.error.issues,
|
|
fields: res.error.fields
|
|
};
|
|
} else {
|
|
body2 = {
|
|
...res.error,
|
|
message: res.error.message
|
|
};
|
|
}
|
|
return {
|
|
type: "error",
|
|
status: res.error.status,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(body2)
|
|
};
|
|
}
|
|
if (res.data === void 0) {
|
|
return {
|
|
type: "empty",
|
|
status: 204
|
|
};
|
|
}
|
|
let body;
|
|
try {
|
|
body = devalueStringify(res.data, {
|
|
// Add support for URL objects
|
|
URL: (value) => value instanceof URL && value.href
|
|
});
|
|
} catch (e) {
|
|
let hint = ActionsReturnedInvalidDataError.hint;
|
|
if (res.data instanceof Response) {
|
|
hint = REDIRECT_STATUS_CODES.includes(res.data.status) ? "If you need to redirect when the action succeeds, trigger a redirect where the action is called. See the Actions guide for server and client redirect examples: https://docs.astro.build/en/guides/actions." : "If you need to return a Response object, try using a server endpoint instead. See https://docs.astro.build/en/guides/endpoints/#server-endpoints-api-routes";
|
|
}
|
|
throw new AstroError({
|
|
...ActionsReturnedInvalidDataError,
|
|
message: ActionsReturnedInvalidDataError.message(String(e)),
|
|
hint
|
|
});
|
|
}
|
|
return {
|
|
type: "data",
|
|
status: 200,
|
|
contentType: "application/json+devalue",
|
|
body
|
|
};
|
|
}
|
|
function deserializeActionResult(res) {
|
|
if (res.type === "error") {
|
|
let json;
|
|
try {
|
|
json = JSON.parse(res.body);
|
|
} catch {
|
|
return {
|
|
data: void 0,
|
|
error: new ActionError({
|
|
message: res.body,
|
|
code: "INTERNAL_SERVER_ERROR"
|
|
})
|
|
};
|
|
}
|
|
if (import.meta.env?.PROD) {
|
|
return { error: ActionError.fromJson(json), data: void 0 };
|
|
} else {
|
|
const error = ActionError.fromJson(json);
|
|
error.stack = actionResultErrorStack.get();
|
|
return {
|
|
error,
|
|
data: void 0
|
|
};
|
|
}
|
|
}
|
|
if (res.type === "empty") {
|
|
return { data: void 0, error: void 0 };
|
|
}
|
|
return {
|
|
data: devalueParse(res.body, {
|
|
URL: (href) => new URL(href)
|
|
}),
|
|
error: void 0
|
|
};
|
|
}
|
|
const actionResultErrorStack = /* @__PURE__ */ function actionResultErrorStackFn() {
|
|
let errorStack;
|
|
return {
|
|
set(stack) {
|
|
errorStack = stack;
|
|
},
|
|
get() {
|
|
return errorStack;
|
|
}
|
|
};
|
|
}();
|
|
function astroCalledServerError() {
|
|
return new AstroError(ActionCalledFromServerError);
|
|
}
|
|
export {
|
|
ACTION_ERROR_CODES,
|
|
ACTION_QUERY_PARAMS,
|
|
ActionError,
|
|
ActionInputError,
|
|
appendForwardSlash,
|
|
astroCalledServerError,
|
|
callSafely,
|
|
deserializeActionResult,
|
|
getActionQueryString,
|
|
isActionError,
|
|
isInputError,
|
|
serializeActionResult
|
|
};
|