175 lines
4.5 KiB
TypeScript
175 lines
4.5 KiB
TypeScript
// utils/errorHandler.ts
|
|
export interface ApiError {
|
|
code: string;
|
|
message: string;
|
|
details?: string;
|
|
field?: string;
|
|
}
|
|
|
|
export class ValidationError extends Error {
|
|
public field?: string;
|
|
constructor(msg: string, field?: string) {
|
|
super(msg);
|
|
this.name = "ValidationError";
|
|
this.field = field;
|
|
}
|
|
}
|
|
|
|
export class NetworkError extends Error {
|
|
constructor(msg: string) {
|
|
super(msg);
|
|
this.name = "NetworkError";
|
|
}
|
|
}
|
|
|
|
export function createValidationErrorResponse(
|
|
msg: string,
|
|
field?: string,
|
|
): Response {
|
|
const error: ApiError = {
|
|
code: "VALIDATION_ERROR",
|
|
message: msg,
|
|
...(field && { field }),
|
|
};
|
|
return new Response(JSON.stringify(error), {
|
|
status: 400,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
export function createNetworkErrorResponse(
|
|
msg: string = "Network error",
|
|
): Response {
|
|
return new Response(JSON.stringify({ code: "NETWORK_ERROR", message: msg }), {
|
|
status: 503,
|
|
headers: { "Content-Type": "application/json" },
|
|
});
|
|
}
|
|
|
|
export function createTimeoutErrorResponse(ms: number): Response {
|
|
return new Response(
|
|
JSON.stringify({
|
|
code: "TIMEOUT_ERROR",
|
|
message: "Request timed out after " + ms + "ms. Please try again.",
|
|
}),
|
|
{ status: 504, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
|
|
export function validateRequiredString(
|
|
value: unknown,
|
|
fieldName: string,
|
|
): void {
|
|
if (value === null || value === undefined || value === "") {
|
|
throw new ValidationError(fieldName + " is required", fieldName);
|
|
}
|
|
if (typeof value !== "string") {
|
|
throw new ValidationError(fieldName + " must be a string", fieldName);
|
|
}
|
|
}
|
|
|
|
export function validateNumber(
|
|
value: unknown,
|
|
fieldName: string,
|
|
opts?: { min?: number; max?: number; allowZero?: boolean },
|
|
): number {
|
|
if (value === null || value === undefined || value === "") {
|
|
throw new ValidationError(fieldName + " is required", fieldName);
|
|
}
|
|
const numValue = typeof value === "string"
|
|
? parseFloat(value)
|
|
: typeof value === "number"
|
|
? value
|
|
: NaN;
|
|
if (isNaN(numValue)) {
|
|
throw new ValidationError(fieldName + " must be a valid number", fieldName);
|
|
}
|
|
if (opts?.min !== undefined && numValue < opts.min) {
|
|
throw new ValidationError(
|
|
fieldName + " must be at least " + opts.min,
|
|
fieldName,
|
|
);
|
|
}
|
|
if (opts?.max !== undefined && numValue > opts.max) {
|
|
throw new ValidationError(
|
|
fieldName + " must be at most " + opts.max,
|
|
fieldName,
|
|
);
|
|
}
|
|
if (!opts?.allowZero && numValue === 0) {
|
|
throw new ValidationError(
|
|
fieldName + " must be greater than zero",
|
|
fieldName,
|
|
);
|
|
}
|
|
return numValue;
|
|
}
|
|
|
|
export function validateInteger(
|
|
value: unknown,
|
|
fieldName: string,
|
|
opts?: { min?: number; max?: number; positive?: boolean },
|
|
): number {
|
|
const numValue = validateNumber(value, fieldName, opts);
|
|
if (!Number.isInteger(numValue)) {
|
|
throw new ValidationError(fieldName + " must be a whole number", fieldName);
|
|
}
|
|
if (opts?.positive && numValue <= 0) {
|
|
throw new ValidationError(
|
|
fieldName + " must be a positive integer",
|
|
fieldName,
|
|
);
|
|
}
|
|
return numValue;
|
|
}
|
|
|
|
export function withTimeout<T>(
|
|
promise: Promise<T>,
|
|
timeoutMs: number,
|
|
errMsg: string = "Timeout",
|
|
): Promise<T> {
|
|
return Promise.race([
|
|
promise,
|
|
new Promise<T>((_, reject) =>
|
|
setTimeout(() => reject(new Error(errMsg)), timeoutMs)
|
|
),
|
|
]);
|
|
}
|
|
|
|
export function handleApiError(error: unknown): Response {
|
|
console.error("API Error:", error);
|
|
if (error instanceof ValidationError) {
|
|
return createValidationErrorResponse(error.message, error.field);
|
|
}
|
|
if (error instanceof NetworkError) {
|
|
return createNetworkErrorResponse(error.message);
|
|
}
|
|
if (error instanceof Error) {
|
|
if (
|
|
error.message.includes("timeout") || error.message.includes("timed out")
|
|
) {
|
|
return createTimeoutErrorResponse(30000);
|
|
}
|
|
if (
|
|
error.message.includes("network") || error.message.includes("connection")
|
|
) {
|
|
return createNetworkErrorResponse(error.message);
|
|
}
|
|
return new Response(
|
|
JSON.stringify({
|
|
code: "INTERNAL_ERROR",
|
|
message: "An unexpected error occurred",
|
|
details: error.message,
|
|
}),
|
|
{ status: 500, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
return new Response(
|
|
JSON.stringify({
|
|
code: "INTERNAL_ERROR",
|
|
message: "An unexpected error occurred",
|
|
}),
|
|
{ status: 500, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|