revert the /stream etag, cache-control and 304

playback did not come back when x-audio-duration did, so the duration header
was not the only thing i broke. the range and whole-file branches are
byte-identical to the version that worked, which leaves these three as the
only remaining difference on /stream.

a cache that keeps a validator after evicting the body sends if-none-match,
gets a 304 and has no audio. plausible, unproven, and not worth another round
trip through someone's phone to find out — the etag was an optimisation and
playback is not.

/stream is now byte-identical to the last version known to play, verified by
running both implementations side by side over no-range, closed, open and
invalid ranges and diffing status, every header and body length.

if-none-match is still accepted and ignored. re-introducing this needs a
capture of what the player actually sends.
This commit is contained in:
2026-08-19 15:43:11 +00:00
parent b430a702b4
commit 1170e411ac
2 changed files with 30 additions and 25 deletions
+20 -20
View File
@@ -118,11 +118,29 @@ function resolveWithinRoot(relPath: string): string | null {
return abs;
}
/** Serve an audio file with byte-range support and an mtime+size ETag. */
/**
* Serve an audio file with byte-range support and an `X-Audio-Duration` header.
*
* ── No ETag, no Cache-Control, no 304 — and that is deliberate ──
*
* All three were added on 2026-08-19 to spare a re-download on replay and seek: /stream was the only
* route with no validator, so every seek re-read the file over the network. The reasoning still looks
* right and the measurement was real.
*
* The mobile player stopped playing. Restoring `X-Audio-Duration` did not fix it, and the range and
* whole-file branches are byte-identical to the version that worked, which left these headers as the
* only remaining difference. A cache that keeps a validator after evicting the body would send
* `If-None-Match`, get a 304, and have no audio — plausible, and not something to keep testing on
* someone's phone.
*
* So /stream is back to exactly what worked. `ifNoneMatch` is still accepted and deliberately unused:
* the caller passes it, and dropping the parameter would only have to be re-added. Re-introducing this
* needs a capture of what the player actually sends, not another guess.
*/
export async function streamAudioFile(
relPath: string,
rangeHeader: string | null,
ifNoneMatch: string | null = null,
_ifNoneMatch: string | null = null,
): Promise<Response> {
const absPath = resolveWithinRoot(relPath);
if (!absPath) return new Response('Invalid path', { status: 400 });
@@ -140,32 +158,14 @@ export async function streamAudioFile(
const contentType = CONTENT_TYPES[ext] ?? 'application/octet-stream';
const file = Bun.file(absPath);
// An audio file's bytes do not change; a re-encode replaces the file, which moves mtime and size. So
// the pair identifies the content, and a client that has played this track once can seek in it without
// fetching anything again.
//
// This was the ONLY route with no validator at all — cover, meta, lyrics and poster have had one all
// along — so every seek and every replay re-read the file over the network.
const etag = `"${s.mtimeMs.toString(36)}-${total.toString(36)}"`;
const duration = (await durationFromIndex(relPath)) ?? (await probeDuration(absPath, s.mtimeMs));
const baseHeaders: Record<string, string> = {
'Content-Type': contentType,
'Accept-Ranges': 'bytes',
...(duration ? { 'X-Audio-Duration': String(duration) } : {}),
ETag: etag,
// `no-cache` means "you may keep it, but ask first" — a conditional request rather than a blind
// refetch. `private` because a member's library is not a proxy's to hold.
'Cache-Control': 'private, no-cache',
};
// Whole-file revalidation. Deliberately NOT applied to a Range request: a 304 there would answer a
// question about part of a file with a statement about all of it, and players handle that badly.
if (!rangeHeader && ifNoneMatch === etag) {
return new Response(null, { status: 304, headers: baseHeaders });
}
if (rangeHeader) {
const m = rangeHeader.match(/bytes=(\d*)-(\d*)/);
if (m) {