diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c7a24dd..80dbb40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,6 +48,11 @@ jobs: - name: Lint run: npm run lint + # TODO: duplicate build step; consider removing from here and only keeping in build-and-push + # once we figure a better way to prevent build errors from reaching build-and-push job. + - name: Build + run: npm run build + - name: Test run: npm run test diff --git a/src/api/middleware/errorHandler.ts b/src/api/middleware/errorHandler.ts index 8912991..545c833 100644 --- a/src/api/middleware/errorHandler.ts +++ b/src/api/middleware/errorHandler.ts @@ -31,26 +31,37 @@ export function errorHandler( ): void { const errorId = generateErrorId(); + // Extract error properties with safe type handling. + // We accept Error | unknown and need to safely access properties that may exist + // on Error objects (message), HTTP error objects (statusCode, status), or custom + // error objects (code). Type casting to any is necessary to access these + // arbitrary properties while maintaining runtime safety through optional chaining. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const errorObj = err as any; // Allow accessing arbitrary properties + const errorMessage = errorObj?.message ?? String(err); + const errorType = errorObj?.constructor?.name ?? 'Unknown'; + const statusCode = errorObj?.statusCode ?? errorObj?.status ?? 500; + const errorCode = errorObj?.code; + // Log the full error internally with correlation ID (safe location) log.error({ message: 'API error', data: { errorId, - type: err?.constructor?.name || 'Unknown', - message: err?.message || String(err) + type: errorType, + message: errorMessage } }); // Return safe error response to client with error ID for tracing - const statusCode = err?.statusCode || err?.status || 500; const errorResponse: ErrorResponse = { error: 'An error occurred processing your request', errorId }; // Add code if it's a validation error or known error type - if (err?.code) { - errorResponse.code = err.code; + if (errorCode) { + errorResponse.code = errorCode; } res.status(statusCode).json(errorResponse); diff --git a/src/api/middleware/logging.ts b/src/api/middleware/logging.ts index a453fb8..f18bbb5 100644 --- a/src/api/middleware/logging.ts +++ b/src/api/middleware/logging.ts @@ -11,9 +11,18 @@ export function requestLogging(req: Request, res: Response, next: NextFunction): const startTime = Date.now(); const clientIp = getClientIP(req); - // Override res.end to capture response - const originalEnd = res.end; - res.end = function (chunk?: Buffer | string, encoding?: string): Response { + // Override res.end to capture response timings and metadata. + // Express Response.end has multiple overloaded signatures: + // end(): Response + // end(callback: Function): Response + // end(data: Buffer | string): Response + // end(data: Buffer | string, callback: Function): Response + // end(data: Buffer | string, encoding: string, callback: Function): Response + // We accept variadic args to match all overloads while preserving the original + // function's ability to handle any combination of parameters. + const originalEnd = res.end.bind(res); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + res.end = function (...args: any[]): Response { const duration = Date.now() - startTime; log.debug({ @@ -27,8 +36,8 @@ export function requestLogging(req: Request, res: Response, next: NextFunction): } }); - return originalEnd.call(this, chunk, encoding); - }; + return originalEnd(...args); + } as typeof res.end; next(); }