openvibers/openrestream Commits

A missing page answers the site's 404 page in the Frame (#174)

OP OpenVibers committed 12 days ago · 2095b1e2b0 · 4 files, +70 −1

A browser that asks for a path OpenRestream does not serve gets the site's own 404 page (the OpenVibe Frame, noindex, one line naming the host, links home, to streams and to sessions). An API, internal or OAuth path, or a request whose Accept is not text/html, keeps the problem document.

Changes

ddiff --git a/server/app.js b/server/app.js
iindex 9a0a7d5..5514e78 100644
---- a/server/app.js
++++ b/server/app.js
@@@ -186,7 +186,13 @@ function createApp({ rt, auth, keys, log = console, fetchImpl }) {
app.use('/', uiRoutes);
}
-- app.use((req, res) => contracts.http.sendProblem(res, 404, 'openre.not_found', { detail: `no route ${req.method} ${req.path}`, ctx: req.ov }));
++ app.use((req, res) => {
++ if (req.path.startsWith('/api/') || req.path.startsWith('/internal/') || req.path.startsWith('/oauth/') || req.accepts(['application/json', 'text/html']) !== 'text/html') {
++ return contracts.http.sendProblem(res, 404, 'openre.not_found', { detail: `no route ${req.method} ${req.path}`, ctx: req.ov });
++ }
++ return res.status(404).type('html').set('Cache-Control', 'no-cache').set('X-Robots-Tag', 'noindex')
++ .send(require('./ui/not-found').renderNotFound({ config }));
++ });
// eslint-disable-next-line no-unused-vars
app.use((err, req, res, _next) => {
if (err.type === 'entity.parse.failed') return contracts.http.sendProblem(res, 400, 'openre.bad_json', { detail: 'request body is not valid JSON', ctx: req.ov });
ddiff --git a/server/ui/layout.js b/server/ui/layout.js
iindex dfeb2cb..01d79a0 100644
---- a/server/ui/layout.js
++++ b/server/ui/layout.js
@@@ -55,6 +55,9 @@ main.ore .sc-hero,main.ore .sc-sec,main.ore .sc-cta{padding-left:0;padding-right
.ore .ore-option{display:flex;gap:10px;align-items:flex-start;margin:0;padding:10px 12px;border:1px solid var(--ore-line);border-radius:8px;background:var(--ore-field);color:var(--ore-text);font-size:.95rem;cursor:pointer}
.ore .ore-option:has(input:checked){border-color:var(--ore-accent);box-shadow:inset 0 0 0 1px var(--ore-accent)}.ore .ore-option:has(input:focus-visible){outline:2px solid var(--ore-accent);outline-offset:2px}
.ore .ore-option input{margin:.3em 0 0;accent-color:var(--ore-accent);flex:none}.ore .ore-option strong{display:block}.ore .ore-option .muted{display:block;font-size:.86rem;margin-top:2px}
++.ore .nf-links{list-style:none;margin:22px 0 0;padding:0;display:flex;flex-wrap:wrap;gap:10px}
++.ore .nf-links a{display:inline-block;padding:9px 16px;border-radius:999px;border:1px solid var(--ore-line);background:var(--ore-panel);color:var(--ore-text);text-decoration:none;font-weight:600}
++.ore .nf-links a:hover,.ore .nf-links a:focus-visible{border-color:var(--ore-accent);outline:0}
.ore form:has(input[name=record_to][value=videos]:checked) .ore-live-only{display:none}
@media (max-width:640px){.ore th:nth-child(n+4),.ore td:nth-child(n+4){display:none}}
/* Touch targets on phones (fleet audit, 2026-10-29): touch pointers only. */
ddiff --git a/server/ui/not-found.js b/server/ui/not-found.js
nnew file mode 100644
iindex 0000000..f0de6b6
---- /dev/null
++++ b/server/ui/not-found.js
@@@ -0,0 +1,24 @@
++'use strict';
++/**
++ * The page a browser gets for a path nothing serves: OpenRestream's own Frame, a way back, and no clue
++ * about what was asked for. An API client keeps the problem document (server/app.js).
++ */
++const { renderPage, esc } = require('./layout');
++
++function renderNotFound({ config }) {
++ const host = String(config.baseUrl).replace(/^https?:\/\//, '').replace(/\/.*$/, '');
++ return renderPage({
++ title: 'Page not found',
++ robots: 'noindex,nofollow',
++ config,
++ body: `<h1>Page not found</h1>
++<p>There is no page at this address on ${esc(host)}. The link may be old or mistyped.</p>
++<ul class="nf-links">
++<li><a href="/">OpenRestream home</a></li>
++<li><a href="/streams">Streams</a></li>
++<li><a href="/sessions">Sessions</a></li>
++</ul>`,
++ });
++}
++
++module.exports = { renderNotFound };
ddiff --git a/test/not-found.test.js b/test/not-found.test.js
nnew file mode 100644
iindex 0000000..10a36cb
---- /dev/null
++++ b/test/not-found.test.js
@@@ -0,0 +1,36 @@
++'use strict';
++// A path nothing serves: a browser gets OpenRestream's own 404 page in the Frame, an API client (or a
++// client that does not accept HTML) keeps the problem document.
++const assert = require('assert');
++const { bootApi, request, suite } = require('./helpers');
++
++const t = suite('not-found');
++
++t('a browser that asks for a missing page gets the site 404 page; an API client the problem document', async () => {
++ const api = await bootApi({});
++ try {
++ const host = api.config.baseUrl.replace(/^https?:\/\//, '').replace(/\/.*$/, '');
++ const page = await request(api.base, 'GET', '/no-such-page-xyz', { headers: { Accept: 'text/html,application/xhtml+xml' } });
++ assert.strictEqual(page.status, 404);
++ assert.match(page.headers.get('content-type'), /^text\/html/);
++ assert.ok(page.text.includes('<h1>Page not found</h1>'), page.text.slice(0, 300));
++ assert.ok(page.text.includes(`There is no page at this address on ${host}. The link may be old or mistyped.`));
++ assert.ok(page.text.includes('id="navbar-mount"'), 'the page is in the site Frame');
++ assert.ok(page.text.includes('<meta name="robots" content="noindex,nofollow">'));
++ assert.strictEqual(page.headers.get('x-robots-tag'), 'noindex');
++ assert.ok(page.text.includes('class="nf-links"') && page.text.includes('href="/"'), 'home is the first way back');
++
++ const apiPath = await request(api.base, 'GET', '/api/no-such', { headers: { Accept: 'text/html' } });
++ assert.strictEqual(apiPath.status, 404);
++ assert.match(apiPath.headers.get('content-type'), /problem\+json/);
++ assert.strictEqual(apiPath.body.code, 'openre.not_found');
++
++ const notHtml = await request(api.base, 'GET', '/no-such-page-xyz', { headers: { Accept: 'application/json' } });
++ assert.strictEqual(notHtml.status, 404);
++ assert.match(notHtml.headers.get('content-type'), /problem\+json/);
++ } finally {
++ await api.close();
++ }
++});
++
++t.run();