fix(download): stop an empty download completing and then hanging the player
Two halves of one failure, either of which is enough to produce an
offline item that never starts.
The worker marked a transfer `completed` without checking it produced
any bytes, so a server that answered 200 with no body — an error page, a
transcode that yielded nothing — renamed a zero-byte `.part` into place
and published it as available offline. That is worse than failing: the
retry budget never applies and the UI shows the item as ready.
The media server then answered a request for that file with a span of
`{ start: 0, end: 0 }`. `end` is inclusive, so `Span::len()` reported
**one** byte: the response declared `Content-Length: 1` and streamed
nothing, which Chromium's media loader waits on forever. The user sees a
downloaded item that just never plays, with nothing explaining why.
A zero-length file has no satisfiable range, so `span_for` now returns
`None` and the server answers 416. An empty transfer is rejected as a
network error, which keeps the `.part` for a resume and lets the existing
retry budget do its job.
This commit is contained in:
@@ -326,8 +326,12 @@ impl Span {
|
||||
///
|
||||
/// TRACES: UR-071 | DR-137 | UT-127
|
||||
pub fn span_for(range: Option<&str>, len: u64) -> Option<Span> {
|
||||
// A zero-length file has no byte to serve. `end` is inclusive, so the
|
||||
// shortest span this type can express is one byte — returning one for an
|
||||
// empty file declared `Content-Length: 1` and then streamed nothing, which
|
||||
// Chromium's media loader waits on forever. 416 says so honestly instead.
|
||||
if len == 0 {
|
||||
return Some(Span { start: 0, end: 0 });
|
||||
return None;
|
||||
}
|
||||
let last = len - 1;
|
||||
let first_chunk = Span {
|
||||
@@ -442,6 +446,42 @@ fn content_type(path: &Path, head: &[u8]) -> &'static str {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// An empty file must not be answered with a span that promises a byte.
|
||||
///
|
||||
/// `Span::len()` is `end + 1 - start`, so the `Span { start: 0, end: 0 }`
|
||||
/// that a zero-length file used to produce reported a length of **one**.
|
||||
/// The response then declared `Content-Length: 1` and streamed nothing,
|
||||
/// which Chromium's media loader waits on forever — reaching the user as a
|
||||
/// downloaded item that never starts. A zero-byte file has no satisfiable
|
||||
/// range, so 416 is the honest answer.
|
||||
///
|
||||
/// TRACES: UR-071 | DR-137 | UT-127
|
||||
#[test]
|
||||
fn test_span_for_an_empty_file_is_unsatisfiable() {
|
||||
assert!(
|
||||
span_for(None, 0).is_none(),
|
||||
"a zero-length file has no byte to serve"
|
||||
);
|
||||
assert!(span_for(Some("bytes=0-"), 0).is_none());
|
||||
assert!(span_for(Some("bytes=0-100"), 0).is_none());
|
||||
}
|
||||
|
||||
/// Whatever a span says, its length must match the bytes that follow it.
|
||||
///
|
||||
/// TRACES: UR-071 | DR-137 | UT-127
|
||||
#[test]
|
||||
fn test_span_len_never_exceeds_the_file() {
|
||||
for len in [0u64, 1, 2, 4095, CHUNK_LEN, CHUNK_LEN + 1] {
|
||||
if let Some(span) = span_for(None, len) {
|
||||
assert!(
|
||||
span.len() <= len,
|
||||
"span for a {len}-byte file claims {} bytes",
|
||||
span.len()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The whole point: a request with no `Range` must still come back bounded.
|
||||
/// That is the case Tauri's asset protocol answers with the entire file —
|
||||
/// the read Chromium abandoned after 31s.
|
||||
|
||||
Reference in New Issue
Block a user