fix(backend): route every async handler through the error middleware (#59)
Express 4 does not forward a rejected promise from an async handler, so an unwrapped async route never responds at all — the request hangs until the client gives up, nothing reaches the error middleware, and monitoring sees an open connection rather than a 500. That silence is the shape of the 2026-08-17 incident, where an unhandled rejection left every item query hanging and the storefront rendered it as an empty shop. `asyncRoute` was written in response, but it was only applied to some routes: 30 handlers added afterwards were still bare, including register, login, the whole cart, and PayPal checkout. Wraps all 30, plus two the issue's inventory missed. `attachCustomer` is a bare async middleware mounted globally in app.ts, so a rejection in its session lookup would hang every request in the application — including the 25 handlers that were already wrapped correctly, which meant the guarantee did not actually hold anywhere. The PayPal webhook registers on a second router named `webhookRouter`, so an audit grepping for `router.` walked straight past it. Adds a unit test that scans the route sources and fails on any registration whose handler is not wrapped. A convention already half-forgotten once will be forgotten again, and enforcement is what the issue asked for; ESLint would be the better home for it but there is no ESLint in this repo yet (#60). The test walks parens rather than lines, so it also catches a handler whose `async` sits on its own line, and it matches any `*Router` name rather than just `router` — the two ways the existing bare handlers escaped notice. It is deleted along with `asyncRoute` if the project moves to Express 5, which forwards rejections natively. No behaviour changes on the success path; the failure path turns a hung request into a logged 500. Closes #59 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+5
-1
@@ -14,6 +14,7 @@ import publicRouter from './routes/public';
|
||||
import cartRouter from './routes/cart';
|
||||
import shippingAddressesRouter from './routes/shippingAddresses';
|
||||
import { attachCustomer } from './middleware/customerAuth';
|
||||
import { asyncRoute } from './asyncRoute';
|
||||
|
||||
const app = express();
|
||||
// Express advertises itself in X-Powered-By by default, which hands an
|
||||
@@ -24,7 +25,10 @@ app.set('trust proxy', 1);
|
||||
app.use('/webhooks/paypal', express.json(), cartCheckoutWebhookRouter);
|
||||
app.use(express.json());
|
||||
app.use(cookieParser());
|
||||
app.use(attachCustomer);
|
||||
// Wrapped like any route: attachCustomer awaits a session lookup, and mounted
|
||||
// globally an unforwarded rejection here would hang every request in the app —
|
||||
// including the routes that wrap their own handlers correctly.
|
||||
app.use(asyncRoute(attachCustomer));
|
||||
app.use('/uploads', express.static(process.env.UPLOADS_DIR || '/app/uploads'));
|
||||
|
||||
app.get('/api/config', (_req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user