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 isIssue(issue: unknown): issue is ValidationIssue { return typeof issue === "object" && issue !== null && typeof Reflect.get(issue, "message") === "string"; } /** Server-fn validators rethrow failures as a plain Error whose message is the issues array as JSON. */ function serializedIssues(message: string): ValidationIssue[] | null { if (!message.startsWith("[")) return null; try { const parsed: unknown = JSON.parse(message); return Array.isArray(parsed) && parsed.length > 0 && parsed.every(isIssue) ? parsed : null; } catch { return null; } } function validationIssues(error: unknown): ValidationIssue[] | null { if (!(error instanceof Error)) return null; if (!VALIDATION_ERROR_NAMES.has(error.name)) return serializedIssues(error.message); const issues: unknown = Reflect.get(error, "issues"); if (!Array.isArray(issues)) return null; return issues.filter(isIssue); } /** * 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"); }