openvibers/openvibe.host Commits

A database error while resolving a request's host now answers 500 instead of hanging the request: the async host-dispatch middleware (tenant.resolve/handle read PostgreSQL since ADR-035) had no error path, and Express 4 does not catch a rejected async handler, so the rejection was unhandled and the socket stayed open. The error reaches the app's error handler, counted as an unknown-host request. test/host-dispatch-error.test.js: resolve rejects -> 500 within 3 s, then the site serves 200 again. Found by the estate-wide floating-promise sweep; reviewed independently (VERDICT: SHIP). npm test 42/42 test files passed.

OP OpenVibers committed 29 Sept 2026 · de873f6d6b · 2 files, +72 −5


Changes

ddiff --git a/server/app.js b/server/app.js
iindex bcd1b90..ce0f578 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -114,12 +114,19 @@ async function createApp(opts = {}) {
app.use(httpMetrics.middleware);
// ── Host dispatch: tenant sites never reach anything below ──
++ // resolve/handle read the database (ADR-035: async), so a rejection here must reach the error
++ // handler below: Express 4 does not catch a rejected async handler, which would hang the request.
app.use(async (req, res, next) => {
-- const target = await tenant.resolve(req.headers.host);
-- req.hostTarget = target.kind;
-- if (target.kind === 'dashboard') return next();
-- if (target.kind === 'site') return await tenant.handle(req, res, target.site);
-- return tenant.unknownHost(req, res);
++ try {
++ const target = await tenant.resolve(req.headers.host);
++ req.hostTarget = target.kind;
++ if (target.kind === 'dashboard') return next();
++ if (target.kind === 'site') return await tenant.handle(req, res, target.site);
++ return tenant.unknownHost(req, res);
++ } catch (err) {
++ req.hostTarget = req.hostTarget || 'unknown';
++ return next(err);
++ }
});
app.use(contracts.http.middleware());
ddiff --git a/test/host-dispatch-error.test.js b/test/host-dispatch-error.test.js
nnew file mode 100644
iindex 0000000..b31ad28
---- /dev/null
++++ b/test/host-dispatch-error.test.js
@@@ -0,0 +1,60 @@
++'use strict';
++/**
++ * Regression (ADR-035): the Host dispatch middleware in server/app.js awaits tenant.resolve and
++ * tenant.handle, which read the database asynchronously. Express 4 does not catch a rejected async
++ * handler: a transient database error used to escape as an unhandled rejection and leave the request
++ * hanging with no response. It must reach the app's error handler instead (a logged 500).
++ */
++const assert = require('assert');
++const { boot, check, done } = require('./stageb/boot');
++
++/** The response, or a failure the moment it is clear no response is coming. */
++function withDeadline(promise, what) {
++ return Promise.race([
++ promise,
++ new Promise((_, reject) => {
++ const timer = setTimeout(() => reject(new Error(`${what}: no response after 3000ms — the rejection never reached the error handler`)), 3000);
++ promise.then(() => clearTimeout(timer), () => clearTimeout(timer));
++ }),
++ ]);
++}
++
++(async () => {
++ const t = await boot();
++ const alice = t.user('alice');
++
++ await check('a failing Host resolve answers 500 instead of hanging the request', async () => {
++ const original = t.ctx.tenant.resolve;
++ t.ctx.tenant.resolve = async () => { throw new Error('transient database error'); };
++ try {
++ const r = await withDeadline(t.get('alice.openvibe.host', '/'), 'resolve');
++ assert.strictEqual(r.status, 500, `expected 500, got ${r.status}: ${r.text.slice(0, 120)}`);
++ assert.match(r.text, /Something went wrong/);
++ } finally {
++ t.ctx.tenant.resolve = original;
++ }
++ });
++
++ await check('a failing tenant handler answers 500 instead of hanging the request', async () => {
++ const project = await t.project(alice, 'Dispatch errors');
++ const site = await t.site(alice, project.id, 'alice');
++ await t.deploy(alice, site.id, { 'index.html': 'hello' });
++ const original = t.ctx.tenant.handle;
++ t.ctx.tenant.handle = async () => { throw new Error('transient database error'); };
++ try {
++ const r = await withDeadline(t.get('alice.openvibe.host', '/'), 'handle');
++ assert.strictEqual(r.status, 500, `expected 500, got ${r.status}: ${r.text.slice(0, 120)}`);
++ } finally {
++ t.ctx.tenant.handle = original;
++ }
++ });
++
++ await check('the site is served normally again once the failure passes', async () => {
++ const r = await t.get('alice.openvibe.host', '/');
++ assert.strictEqual(r.status, 200);
++ assert.strictEqual(r.text, 'hello');
++ });
++
++ await t.close();
++ done();
++})();