const ASCII_EQUIVALENTS: Record = { "—": "-", "–": "-", "‘": "'", "“": '"', "”": '"', "…": "...", }; /** * Error messages thrown from server actions are forwarded to the client via * the `X-Error` response header, which only accepts Latin-1 (ByteString) * values — undici throws a TypeError on anything above U+00FF and the request * dies with a 500 before the client can display the message. Map common * typographic characters to ASCII and drop anything else outside Latin-1. */ export function clientSafeMessage(message: string): string { const mapped = message.replace(/[—–‘“”…]/g, (char) => ASCII_EQUIVALENTS[char] ?? char); return mapped.replace(/[^\t\n\r\x20-\x7E\u00A0-\u00FF]/g, ""); } export class RouteError extends Error { constructor( message: string, public statusCode: number = 500, public title: string = "Error", public code: string = "internal_error", ) { super(message); this.name = "RouteError"; } } export class NotFoundError extends RouteError { constructor(message: string = "Page not found") { super(message, 404, "Not Found", "not_found"); } } export class ForbiddenError extends RouteError { constructor(message: string = "Access denied", code: string = "forbidden") { super(message, 403, "Forbidden", code); } } export class UnauthorizedError extends RouteError { constructor(message: string = "Please sign in to continue") { super(message, 401, "Unauthorized", "unauthorized"); } } export function isRouteError(error: unknown): error is RouteError { return ( error instanceof RouteError || (error instanceof Error && error.name === "RouteError") || (typeof error === "object" && error !== null && "name" in error && (error as { name: unknown }).name === "RouteError") ); }