randomayzer/src/core/errors/http-errors.ts

286 lines
7.1 KiB
TypeScript

import { NextResponse } from 'next/server';
import { ZodError } from 'zod';
import { VkClientError } from '@/integrations/vk/vk-errors';
export abstract class AppError extends Error {
abstract readonly statusCode: number;
abstract readonly code: string;
readonly details?: any;
constructor(message: string, details?: any) {
super(message);
this.name = this.constructor.name;
this.details = details;
Object.setPrototypeOf(this, new.target.prototype);
}
}
export class ValidationError extends AppError {
readonly statusCode = 400;
readonly code = 'VALIDATION_ERROR';
}
export class UnauthorizedError extends AppError {
readonly statusCode = 401;
readonly code = 'UNAUTHORIZED';
}
export class VkReauthenticationRequiredError extends AppError {
readonly statusCode = 401;
readonly code = 'VK_REAUTHENTICATION_REQUIRED';
}
export class ForbiddenError extends AppError {
readonly statusCode = 403;
readonly code = 'FORBIDDEN';
}
export class VkPermissionRequiredError extends AppError {
readonly statusCode = 403;
readonly code = 'VK_PERMISSION_REQUIRED';
}
export class VkResourcePrivateError extends AppError {
readonly statusCode = 403;
readonly code = 'VK_RESOURCE_PRIVATE';
}
export class NotFoundError extends AppError {
readonly statusCode = 404;
readonly code = 'NOT_FOUND';
}
export class ConflictError extends AppError {
readonly statusCode = 409;
readonly code = 'CONFLICT';
}
export class IdempotencyKeyReusedError extends AppError {
readonly statusCode = 409;
readonly code = 'IDEMPOTENCY_KEY_REUSED';
constructor(
message: string = 'Idempotency key was previously used with different request parameters',
details?: any
) {
super(message, details);
}
}
export class DrawAlreadyCompletedError extends AppError {
readonly statusCode = 409;
readonly code = 'DRAW_ALREADY_COMPLETED';
constructor(
message: string = 'Giveaway has already been drawn and finalized. Repeat draws are not permitted.',
details?: any
) {
super(message, details);
}
}
export class RateLimitError extends AppError {
readonly statusCode = 429;
readonly code = 'RATE_LIMIT_EXCEEDED';
}
export class DependencyUnavailableError extends AppError {
readonly statusCode = 503;
readonly code = 'DEPENDENCY_UNAVAILABLE';
}
export class InternalError extends AppError {
readonly statusCode = 500;
readonly code = 'INTERNAL_SERVER_ERROR';
}
/**
* Normalizes any error into a consistent JSON response
*/
export function handleApiError(error: unknown): NextResponse {
// Handle Zod validation errors
if (error instanceof ZodError) {
const formattedIssues = error.issues.map(i => ({
path: i.path.join('.'),
message: i.message,
}));
return NextResponse.json(
{
success: false,
error: {
code: 'VALIDATION_ERROR',
message: 'Invalid request payload',
details: formattedIssues,
},
},
{ status: 400 }
);
}
// Handle known AppErrors
if (error instanceof AppError) {
return NextResponse.json(
{
success: false,
error: {
code: error.code,
message: error.message,
details: error.details || null,
},
},
{ status: error.statusCode }
);
}
// Handle typed VK client errors safely without exposing tokens or internal structures
if (error instanceof VkClientError) {
switch (error.category) {
case 'AUTH':
case 'REAUTHENTICATION_REQUIRED':
return NextResponse.json(
{
success: false,
error: {
code: 'VK_REAUTHENTICATION_REQUIRED',
message: 'VK authorization expired or required. Please reconnect your VK account.',
},
},
{ status: 401 }
);
case 'PERMISSION':
return NextResponse.json(
{
success: false,
error: {
code: 'VK_PERMISSION_REQUIRED',
message: 'Insufficient VK permissions to access this resource or perform this action.',
},
},
{ status: 403 }
);
case 'PRIVATE_RESOURCE':
return NextResponse.json(
{
success: false,
error: {
code: 'VK_RESOURCE_PRIVATE',
message: 'This VK resource or post is in a private/restricted community or profile.',
},
},
{ status: 403 }
);
case 'NOT_FOUND':
return NextResponse.json(
{
success: false,
error: {
code: 'VK_RESOURCE_NOT_FOUND',
message: 'VK post or resource was not found. Please check the post URL.',
},
},
{ status: 404 }
);
case 'RATE_LIMIT':
return NextResponse.json(
{
success: false,
error: {
code: 'VK_RATE_LIMIT_EXCEEDED',
message: 'VK API rate limit reached. Please retry in a few moments.',
},
},
{ status: 429 }
);
case 'TIMEOUT':
return NextResponse.json(
{
success: false,
error: {
code: 'VK_GATEWAY_TIMEOUT',
message: 'VK API did not respond in time. Please try again.',
},
},
{ status: 504 }
);
case 'INCOMPLETE_DATA':
case 'PAGINATION_LIMIT_REACHED':
return NextResponse.json(
{
success: false,
error: {
code: 'VK_INCOMPLETE_DATA',
message: error.message,
},
},
{ status: 502 }
);
default:
return NextResponse.json(
{
success: false,
error: {
code: 'VK_UPSTREAM_ERROR',
message: 'A temporary error occurred while communicating with VK API.',
},
},
{ status: 502 }
);
}
}
// Handle SyntaxError (Malformed JSON in request body)
if (error instanceof SyntaxError && 'body' in error) {
return NextResponse.json(
{
success: false,
error: {
code: 'INVALID_JSON',
message: 'Malformed JSON payload in request body',
},
},
{ status: 400 }
);
}
// Handle Prisma Known Request Errors
const anyErr = error as any;
if (anyErr?.code === 'P2002') {
return NextResponse.json(
{
success: false,
error: {
code: 'CONFLICT',
message: 'Resource conflict or duplicate constraint violation',
},
},
{ status: 409 }
);
}
// Unexpected runtime errors
console.error('Unhandled server error:', error);
const isProd = process.env.NODE_ENV === 'production';
const errorMessage = isProd
? 'An unexpected internal server error occurred'
: (error instanceof Error ? error.message : 'Internal Server Error');
return NextResponse.json(
{
success: false,
error: {
code: 'INTERNAL_SERVER_ERROR',
message: errorMessage,
},
},
{ status: 500 }
);
}