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
- Open a vault on Android that contains any file larger than about 2 KB (an image attachment is fine).
- 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: 10followed by 632,699 bytes is a malformed response. Code that pre-allocates fromContent-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.