// Generic wrapper that turns "unhandled error inside an async route // handler" into "logged 500 response". Without it, an uncaught throw // either crashes the request silently or surfaces as a generic 500 // with no server-side trace (#779 / DRY audit batch B). // // Migration story: `server/api/routes/plugins.ts` shipped a private // `wrapPluginExecute` with this exact shape, hard-coded to the // "plugins" log namespace. This module generalises the same idea so // every route file uses one wrapper. // // Scope: // // - Catches anything the inner handler throws. The wrapper logs // the raw error message on the server side (full detail kept for // debugging) and returns a 500 carrying ONLY the caller-supplied // `fallbackMessage` — never the raw `err.message`. Leaking // internal error text to clients would surface stack-shape // details, file paths, and library internals to anyone hitting // the endpoint. // - The inner handler stays in charge of 4xx mapping (validation, // not-found, etc.) — those paths respond + `return` inside the // handler before the wrapper's catch ever runs. // - When the response has already been sent (`headersSent`), a // second status can't be written, so the error is forwarded to // Express via `next(err)` instead. Measured against Express 5.2.1: // returning without forwarding leaves the request hanging with no // end to its body, while forwarding makes finalhandler destroy the // socket in milliseconds. No route wrapped here streams today, so // this branch is currently unreachable — it exists so the first // streaming route added doesn't inherit a silent hang. // // Naming: `namespace` is the log tag (e.g. "accounting", "wiki") — // matches the existing `log.info("namespace", …)` convention across // the route layer. `fallbackMessage` mirrors the strings the // hand-rolled try/catch blocks used before the migration ("failed to // load news items", "Failed to list tasks", …) so the client-facing // behaviour is unchanged. import type { NextFunction, Request, Response } from "express"; import { log } from "../system/logger/index.js"; import { errorMessage } from "./errors.js"; import { serverError, type ErrorSendable } from "./httpError.js"; // The TReq / TRes bounds name exactly what the catch path dereferences — // nothing more. // // They are NOT `extends Request` / `extends Response`. Express's // `Request
` uses its type parameters in mixed // variance positions, so a nominal `extends Request<…>` bound rejects // perfectly valid call sites like `Request