[Bug Report]Android: the local resource server ignores the end of a Range request, and its own headers say otherwise

On Android, a Range request for a file served through app.vault.adapter.getResourcePath() returns the bytes from the requested start through to the end of the file, no matter what end the request asked for. The response nevertheless carries 206 Partial Content, a Content-Range naming the requested range, and a Content-Length naming the requested length — all three of which disagree with the body that is actually delivered.

The desktop app is correct. This is Android-only.

Steps to reproduce

  1. Open a vault on Android that contains any file larger than about 2 KB (an image attachment is fine).
  2. Run the following against that file — from a plugin, or over the WebView inspector:
const file = app.vault.getFiles().find((f) => f.stat.size > 2000);
const url = app.vault.adapter.getResourcePath(file.path);

const response = await fetch(url, { headers: { Range: 'bytes=0-9' } });
const body = await response.arrayBuffer();

console.log({
  file: file.path,
  fileSize: file.stat.size,
  status: response.status,
  contentRange: response.headers.get('Content-Range'),
  contentLength: response.headers.get('Content-Length'),
  requestedBytes: 10,
  actualBodyBytes: body.byteLength,
});

Expected result

The body is 10 bytes — the length the request asked for, and the length both Content-Range and Content-Length state.

Actual result

The body is the entire file. Measured against a 632,699-byte JPEG in the vault:

{
  file: "photos/causeway-5.jpg",
  fileSize: 632699,
  status: 206,
  contentRange: "bytes 0-9/632699",
  contentLength: "10",
  requestedBytes: 10,
  actualBodyBytes: 632699      // <- the whole file
}

The start of the range is honoured. A request further into the file returns from that offset to EOF:

Request Content-Range Content-Length Actual body First body byte
bytes=0-9 bytes 0-9/632699 10 632,699 file byte 0
bytes=1000-1009 bytes 1000-1009/632699 10 631,699 file byte 1000 ✓
bytes=4000- bytes 4000-4421/4422 422 ✓ file byte 4000 ✓

The third row is correct only because the requested end is the end of the file.

So the server seeks to the start offset and then streams to EOF, while advertising a range and a length it does not deliver.

Desktop, for contrast

The same three requests against the same file through app:// on the desktop return exactly the requested slices:

Request Status Content-Range Actual body
bytes=200-299 200 (absent) 100 ✓
bytes=0-126 200 (absent) 127 ✓
bytes=4000- 200 (absent) 422 ✓

Desktop answers 200 without a Content-Range header, which is unusual for a range request but at least does not misreport what it sent. Android reports 206 with headers that contradict the body.

Why it matters

Any partial read of a local file over-reads to the end of that file:

  • Reading a file header. Sniffing EXIF, or a container’s magic bytes, is a few hundred bytes of intent and a whole-file transfer in practice. On a 40 MB video that is 40 MB into renderer memory to read 12 bytes.
  • Range-addressed single-file formats. PMTiles, and anything else that stores an index and seeks into one archive, become unusable at real sizes: fetching one 1 KB tile out of a 500 MB archive transfers the rest of the archive.
  • Anything that trusts the headers. Content-Length: 10 followed by 632,699 bytes is a malformed response. Code that pre-allocates from Content-Length, or that streams and stops at it, will be wrong rather than merely slow.

Media seeking is likely affected by the same path, since <video> and <audio> seek with range requests — I did not test that, so I am not claiming it.

Environment

  • Obsidian: 1.13.7 (versionCode 364)
  • Platform: Android 16, Android Emulator (sdk_gphone64_x86_64), x86_64
  • WebView: Chrome 133.0.6943.137
  • Resource URL form: http://localhost/_capacitor_file_/storage/emulated/0/Documents/<vault>/<path>

Reproduced repeatedly across app restarts, on files inside the vault and on files outside it.

Suggested fix

Honour the end of the range and truncate the body to it, so the delivered bytes match the Content-Range and Content-Length already being sent. Failing that, do not advertise 206 with a Content-Range/Content-Length the response does not satisfy — a plain 200 with the whole file, as the desktop does for its part, at least lets a caller detect that the range was refused.

I think this is more of a Capacitor bug than an Obsidian one, exact issue: [Bug]: HTTP Range Responses not within spec · Issue #8371 · ionic-team/capacitor · GitHub.

Thanks — that’s it exactly, right down to PMTiles being what tripped it.

One thing I’d flag: that issue has been open and untouched since March, and its 2023 predecessor (#7151) was closed without a fix. The last attempt to fix it was merged and then reverted. So “upstream has it” isn’t really a timeline.

Two things stay on Obsidian’s side either way: picking up the fix once it lands, and — independent of Capacitor — not sending 206 with a Content-Range and Content-Length the body doesn’t satisfy. Desktop already answers a plain 200 with the whole file. Doing that on Android would at least leave a caller able to tell that the range was refused, instead of handing it a malformed response.

I’ll take the measurements over to the Capacitor issue.