Compare commits

...

2 Commits

Author SHA1 Message Date
Gitea Actions
dd4b34edfa ci: Bump version to 0.1.13 [skip ci] 2025-12-26 21:44:58 +05:00
91fa2f0516 not sure why those errors got removed we'll see
All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 12m30s
2025-12-26 08:43:49 -08:00
4 changed files with 75 additions and 50 deletions

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "flyer-crawler",
"version": "0.1.12",
"version": "0.1.13",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "flyer-crawler",
"version": "0.1.12",
"version": "0.1.13",
"dependencies": {
"@bull-board/api": "^6.14.2",
"@bull-board/express": "^6.14.2",

View File

@@ -1,7 +1,7 @@
{
"name": "flyer-crawler",
"private": true,
"version": "0.1.12",
"version": "0.1.13",
"type": "module",
"scripts": {
"dev": "concurrently \"npm:start:dev\" \"vite\"",

View File

@@ -112,7 +112,7 @@ describe('errorHandler Middleware', () => {
// In test/dev, we now expect a stack trace for 5xx errors.
expect(response.body.message).toBe('A generic server error occurred.');
expect(response.body.stack).toBeDefined();
expect(response.body.stack).toEqual(expect.any(String));
expect(response.body.errorId).toEqual(expect.any(String));
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.any(Error),
@@ -136,15 +136,11 @@ describe('errorHandler Middleware', () => {
expect(mockLogger.warn).toHaveBeenCalledWith(
{
err: expect.any(Error),
validationErrors: undefined,
statusCode: 404,
},
'Client Error on GET /http-error-404: Resource not found',
);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('--- [TEST] UNHANDLED ERROR ---'),
expect.any(Error),
);
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
it('should handle a NotFoundError with a 404 status', async () => {
@@ -156,15 +152,11 @@ describe('errorHandler Middleware', () => {
expect(mockLogger.warn).toHaveBeenCalledWith(
{
err: expect.any(NotFoundError),
validationErrors: undefined,
statusCode: 404,
},
'Client Error on GET /not-found-error: Specific resource missing',
);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('--- [TEST] UNHANDLED ERROR ---'),
expect.any(NotFoundError),
);
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
it('should handle a ForeignKeyConstraintError with a 400 status and the specific error message', async () => {
@@ -176,15 +168,11 @@ describe('errorHandler Middleware', () => {
expect(mockLogger.warn).toHaveBeenCalledWith(
{
err: expect.any(ForeignKeyConstraintError),
validationErrors: undefined,
statusCode: 400,
},
'Client Error on GET /fk-error: The referenced item does not exist.',
);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('--- [TEST] UNHANDLED ERROR ---'),
expect.any(ForeignKeyConstraintError),
);
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
it('should handle a UniqueConstraintError with a 409 status and the specific error message', async () => {
@@ -196,15 +184,11 @@ describe('errorHandler Middleware', () => {
expect(mockLogger.warn).toHaveBeenCalledWith(
{
err: expect.any(UniqueConstraintError),
validationErrors: undefined,
statusCode: 409,
},
'Client Error on GET /unique-error: This item already exists.',
);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('--- [TEST] UNHANDLED ERROR ---'),
expect.any(UniqueConstraintError),
);
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
it('should handle a ValidationError with a 400 status and include the validation errors array', async () => {
@@ -225,10 +209,7 @@ describe('errorHandler Middleware', () => {
},
'Client Error on GET /validation-error: Input validation failed',
);
expect(consoleErrorSpy).toHaveBeenCalledWith(
expect.stringContaining('--- [TEST] UNHANDLED ERROR ---'),
expect.any(ValidationError),
);
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
it('should handle a DatabaseError with a 500 status and a generic message', async () => {
@@ -238,7 +219,7 @@ describe('errorHandler Middleware', () => {
// In test/dev, we now expect a stack trace for 5xx errors.
expect(response.body.message).toBe('A database connection issue occurred.');
expect(response.body.stack).toBeDefined();
expect(response.body.stack).toEqual(expect.any(String));
expect(response.body.errorId).toEqual(expect.any(String));
expect(mockLogger.error).toHaveBeenCalledWith(
expect.objectContaining({
err: expect.any(DatabaseError),
@@ -258,8 +239,14 @@ describe('errorHandler Middleware', () => {
expect(response.status).toBe(401);
expect(response.body).toEqual({ message: 'Invalid Token' });
// 4xx errors log as warn
expect(mockLogger.warn).toHaveBeenCalled();
expect(mockLogger.warn).toHaveBeenCalledWith(
{
err: expect.any(Error),
statusCode: 401,
},
'Client Error on GET /unauthorized-error-no-status: Invalid Token',
);
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
it('should handle an UnauthorizedError with explicit status', async () => {
@@ -267,6 +254,14 @@ describe('errorHandler Middleware', () => {
expect(response.status).toBe(401);
expect(response.body).toEqual({ message: 'Invalid Token' });
expect(mockLogger.warn).toHaveBeenCalledWith(
{
err: expect.any(Error),
statusCode: 401,
},
'Client Error on GET /unauthorized-error-with-status: Invalid Token',
);
expect(consoleErrorSpy).not.toHaveBeenCalled();
});
it('should call next(err) if headers have already been sent', () => {
@@ -311,6 +306,7 @@ describe('errorHandler Middleware', () => {
expect(response.body.message).toMatch(
/An unexpected server error occurred. Please reference error ID: \w+/,
);
expect(response.body.stack).toBeUndefined();
});
it('should return the actual error message for client errors (4xx) in production', async () => {

View File

@@ -1,5 +1,6 @@
// src/middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import crypto from 'crypto';
import { ZodError } from 'zod';
import {
ForeignKeyConstraintError,
@@ -24,45 +25,73 @@ export const errorHandler = (err: Error, req: Request, res: Response, next: Next
// Use the request-scoped logger if available, otherwise fall back to the global logger.
const log = req.log || logger;
// --- Handle Zod Validation Errors ---
// --- Handle Zod Validation Errors (from validateRequest middleware) ---
if (err instanceof ZodError) {
log.warn({ err: err.flatten() }, 'Request validation failed');
return res.status(400).json({
message: 'The request data is invalid.',
errors: err.issues.map((e) => ({ path: e.path, message: e.message })),
});
const statusCode = 400;
const message = 'The request data is invalid.';
const errors = err.issues.map((e) => ({ path: e.path, message: e.message }));
log.warn({ err, validationErrors: errors, statusCode }, `Client Error on ${req.method} ${req.path}: ${message}`);
return res.status(statusCode).json({ message, errors });
}
// --- Handle Custom Operational Errors ---
if (err instanceof NotFoundError) {
log.info({ err }, 'Resource not found');
return res.status(404).json({ message: err.message });
const statusCode = 404;
log.warn({ err, statusCode }, `Client Error on ${req.method} ${req.path}: ${err.message}`);
return res.status(statusCode).json({ message: err.message });
}
if (err instanceof ValidationError) {
log.warn({ err }, 'Validation error occurred');
return res.status(400).json({ message: err.message, errors: err.validationErrors });
const statusCode = 400;
log.warn(
{ err, validationErrors: err.validationErrors, statusCode },
`Client Error on ${req.method} ${req.path}: ${err.message}`,
);
return res.status(statusCode).json({ message: err.message, errors: err.validationErrors });
}
if (err instanceof UniqueConstraintError) {
log.warn({ err }, 'Constraint error occurred');
return res.status(409).json({ message: err.message }); // Use 409 Conflict for unique constraints
const statusCode = 409;
log.warn({ err, statusCode }, `Client Error on ${req.method} ${req.path}: ${err.message}`);
return res.status(statusCode).json({ message: err.message }); // Use 409 Conflict for unique constraints
}
if (err instanceof ForeignKeyConstraintError) {
log.warn({ err }, 'Foreign key constraint violation');
return res.status(400).json({ message: err.message });
const statusCode = 400;
log.warn({ err, statusCode }, `Client Error on ${req.method} ${req.path}: ${err.message}`);
return res.status(statusCode).json({ message: err.message });
}
// --- Handle Generic Errors ---
// Log the full error object for debugging. The pino logger will handle redaction.
log.error({ err }, 'An unhandled error occurred in an Express route');
// --- Handle Generic Client Errors (e.g., from express-jwt, or manual status setting) ---
const status = (err as any).status || (err as any).statusCode;
if (status && status >= 400 && status < 500) {
log.warn({ err, statusCode: status }, `Client Error on ${req.method} ${req.path}: ${err.message}`);
return res.status(status).json({ message: err.message });
}
// --- Handle All Other (500-level) Errors ---
const errorId = crypto.randomBytes(4).toString('hex');
log.error(
{
err,
errorId,
req: { method: req.method, url: req.url, headers: req.headers, body: req.body },
},
`Unhandled API Error (ID: ${errorId})`,
);
// Also log to console in test environment for visibility in test runners
if (process.env.NODE_ENV === 'test') {
console.error(`--- [TEST] UNHANDLED ERROR (ID: ${errorId}) ---`, err);
}
// In production, send a generic message to avoid leaking implementation details.
if (process.env.NODE_ENV === 'production') {
return res.status(500).json({ message: 'An internal server error occurred.' });
return res.status(500).json({
message: `An unexpected server error occurred. Please reference error ID: ${errorId}`,
});
}
// In non-production environments (dev, test, etc.), send more details for easier debugging.
return res.status(500).json({ message: err.message, stack: err.stack });
return res.status(500).json({ message: err.message, stack: err.stack, errorId });
};