48 lines · 2.0 KB
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");