feat(html-view): answer Range requests through the shared sendFileBody()

A rendered page's own <video> or <audio> could not seek, because the
route streamed a plain 200 without Accept-Ranges. sendFileBody() and
localFileSource() are now exported from file-routes.ts and the serving
route uses them, so it advertises Accept-Ranges and answers a Range with
206 + Content-Range (or 416), exactly like file-raw, the attachment raw
route and /api/download. The body sender hijacks the reply and copies
every header set before it (the sandbox CSP, CORS, no-store), which the
new test pins on a 206.

Media types stay in ASSET_TYPES: with Range support they work, and they
are served under the same sandbox CSP as everything else.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-13 17:11:50 +02:00
1 parent bc940a3953
commit fa23f8be2a
5 files changed
+37 -8

No files matched your search

+22
View File
@@ -94,6 +94,28 @@ describe('html-view routes', () => {
expect(res.body).toBe('<h1>hi</h1>');
});
it('answers Range requests, so a page can seek its own media', async () => {
writeFileSync(join(workspace, 'clip.mp4'), '0123456789');
const base = (await openUrl()).replace(/index\.html$/, '');
const full = await harness.app.inject({ method: 'GET', url: `${base}clip.mp4` });
expect(full.statusCode).toBe(200);
expect(full.headers['accept-ranges']).toBe('bytes');
expect(full.headers['content-length']).toBe('10');
const part = await harness.app.inject({ method: 'GET', url: `${base}clip.mp4`, headers: { range: 'bytes=2-5' } });
expect(part.statusCode).toBe(206);
expect(part.headers['content-range']).toBe('bytes 2-5/10');
expect(part.body).toBe('2345');
// Every header the route sets survives the hijacked body sender.
expect(part.headers['content-type']).toBe('video/mp4');
expect(String(part.headers['content-security-policy'])).toMatch(/^sandbox /);
expect(part.headers['access-control-allow-origin']).toBe('*');
expect(part.headers['cache-control']).toBe('no-store');
const beyond = await harness.app.inject({ method: 'GET', url: `${base}clip.mp4`, headers: { range: 'bytes=50-' } });
expect(beyond.statusCode).toBe(416);
});
it('serves sibling assets, including subdirectories', async () => {
const base = (await openUrl()).replace(/index\.html$/, '');
expect((await harness.app.inject({ method: 'GET', url: `${base}assets/app.css` })).statusCode).toBe(200);