openvibers/openvibe.chat Commits

Unknown pages answer an HTML 404 page to a browser (browser check, WS-Q task 3: openvibe.chat answered every unknown path with JSON). A GET or HEAD that asks for text/html, outside /api/, /internal/ and /ws/, gets the site's own page inside the Frame: 404, noindex, the path escaped, links to global chat, rooms and messages. The API, those prefixes, other methods and clients not asking for HTML keep the JSON 404. Test: web.test.js

OP OpenVibers committed 26 Sept 2026 · f07cf8e5c7 · 2 files, +35 −0


Changes

ddiff --git a/server/web/pages.js b/server/web/pages.js
iindex f9fa1bf..d2fb03f 100644
---- a/server/web/pages.js
++++ b/server/web/pages.js
@@@ -476,6 +476,19 @@ ${can.manage ? `<form class="oc-form" method="post" action="/r/${esc(room.slug)}
router.get('/sitemap.xml', (req, res) => res.type('application/xml').set('Cache-Control', 'public, max-age=3600')
.send(`<?xml version="1.0" encoding="UTF-8"?>\n<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${['/', '/rooms', '/updates', ...rooms.list(null, { limit: 100 }).public.map((r) => `/r/${r.slug}`)].map((p) => `<url><loc>${site}${p}</loc></url>`).join('')}</urlset>\n`));
++ // ── Nothing here: a browser asking for a page gets the site's 404 page (the browser check found
++ // JSON). The API, /internal/, /ws/ and any client not asking for HTML keep the JSON 404 that
++ // server/app.js answers after this router.
++ router.use(async (req, res, next) => {
++ if ((req.method !== 'GET' && req.method !== 'HEAD') || /^\/(api|internal|ws)(\/|$)/.test(req.path) || !/\btext\/html\b/.test(req.get('accept') || '')) return next();
++ try {
++ await page(req, res, 404, {
++ title: 'Page not found', path: req.path, robots: 'noindex',
++ body: `<h1>Page not found</h1><p>There is no page at <code>${esc(req.path.slice(0, 200))}</code>. It may have moved, or the link may be wrong.</p><p><a href="/">Global chat</a> · <a href="/rooms">Rooms</a> · <a href="/messages">Messages</a></p>`,
++ });
++ } catch (err) { next(err); }
++ });
++
return router;
}
ddiff --git a/test/web.test.js b/test/web.test.js
iindex 3f17267..c1a0f48 100644
---- a/test/web.test.js
++++ b/test/web.test.js
@@@ -159,4 +159,26 @@ t('sign-in, the Frame, robots, sitemap and /updates', async () => {
assert.strictEqual((await req('GET', '/api/nope')).status, 404, 'the API keeps its JSON 404');
});
++t('unknown pages: a browser gets the site\'s HTML 404 page, the API and other clients keep JSON', async () => {
++ const html = { accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8' };
++ const r = await req('GET', '/__ovcheck-404', { headers: html });
++ assert.strictEqual(r.status, 404);
++ assert.match(r.headers.get('content-type'), /^text\/html/);
++ assert.match(r.text, /<h1>Page not found<\/h1>/);
++ assert.match(r.text, /<meta name="robots" content="noindex">/);
++ assert.match(r.text, /<code>\/__ovcheck-404<\/code>/);
++ assert.match(r.text, /\/shared\/navbar\.js\?v=/, 'inside the Frame, like every page');
++ const x = await req('GET', '/%3Cscript%3Ex', { headers: html });
++ assert.strictEqual(x.status, 404);
++ assert.ok(!x.text.includes('<script>x'), 'the path is escaped');
++ for (const [path, headers] of [['/api/nope', html], ['/internal/nope', html], ['/nope', {}], ['/nope', { accept: 'application/json' }]]) {
++ const j = await req('GET', path, { headers });
++ assert.strictEqual(j.status, 404, path);
++ assert.match(j.headers.get('content-type'), /^application\/json/, `${path} ${JSON.stringify(headers)}: JSON`);
++ }
++ const post = await req('POST', '/nope', { headers: html });
++ assert.strictEqual(post.status, 404);
++ assert.match(post.headers.get('content-type'), /^application\/json/, 'a form post to nowhere keeps JSON');
++});
++
t.run(async () => { if (h && h.close) await h.close(); });