import { NextResponse } from 'next/server'; import { ZodError } from 'zod'; import { logger } from '@/lib/logger'; export class AppError extends Error { constructor( public statusCode: number, message: string, public code?: string, ) { super(message); this.name = 'AppError'; } } export class NotFoundError extends AppError { constructor(entity: string) { super(404, `${entity} not found`, 'NOT_FOUND'); } } export class ForbiddenError extends AppError { constructor(message = 'Insufficient permissions') { super(403, message, 'FORBIDDEN'); } } export class ValidationError extends AppError { constructor( message: string, public details?: Array<{ field: string; message: string }>, ) { super(400, message, 'VALIDATION_ERROR'); } } export class ConflictError extends AppError { constructor(message: string) { super(409, message, 'CONFLICT'); } } export class RateLimitError extends AppError { constructor(public retryAfter: number) { super(429, 'Too many requests', 'RATE_LIMITED'); } } /** * Converts any thrown value into a sanitised NextResponse. * Never leaks stack traces, internal paths, or database error details to the client. */ export function errorResponse(error: unknown): NextResponse { if (error instanceof AppError) { const body: Record = { error: error.message, code: error.code, }; if (error instanceof ValidationError && error.details) { body.details = error.details; } if (error instanceof RateLimitError) { body.retryAfter = error.retryAfter; } return NextResponse.json(body, { status: error.statusCode }); } if (error instanceof ZodError) { return NextResponse.json( { error: 'Validation failed', code: 'VALIDATION_ERROR', details: error.errors.map((e) => ({ field: e.path.join('.'), message: e.message, })), }, { status: 400 }, ); } // Log full details server-side; never send them to the client. logger.error({ err: error }, 'Unhandled error'); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); }