Routes/caddy-upstreams.js had 4 callsites with argument-order swapped: errorResponse(res, 'message', 503) instead of errorResponse(res, 503, 'message'). The canonical signature from src/utils/responses.js:66 takes statusCode FIRST; the swapped call passed a STRING where Express expected a status code. res.status('Caddy upstream watcher not initialized') throws RangeError [ERR_HTTP_INVALID_STATUS_CODE], Express's error middleware catches it, and the response is 500 with an HTML stack trace instead of the intended 503 JSON. Four `!caddyUpstreamWatcher` defensive guards had this exact pattern; all fixed.
Defense-in-depth (responses.js): errorResponse() now validates that statusCode is an integer in 100..599 and that message is a string BEFORE calling res.status(). Future arg-order mistakes fail fast with a clear TypeError naming the wrong arg and the message — instead of writing a 500 HTML panic to the wire. Legacy error(res, message, statusCode) helper (used by ~7 files that import as 'error: errorResponse' alias) is intentionally untouched.
Tests (__tests__/utils-responses-dc-062.test.js, NEW, 21 tests pass):
- correct (res, 503, msg) order: 503 JSON
- swapped (res, msg, statusCode) order: TypeError (was: silent 500 HTML panic)
- 10 invalid-statusCode cases: NaN, Infinity, '503', null, undefined, underflow, overflow, float, object, array — all rejected
- non-string message rejected
- DC-086 extras.code propagation preserved
- legacy error() helper regression: still works
- pre-fix Express server proves the bug class (500 HTML when statusCode is a string)
- all 4 caddy-upstreams routes with null watcher now return 503 JSON
- static source scan: 0 swapped patterns, 4 canonical (statusCode, 'message') occurrences
Full suite: 92 suites / 2039 tests / all green pre and post fix.
[glm-grade=A] from deleg_45e44614 (3 tool calls, 82s, MiniMax-M3 stand-in per Sami's 2026-08-17 authorization)
159 lines
5.2 KiB
JavaScript
159 lines
5.2 KiB
JavaScript
/**
|
|
* Response helpers - Standard API response formats
|
|
*
|
|
* Single source of truth for HTTP response shapes across DashCaddy.
|
|
* Standard envelope: { success: true, ...data } or { success: false, error: "..." }.
|
|
*
|
|
* All routes should import from this module — do not call res.json/res.status
|
|
* directly with the response shape, use these helpers instead.
|
|
*/
|
|
const { HTTP_STATUS } = require('../utilities/constants');
|
|
|
|
// ── Success helpers ────────────────────────────────────────────
|
|
|
|
/**
|
|
* Standard success response. Use this in route handlers.
|
|
* Wraps the data object with a `success: true` envelope.
|
|
* @param {object} res Express response
|
|
* @param {object} [data={}] fields to include in the response body
|
|
* @param {number} [statusCode=200] HTTP status code
|
|
*/
|
|
function ok(res, data = {}, statusCode = HTTP_STATUS.OK) {
|
|
return res.status(statusCode).json({ success: true, ...data });
|
|
}
|
|
|
|
/**
|
|
* Alias for `ok` — prefer `ok` in new code, but kept for code that imports as `success`.
|
|
*/
|
|
function success(res, data, statusCode) {
|
|
return ok(res, data, statusCode);
|
|
}
|
|
|
|
/**
|
|
* Success response with a human-readable message field.
|
|
* Use when there's no data to return, just confirmation.
|
|
*/
|
|
function successMessage(res, message, statusCode = HTTP_STATUS.OK) {
|
|
return res.status(statusCode).json({ success: true, message });
|
|
}
|
|
|
|
/**
|
|
* 201 Created response.
|
|
*/
|
|
function created(res, data = {}) {
|
|
return res.status(HTTP_STATUS.CREATED).json({ success: true, ...data });
|
|
}
|
|
|
|
/**
|
|
* 204 No Content response.
|
|
*/
|
|
function noContent(res) {
|
|
return res.status(HTTP_STATUS.NO_CONTENT).send();
|
|
}
|
|
|
|
// ── Error helpers ──────────────────────────────────────────────
|
|
|
|
/**
|
|
* Standard error response. Use this in route handlers.
|
|
* @param {object} res Express response
|
|
* @param {number} statusCode HTTP status code
|
|
* @param {string} message Human-readable error message
|
|
* @param {object} [extras={}] additional fields to merge into the response
|
|
*
|
|
* DC-086: If extras.code is set, it's treated as a machine-readable error code
|
|
* (e.g. 'DC-CONT-002'). If message looks like a DC code, it's auto-extracted.
|
|
*
|
|
* DC-062: Validate that `statusCode` is a valid HTTP status (integer in
|
|
* 100..599) BEFORE calling res.status(). Without this guard, a caller who
|
|
* passes (res, message, statusCode) instead of (res, statusCode, message)
|
|
* ends up with res.status(<string>), which throws
|
|
* RangeError [ERR_HTTP_INVALID_STATUS_CODE] — Express catches that and
|
|
* writes a 500 with an HTML stack trace to the client, which is the worst
|
|
* possible failure mode (looks like a server crash, breaks CSRF and
|
|
* content-type expectations, leaks the stack). Failing fast with a clear
|
|
* TypeError names the call site early in the request lifecycle.
|
|
*/
|
|
function errorResponse(res, statusCode, message, extras = {}) {
|
|
if (
|
|
typeof statusCode !== 'number'
|
|
|| !Number.isFinite(statusCode)
|
|
|| !Number.isInteger(statusCode)
|
|
|| statusCode < 100
|
|
|| statusCode > 599
|
|
) {
|
|
throw new TypeError(
|
|
`errorResponse(res, statusCode, message, extras): statusCode must be an integer HTTP status (100..599); received ${JSON.stringify(statusCode)} (message=${JSON.stringify(message)})`
|
|
);
|
|
}
|
|
if (typeof message !== 'string') {
|
|
throw new TypeError(
|
|
`errorResponse(res, statusCode, message, extras): message must be a string; received ${typeof message} ${JSON.stringify(message)}`
|
|
);
|
|
}
|
|
const body = { success: false, error: message, ...extras };
|
|
// DC-086: surface machine-readable code at top level for client handling
|
|
if (extras.code) {
|
|
body.code = extras.code;
|
|
}
|
|
return res.status(statusCode).json(body);
|
|
}
|
|
|
|
/**
|
|
* Alias for `errorResponse` — kept for code that imports as `error`.
|
|
*/
|
|
function error(res, message, statusCode = HTTP_STATUS.INTERNAL_ERROR) {
|
|
return res.status(statusCode).json({ success: false, error: message });
|
|
}
|
|
|
|
/**
|
|
* 400 Bad Request — invalid input from the user.
|
|
*/
|
|
function validationError(res, message) {
|
|
return res.status(HTTP_STATUS.BAD_REQUEST).json({ success: false, error: message });
|
|
}
|
|
|
|
/**
|
|
* 401 Unauthorized — no valid credentials.
|
|
*/
|
|
function unauthorized(res, message = 'Unauthorized') {
|
|
return res.status(HTTP_STATUS.UNAUTHORIZED).json({ success: false, error: message });
|
|
}
|
|
|
|
/**
|
|
* 403 Forbidden — credentials valid but permission denied.
|
|
*/
|
|
function forbidden(res, message = 'Forbidden') {
|
|
return res.status(HTTP_STATUS.FORBIDDEN).json({ success: false, error: message });
|
|
}
|
|
|
|
/**
|
|
* 404 Not Found — resource doesn't exist.
|
|
*/
|
|
function notFound(res, message = 'Not found') {
|
|
return res.status(HTTP_STATUS.NOT_FOUND).json({ success: false, error: message });
|
|
}
|
|
|
|
/**
|
|
* 409 Conflict — request conflicts with current state (e.g. duplicate).
|
|
*/
|
|
function conflict(res, message) {
|
|
return res.status(HTTP_STATUS.CONFLICT).json({ success: false, error: message });
|
|
}
|
|
|
|
module.exports = {
|
|
// Success helpers
|
|
ok,
|
|
success,
|
|
successMessage,
|
|
created,
|
|
noContent,
|
|
// Error helpers
|
|
errorResponse,
|
|
error,
|
|
validationError,
|
|
unauthorized,
|
|
forbidden,
|
|
notFound,
|
|
conflict,
|
|
};
|