Summary
Multiple Express route handlers in src/server/routes.ts are async functions that perform property destructuring and await calls without wrapping them in try/catch. When any of these throw (e.g. due to a null body, a JSON.parse error, or a downstream failure), the resulting unhandled promise rejection crashes the entire Express process with an UnhandledPromiseRejectionWarning. Any user or attacker who can reach /api (e.g. via loopback) can DoS the server with a single malformed request.
Panel verdict: P0, VERIFIED, EXISTING_DEFECT, unanimous consensus.
Affected Files
| File |
Lines |
Issue |
src/server/routes.ts |
L470–L485 |
async POST /chat/:slug/:id/messages — await session.sendMessage() not guarded |
src/server/routes.ts |
L422–L429 |
async GET /preflight/gh — await checkGh() wrapped but error re-thrown if res is closed |
src/server/index.ts |
L29 |
No global Express async error handler mounted |
Root Cause — Code Evidence
/chat/:slug/:id/messages async handler (src/server/routes.ts, lines 470–485):
router.post('/chat/:slug/:id/messages', async (req, res) => {
const { slug, id } = req.params;
if (!validId(slug) || !validId(id)) return res.status(400).json({ error: 'Invalid id' });
const text = (req.body as { text?: unknown }).text;
if (typeof text !== 'string' || !text.trim()) {
return res.status(400).json({ error: 'text is required' });
}
const session = chat.getOrCreate({ slug, reportId: id });
if (!session) return res.status(404).json({ error: 'Scan or report not found' });
try {
await session.sendMessage(text.trim());
res.json({ ok: true });
} catch (err) {
res.status(409).json({ error: err instanceof Error ? err.message : String(err) });
}
// ↑ If res is already closed (client disconnected) when sendMessage() resolves,
// res.json() throws — this throw is NOT caught and becomes an unhandled rejection.
});
No global error handler in src/server/index.ts:
const app = express();
app.disable('etag');
const api = createApiRouter();
app.use('/api', api);
// ↑ No app.use((err, req, res, next) => { ... }) at the end
// Express cannot catch async rejections without express-async-errors or manual wrapping
Impact
Steps to Reproduce
# Start Probus
npm run dev
# In another terminal: send a chat message and immediately close the connection
curl -X POST http://127.0.0.1:9090/api/chat/some-slug/some-id/messages \
-H 'Content-Type: application/json' \
-d '{"text": "hello"}' \
--max-time 0.001 # disconnect immediately
Observe the Probus process log for UnhandledPromiseRejection and potential crash.
Remediation
Option A — Install express-async-errors (simplest, zero code change)
npm install express-async-errors
// src/server/index.ts — add at the very top, before express is imported
import 'express-async-errors';
import express from 'express';
// ...
// Add a global error handler at the END of startServer(), after all routes:
app.use((err: Error, _req: Request, res: Response, _next: NextFunction) => {
console.error('[express error]', err.stack ?? err.message);
if (!res.headersSent) {
res.status(500).json({ error: 'Internal server error' });
}
});
Option B — Wrap every async handler manually
Add a utility wrapper:
const asyncRoute = (
fn: (req: Request, res: Response, next: NextFunction) => Promise<void>
) => (req: Request, res: Response, next: NextFunction) => fn(req, res, next).catch(next);
Then wrap every async handler:
router.post('/chat/:slug/:id/messages', asyncRoute(async (req, res) => {
// ... handler body unchanged
}));
Option C — Guard res.json calls after await
Check res.writableEnded before writing:
if (!res.writableEnded) {
res.json({ ok: true });
}
References
Summary
Multiple Express route handlers in
src/server/routes.tsareasyncfunctions that perform property destructuring andawaitcalls without wrapping them intry/catch. When any of these throw (e.g. due to anullbody, aJSON.parseerror, or a downstream failure), the resulting unhandled promise rejection crashes the entire Express process with anUnhandledPromiseRejectionWarning. Any user or attacker who can reach/api(e.g. via loopback) can DoS the server with a single malformed request.Panel verdict: P0, VERIFIED, EXISTING_DEFECT, unanimous consensus.
Affected Files
src/server/routes.tsasyncPOST/chat/:slug/:id/messages—await session.sendMessage()not guardedsrc/server/routes.tsasyncGET/preflight/gh—await checkGh()wrapped but error re-thrown ifresis closedsrc/server/index.tsRoot Cause — Code Evidence
/chat/:slug/:id/messagesasync handler (src/server/routes.ts, lines 470–485):No global error handler in
src/server/index.ts:Impact
/api/chat/:slug/:id/messageswith a disconnected client or unexpected body causes the entire Node.js process to crash.Steps to Reproduce
Observe the Probus process log for
UnhandledPromiseRejectionand potential crash.Remediation
Option A — Install express-async-errors (simplest, zero code change)
Option B — Wrap every async handler manually
Add a utility wrapper:
Then wrap every async handler:
Option C — Guard res.json calls after await
Check
res.writableEndedbefore writing:References