diff --git a/src/middlewares/error.middleware.test.ts b/src/middlewares/error.middleware.test.ts new file mode 100644 index 0000000..4a7521e --- /dev/null +++ b/src/middlewares/error.middleware.test.ts @@ -0,0 +1,66 @@ +import { errorHandler } from './error.middleware'; +import { logger } from '../utils/logger.utils'; +import { RpcTimeoutError } from '../utils/rpc-timeout.utils'; + +type MockResponse = { + status: jest.Mock; + json: jest.Mock; +}; + +jest.mock('../utils/logger.utils', () => ({ + logger: { + warn: jest.fn(), + }, +})); + +describe('Error Middleware', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should warn with structured creator list timeout context and keep the response unchanged', () => { + const req: any = { + method: 'GET', + originalUrl: '/api/v1/creators?limit=10&offset=0', + query: { + limit: '10', + offset: '0', + }, + requestId: 'request-123', + hostname: 'localhost', + protocol: 'http', + }; + + const res: MockResponse = { + status: jest.fn().mockReturnThis(), + json: jest.fn().mockReturnThis(), + }; + + const timeoutError = new RpcTimeoutError('creatorListQuery', 5000); + + errorHandler(timeoutError, req, res as any, jest.fn()); + + expect(logger.warn).toHaveBeenCalledWith( + expect.objectContaining({ + msg: 'Creator list request timed out', + requestId: 'request-123', + route: 'GET /api/v1/creators?limit=10&offset=0', + queryParams: { + limit: '10', + offset: '0', + }, + elapsedMs: 5000, + timeoutMs: 5000, + }) + ); + expect(res.status).toHaveBeenCalledWith(500); + expect(res.json).toHaveBeenCalledWith( + expect.objectContaining({ + success: false, + code: expect.any(String), + message: expect.any(String), + requestId: 'request-123', + }) + ); + }); +}); diff --git a/src/middlewares/error.middleware.ts b/src/middlewares/error.middleware.ts index 3b436be..8e06d3c 100644 --- a/src/middlewares/error.middleware.ts +++ b/src/middlewares/error.middleware.ts @@ -6,13 +6,14 @@ import chalk from 'chalk'; import { z } from 'zod'; import { ErrorCode, ErrorCodeType } from '../constants/error.constants'; import { logger } from '../utils/logger.utils'; +import { RpcTimeoutError } from '../utils/rpc-timeout.utils'; import { mapUnknownRouteError } from '../utils/route-error.utils'; export class ApiError extends Error { statusCode: number; isOperational: boolean; errorCode?: ErrorCodeType; - + constructor( statusCode: number, message: string, @@ -48,6 +49,17 @@ export const temporarilyDisabled = ( next(error); }; +const isCreatorListTimeout = ( + err: unknown, + req: Request +): err is RpcTimeoutError => { + return ( + err instanceof RpcTimeoutError && + req.method === 'GET' && + req.path === '/api/v1/creators' + ); +}; + // Improved global error handling middleware export const errorHandler: ErrorRequestHandler = ( err: any, @@ -60,6 +72,17 @@ export const errorHandler: ErrorRequestHandler = ( console.error('URL:', req.method, req.originalUrl); console.error('Error:', err); + if (isCreatorListTimeout(err, req)) { + logger.warn({ + msg: 'Creator list request timed out', + requestId: req.requestId, + route: `${req.method} ${req.originalUrl}`, + queryParams: req.query, + elapsedMs: err.timeoutMs, + timeoutMs: err.timeoutMs, + }); + } + // Handle Zod validation errors if (err instanceof z.ZodError || err.name === 'ZodError') { res.status(400).json({ @@ -127,7 +150,11 @@ export const errorHandler: ErrorRequestHandler = ( } // Handle oversized request payload (413) - if (err.type === 'entity.too.large' || err.status === 413 || err.statusCode === 413) { + if ( + err.type === 'entity.too.large' || + err.status === 413 || + err.statusCode === 413 + ) { logger.warn({ msg: 'Request payload too large', route: `${req.method} ${req.originalUrl}`,