From 712b25759d9147dc9beb9b1307fd6f3cd0e242af Mon Sep 17 00:00:00 2001 From: DOLBA3B Date: Thu, 21 May 2026 11:00:33 +0200 Subject: [PATCH] Add VERBOSE_ERRORS env var to surface internal error messages --- backend/app.js | 13 ++++++++----- backend/index.js | 7 +++++++ backend/lib/config.js | 9 ++++++++- docs/src/advanced-config/index.md | 9 +++++++++ 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/backend/app.js b/backend/app.js index b1f047661c..26b7d75895 100644 --- a/backend/app.js +++ b/backend/app.js @@ -2,7 +2,7 @@ import bodyParser from "body-parser"; import compression from "compression"; import express from "express"; import fileUpload from "express-fileupload"; -import { isDebugMode } from "./lib/config.js"; +import { isDebugMode, isVerboseErrors } from "./lib/config.js"; import cors from "./lib/express/cors.js"; import jwt from "./lib/express/jwt.js"; import { debug, express as logger } from "./logger.js"; @@ -58,12 +58,15 @@ app.use(jwt()); app.use("/", mainRoutes); // production error handler -// no stacktraces leaked to user -app.use((err, req, res, _) => { +// non-public errors are masked as "Internal Error" unless VERBOSE_ERRORS is set +// stack traces stay gated behind DEBUG +app.use((err, _req, res, _next) => { + const exposeMessage = err.public || isVerboseErrors(); + const payload = { error: { code: err.status || 500, - message: err.public ? err.message : "Internal Error", + message: exposeMessage ? err.message : "Internal Error", }, }; @@ -71,7 +74,7 @@ app.use((err, req, res, _) => { payload.error.message_i18n = err.message_i18n; } - if (isDebugMode() || (req.baseUrl + req.path).includes("nginx/certificates")) { + if (isDebugMode()) { payload.debug = { stack: typeof err.stack !== "undefined" && err.stack ? err.stack.split("\n") : null, previous: err.previous, diff --git a/backend/index.js b/backend/index.js index 0028566727..62a66259ac 100644 --- a/backend/index.js +++ b/backend/index.js @@ -3,6 +3,7 @@ import app from "./app.js"; import internalCertificate from "./internal/certificate.js"; import internalIpRanges from "./internal/ip_ranges.js"; +import { isVerboseErrors } from "./lib/config.js"; import { global as logger } from "./logger.js"; import { migrateUp } from "./migrate.js"; import { getCompiledSchema } from "./schema/index.js"; @@ -31,6 +32,12 @@ async function appStart() { const server = app.listen(3000, () => { logger.info(`Backend PID ${process.pid} listening on port 3000 ...`); + if (isVerboseErrors()) { + logger.warn( + "VERBOSE_ERRORS is enabled, internal error messages will be returned to API clients", + ); + } + process.on("SIGTERM", () => { logger.info(`PID ${process.pid} received SIGTERM`); server.close(() => { diff --git a/backend/lib/config.js b/backend/lib/config.js index a491c190e8..031e5d1cbe 100644 --- a/backend/lib/config.js +++ b/backend/lib/config.js @@ -217,6 +217,13 @@ const isPostgres = () => { */ const isDebugMode = () => !!process.env.DEBUG; +/** + * Should non-public error messages be returned to API clients? + * + * @returns {boolean} + */ +const isVerboseErrors = () => !!process.env.VERBOSE_ERRORS; + /** * Are we running in CI? * @@ -259,4 +266,4 @@ const useLetsencryptServer = () => { return null; }; -export { isCI, configHas, configGet, isSqlite, isMysql, isPostgres, isDebugMode, getPrivateKey, getPublicKey, useLetsencryptStaging, useLetsencryptServer }; +export { isCI, configHas, configGet, isSqlite, isMysql, isPostgres, isDebugMode, isVerboseErrors, getPrivateKey, getPublicKey, useLetsencryptStaging, useLetsencryptServer }; diff --git a/docs/src/advanced-config/index.md b/docs/src/advanced-config/index.md index 3ab04ce25b..f4836fce0d 100644 --- a/docs/src/advanced-config/index.md +++ b/docs/src/advanced-config/index.md @@ -248,3 +248,12 @@ On startup, we generate a resolvers directive for Nginx unless this is defined: In this configuration, all DNS queries performed by Nginx will fall to the `/etc/hosts` file and then the `/etc/resolv.conf`. + +## Verbose Error Messages + +By default the API returns a generic `Internal Error` and the real cause stays in the container logs. Set `VERBOSE_ERRORS` to return the real message in the API response (and the UI): + +```yml + environment: + VERBOSE_ERRORS: 'true' +``` \ No newline at end of file