122 lines
3.9 KiB
TypeScript
122 lines
3.9 KiB
TypeScript
import { TRPCClientError } from "@trpc/client";
|
|
|
|
function errorMessage(error: unknown): string {
|
|
if (error instanceof TRPCClientError) return error.message;
|
|
if (error instanceof Error) return error.message;
|
|
if (typeof error === "object" && error !== null && "message" in error) {
|
|
const message = (error as { message: unknown }).message;
|
|
if (typeof message === "string") return message;
|
|
}
|
|
return "";
|
|
}
|
|
|
|
function errorStatus(error: unknown): number | undefined {
|
|
if (typeof error !== "object" || error === null || !("status" in error)) return undefined;
|
|
const status = (error as { status: unknown }).status;
|
|
return typeof status === "number" ? status : undefined;
|
|
}
|
|
|
|
export function parseRetryAfterSeconds(value: string | number | null | undefined): number | null {
|
|
if (value == null || value === "") return null;
|
|
const seconds = typeof value === "number" ? value : Number(value);
|
|
if (!Number.isFinite(seconds) || seconds <= 0) return null;
|
|
return Math.ceil(seconds);
|
|
}
|
|
|
|
export function isRateLimitError(error: unknown): boolean {
|
|
if (errorStatus(error) === 429) return true;
|
|
|
|
if (error instanceof TRPCClientError && error.data?.code === "TOO_MANY_REQUESTS") {
|
|
return true;
|
|
}
|
|
|
|
const message = errorMessage(error).toLowerCase();
|
|
return (
|
|
message.includes("too many") ||
|
|
message.includes("rate limit") ||
|
|
message.includes("try again later")
|
|
);
|
|
}
|
|
|
|
export function isUnauthorizedError(error: unknown): boolean {
|
|
if (isRateLimitError(error)) return false;
|
|
|
|
if (error instanceof TRPCClientError) {
|
|
if (error.data?.code === "UNAUTHORIZED") return true;
|
|
}
|
|
|
|
if (errorStatus(error) === 401) return true;
|
|
|
|
const message = errorMessage(error).toLowerCase();
|
|
return message === "unauthorized" || message.includes("not authenticated");
|
|
}
|
|
|
|
export function formatRateLimitMessage(retryAfterSeconds?: number | null): string {
|
|
const retryAfter = parseRetryAfterSeconds(retryAfterSeconds ?? null);
|
|
if (retryAfter != null) {
|
|
if (retryAfter < 60) {
|
|
return `Too many attempts. Wait ${retryAfter} second${retryAfter === 1 ? "" : "s"} and try again.`;
|
|
}
|
|
const minutes = Math.ceil(retryAfter / 60);
|
|
return `Too many attempts. Wait about ${minutes} minute${minutes === 1 ? "" : "s"} and try again.`;
|
|
}
|
|
return "Too many attempts. Please wait a moment and try again.";
|
|
}
|
|
|
|
export function formatTrpcErrorMessage(error: unknown, fallback = "Something went wrong"): string {
|
|
if (isRateLimitError(error)) {
|
|
return formatRateLimitMessage();
|
|
}
|
|
if (isUnauthorizedError(error)) {
|
|
return "Your session expired. Sign in again to continue.";
|
|
}
|
|
if (error instanceof TRPCClientError) {
|
|
return error.message || fallback;
|
|
}
|
|
if (error instanceof Error) {
|
|
return error.message || fallback;
|
|
}
|
|
return fallback;
|
|
}
|
|
|
|
type AuthLikeError = {
|
|
message?: string;
|
|
status?: number;
|
|
};
|
|
|
|
export function formatAuthErrorMessage(error: AuthLikeError | null | undefined): string {
|
|
if (!error) return "Something went wrong";
|
|
|
|
if (isRateLimitError(error)) {
|
|
return formatRateLimitMessage();
|
|
}
|
|
|
|
const message = error.message ?? "";
|
|
if (message.toLowerCase().includes("internal") || message.includes("500")) {
|
|
return "Server error — is the API running with Postgres? Check beenvoice dev + docker.";
|
|
}
|
|
|
|
return message || "Invalid email or password";
|
|
}
|
|
|
|
export async function readHttpErrorMessage(response: Response): Promise<string> {
|
|
if (response.status === 429) {
|
|
const retryAfter = parseRetryAfterSeconds(
|
|
response.headers.get("x-retry-after") ?? response.headers.get("retry-after"),
|
|
);
|
|
return formatRateLimitMessage(retryAfter);
|
|
}
|
|
|
|
const data = (await response.json().catch(() => ({}))) as {
|
|
error?: string;
|
|
message?: string;
|
|
};
|
|
|
|
const message = data.error ?? data.message;
|
|
if (message && isRateLimitError({ message, status: response.status })) {
|
|
return formatRateLimitMessage();
|
|
}
|
|
|
|
return message ?? "Something went wrong";
|
|
}
|