Skip to content

[Bug]: Deezer preview proxy advertises Accept-Ranges but ignores Range — silent playback failure on iOS/Safari #227

Description

@sbrady116-png

Bug Description

The Deezer preview proxy added for #173 forwards Deezer's Accept-Ranges: bytes header to the client but never reads req.headers.range. Every request is answered 200 with the full body, even when the client asked for a byte range.

WebKit — Safari on macOS and all iOS browsers — requires 206 Partial Content in response to its Range request for <audio>. Receiving 200 instead, it abandons playback silently: no console error, no failed request, no visible state change. Chrome and Firefox tolerate the same response and play normally.

Net effect: artist previews are completely non-functional on iPhone/iPad and work fine on desktop Chrome, which makes it read as a device problem rather than a server one.

This looks like a regression from the #173 fix — the same-origin proxy pipes the body correctly but does not implement the range semantics it advertises.

Affected code: backend/src/routes/artists.ts, the /preview/:artistName/:trackTitle/stream handler. In the compiled dist/routes/artists.js shipped in the image, the only two lines in the whole file that mention ranges are these:

if (upstream.headers["accept-ranges"]) {
    res.setHeader("Accept-Ranges", upstream.headers["accept-ranges"]);
}

There is no req.headers.range read, no 206, and no Content-Range anywhere in the file.

Measurements. Deezer's CDN behaves correctly and supports ranges — only this proxy hop does not. Run from inside the container against a fresh preview URL:

request Deezer CDN kima v1.9.0 proxy
no Range 200, audio/mpeg, 479,827 bytes 200, full body
Range: bytes=0-1 206, content-range: bytes 0-1/479827 200, full body
Range: bytes=0-1023 206, content-range: bytes 0-1023/479827 200, full body

bytes=0-1 is the probe WebKit opens media with, so it is the first request that fails.

Suggested fix. Forward the client's Range upstream and relay the partial response — Deezer already does the hard part:

const clientRange = req.headers.range;
const upstream = await axios.get(previewUrl, {
    responseType: "stream",
    timeout: 10000,
    headers: clientRange ? { Range: clientRange } : undefined,
});

res.setHeader("Content-Type", upstream.headers["content-type"] || "audio/mpeg");
if (upstream.headers["content-length"]) {
    res.setHeader("Content-Length", upstream.headers["content-length"]);
}
if (upstream.headers["accept-ranges"]) {
    res.setHeader("Accept-Ranges", upstream.headers["accept-ranges"]);
}
// Only claim 206 when upstream actually served a partial body — if it ignored
// the Range, fall through as 200 rather than lying in the other direction.
if (upstream.status === 206 && upstream.headers["content-range"]) {
    res.setHeader("Content-Range", upstream.headers["content-range"]);
    res.status(206);
}

Verified with this change in place, against the live CDN:

no Range            -> 200 | content-range: (none)              | 479827 bytes
Range: bytes=0-1    -> 206 | content-range: bytes 0-1/479827    |      2 bytes
Range: bytes=0-1023 -> 206 | content-range: bytes 0-1023/479827 |   1024 bytes

axios treats 206 as success under its default validateStatus, so no other change is needed. I am running this patch locally and previews now play on iOS/Safari.

Happy to open a PR if useful.

Steps to Reproduce

  1. Open Kima on an iPhone or iPad (any browser — all are WebKit), or Safari on macOS.
  2. Go to an artist's page and tap a Similar Artist to open an artist not in the library.
  3. In the Popular list, tap a track to play its Deezer preview.

Expected Behavior

The 30-second Deezer preview plays.

Actual Behavior

Nothing plays. No error is surfaced anywhere — no toast, no console error, no failed network request. The same track previews correctly in Chrome on desktop.

Kima Version

v1.9.0

Deployment Method

Docker (standalone)

Environment Details

  • Image: chevron7locked/kima:v1.9.0
  • Client: iOS 18 / Safari (reproduced); also affects Safari on macOS
  • Working for comparison: Chrome on desktop
  • Behind a reverse proxy, TLS terminated upstream; no auth layer in front of Kima

Additional Notes

Worth auditing any other route that proxies media the same way — the underlying mistake is "forward an upstream capability header without implementing the capability", and it is invisible on Chromium.

Same silent-failure shape as #173, just a different browser engine: strict clients reject, permissive clients mask the problem.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions