Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
});
Expand All @@ -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'
});
Expand Down Expand Up @@ -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 = {
Expand Down
1 change: 1 addition & 0 deletions src/middlewares/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
14 changes: 12 additions & 2 deletions src/services/paramParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/

/**
Expand Down Expand Up @@ -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
}
};
Expand All @@ -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
}
};
Expand Down
12 changes: 12 additions & 0 deletions src/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading