import { isNotFound, isRedirect } from "@tanstack/react-router";
import { HttpError, badRequest } from "./errors";
const HTTP_INTERNAL = 500;
const VALIDATION_ERROR_NAMES = new Set(["ZodError", "$ZodError"]);
type ValidationIssue = { message: string };
function validationIssues(error: unknown): ValidationIssue[] | null {
if (!(error instanceof Error) || !VALIDATION_ERROR_NAMES.has(error.name)) return null;
const issues: unknown = Reflect.get(error, "issues");
if (!Array.isArray(issues)) return null;
return issues.filter(
(issue): issue is ValidationIssue =>
typeof issue === "object" && issue !== null && typeof Reflect.get(issue, "message") === "string",
);
}
/**
* Anything a server function throws reaches the client serialized, so only deliberate errors keep their
* message: HttpErrors, router control flow and input-validation failures. Everything else (a D1 error, a
* TypeError naming internals) is logged here and replaced by a generic 500 (DEPLOY-P8).
*/
export function clientSafeError(error: unknown): unknown {
if (error instanceof HttpError && error.status >= HTTP_INTERNAL) {
console.error("Server function 5xx", error);
return new HttpError(error.status, "Internal error");
}
if (error instanceof HttpError || isRedirect(error) || isNotFound(error) || error instanceof Response) return error;
const issues = validationIssues(error);
if (issues) return badRequest(issues[0]?.message ?? "Invalid input");
console.error("Unhandled server function error", error);
return new HttpError(HTTP_INTERNAL, "Internal error");
}