From 5060cfd3bf87487c90bc82d5e009a376d33bbfdd Mon Sep 17 00:00:00 2001 From: CDN-guy Date: Thu, 27 Aug 2026 00:46:23 -0400 Subject: [PATCH] unify error messages --- src/app.js | 31 ++++++++++++++++++++++++++----- src/middlewares/cache.js | 1 + src/services/paramParser.js | 14 ++++++++++++-- src/test/index.test.js | 12 ++++++++++++ 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/app.js b/src/app.js index fec85f1..8cad037 100644 --- a/src/app.js +++ b/src/app.js @@ -42,6 +42,7 @@ app.get('/images/*path', cache(), async (req, res, next) => { if (responseStatusCode !== 200) { return res.status(responseStatusCode).json({ error_message: "Error: Failed to retrieve original image", + details: `Origin server responded with status code ${responseStatusCode}`, image_origin_url: imageUrl, error_response_code: responseStatusCode }); @@ -54,14 +55,15 @@ app.get('/images/*path', cache(), async (req, res, next) => { } catch (err) { return res.status(415).json({ error_message: "Error: Input image is corrupted or unsupported format", - image_origin_url: imageUrl, - details: /** @type {Error} */ (err).message + details: /** @type {Error} */ (err).message, + image_origin_url: imageUrl }); } if (!metadata || !metadata.format || !SUPPORTED_INPUT_FORMATS.includes(metadata.format.toLowerCase())) { return res.status(415).json({ - error_message: `Error: Unsupported input image format '${metadata ? metadata.format : 'unknown'}'. Supported input formats: ${SUPPORTED_INPUT_FORMATS.join(', ')}`, + error_message: `Error: Unsupported input image format '${metadata ? metadata.format : 'unknown'}'`, + details: `Supported input formats: ${SUPPORTED_INPUT_FORMATS.join(', ')}`, image_origin_url: imageUrl, detected_format: metadata ? metadata.format : 'unknown' }); @@ -94,8 +96,27 @@ app.get('/healthz', (_req, res) => { }); // Block any request not matching /images/* or /healthz -app.get('/*others', (_req, res) => { - res.status(403).send("Access Denied: invalid request - not /images/*"); +app.get('/*others', (req, res) => { + res.status(403).json({ + error_message: "Access Denied: Invalid request path", + details: `Route '${req.path}' is not accessible - only /images/* and /healthz are allowed` + }); +}); + +// Global Express Error Handler for unexpected runtime exceptions +// @ts-ignore Express error-handling middleware requires 4 parameters +app.use((err, req, res, _next) => { + if (res.headersSent) { + return _next(err); + } + + console.error(`[express error] ${req.method} ${req.originalUrl}:`, err); + + const statusCode = err.statusCode || err.status || 500; + return res.status(statusCode).json({ + error_message: "Error: Internal processing error", + details: err.message || "An unexpected error occurred during image processing" + }); }); module.exports = { diff --git a/src/middlewares/cache.js b/src/middlewares/cache.js index e3f4674..5ca37e0 100644 --- a/src/middlewares/cache.js +++ b/src/middlewares/cache.js @@ -24,6 +24,7 @@ const cache = () => { if (error) { res.status(error.status).json({ error_message: error.message, + details: error.details, requested_format: error.requestedFormat }); return; diff --git a/src/services/paramParser.js b/src/services/paramParser.js index e1e3c6a..ff65c44 100644 --- a/src/services/paramParser.js +++ b/src/services/paramParser.js @@ -11,10 +11,18 @@ const { SUPPORTED_OUTPUT_FORMATS } = require('../config/constants'); * @property {string} cacheKey - Cache key for LRU cache lookup */ +/** + * @typedef {Object} ParamParseError + * @property {number} status - HTTP status code + * @property {string} message - Unified error message title + * @property {string} details - Detailed explanation of the error + * @property {string} [requestedFormat] - Requested format string + */ + /** * @typedef {Object} ParamParseResult * @property {ParsedImageParams | null} params - * @property {{ status: number, message: string, requestedFormat?: string } | null} error + * @property {ParamParseError | null} error */ /** @@ -158,6 +166,7 @@ function parseImageParams(req) { error: { status: 400, message: "Error: AVIF output format is currently disabled", + details: "The AVIF output format is temporarily disabled pending upstream libvips updates.", requestedFormat: req.query.f } }; @@ -167,7 +176,8 @@ function parseImageParams(req) { params: null, error: { status: 400, - message: `Error: Unsupported output format '${req.query.f}'. Supported formats: ${SUPPORTED_OUTPUT_FORMATS.join(', ')}`, + message: `Error: Unsupported output format '${req.query.f}'`, + details: `Supported output formats: ${SUPPORTED_OUTPUT_FORMATS.join(', ')}`, requestedFormat: req.query.f } }; diff --git a/src/test/index.test.js b/src/test/index.test.js index c143a52..33a498a 100644 --- a/src/test/index.test.js +++ b/src/test/index.test.js @@ -176,6 +176,18 @@ describe('Image Optimizer Service Tests', () => { assert.ok(json.error_message.includes('Input image is corrupted or unsupported format')); }); + it('should handle unhandled fetch errors via global error handler returning 500 JSON', async () => { + const res = await fetch(`http://127.0.0.1:${appPort}/images/valid.jpg`, { + headers: { + 'x-client-host': '127.0.0.1:1' + } + }); + assert.strictEqual(res.status, 500); + const json = await res.json(); + assert.strictEqual(json.error_message, 'Error: Internal processing error'); + assert.ok(typeof json.details === 'string'); + }); + it('should optimize image, return 200, and populate LRU cache headers', async () => { lru_cache.clear();