33 lines · 1.5 KB
import { createSerializationAdapter } from "@tanstack/react-router";
import { HttpError } from "~/server/errors";

const INTERNAL_ERROR = 500;
/** These embed internal storage names and upstream text; other HttpErrors carry deliberate, user-facing messages. */
const MASKED_NAMES = new Set(["ArtifactsUnavailableError"]);

function masked(error: HttpError): boolean {
  return error.status === INTERNAL_ERROR || MASKED_NAMES.has(error.name);
}

const PUBLIC_DETAIL_KEYS = ["code", "meter", "reason", "manageable", "account", "billingPath", "billingUrl"];

/** Only known, client-safe detail keys cross the boundary (internal ids such as accountId never do). */
function publicDetail(detail: HttpError["detail"]): HttpError["detail"] {
  if (!detail) return null;
  const out: NonNullable<HttpError["detail"]> = {};
  for (const key of PUBLIC_DETAIL_KEYS) if (key in detail) out[key] = detail[key];
  return out;
}

/** Keeps `HttpError.status` across the server → client boundary so error pages can tell 403 from 500. */
export const httpErrorAdapter = createSerializationAdapter({
  key: "gitfor-http-error",
  test: (value): value is HttpError => value instanceof HttpError,
  toSerializable: (error) => ({
    status: error.status,
    message: masked(error) ? "Something went wrong on our side; try again shortly" : error.message,
    detail: masked(error) ? null : publicDetail(error.detail),
  }),
  fromSerializable: ({ status, message, detail }) => new HttpError(status, message, detail ?? null),
});