fix(proxy): preserve seek range requests after inconclusive HEAD (#437)

## Summary

- keep non-zero Range requests when a HEAD response does not advertise
Accept-Ranges
- use slice caching when the upstream answers 206 and preserve
full-response fallback when it answers 200
- add regression coverage for inconclusive HEAD probes and range
fallback behavior

## Validation

- cargo test -p synctv-proxy --test slice_cache_tests (94 passed)
- cargo check --workspace --all-targets
- cargo fmt --check
- verified Alist, Emby, and Bilibili proxy seeks return correct 206
responses and byte ranges
pull/438/head
zijiren 1 month ago committed by GitHub
parent 882cbe09bb
commit b946d397dd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -24,6 +24,7 @@ pub struct CachedResourceMeta {
/// Total size of the resource as reported by upstream.
pub total_size: Option<u64>,
/// Whether upstream has proven byte-range support for this resource.
/// `false` means unknown and must not suppress a later range probe.
pub supports_ranges: bool,
/// Content-Type of the resource.
pub content_type: Option<String>,

@ -425,22 +425,6 @@ async fn proxy_slice_cache(
}
let cached_meta = cache.get_resource_meta(url, provider_headers);
if cached_meta
.as_ref()
.is_some_and(|meta| !meta.supports_ranges)
{
return stream_through_with_status(StreamThroughRequest {
client: cache.client(),
ssrf_guard: cache.ssrf_guard(),
url,
provider_headers,
range_header: None,
cache_status: CacheStatus::Bypass,
request_control,
upstream_header_timeout,
})
.await;
}
let known_total_size = cached_meta.and_then(|meta| meta.total_size);
if let Some(total_size) = known_total_size {
range_bounds_for_total(plan, total_size).map_err(proxy_error_from_client_range_error)?;

@ -1094,10 +1094,10 @@ async fn test_proxy_head_with_cache_uses_head_and_reuses_cached_metadata() {
}
#[tokio::test]
async fn test_head_without_accept_ranges_stores_length_without_range_support() {
async fn test_head_without_accept_ranges_does_not_block_later_range_probe() {
let mock_server = MockServer::start().await;
let total_size: u64 = 4096;
let full_body = Bytes::from(vec![0xE1; 4096]);
let slice_body = Bytes::from((0_u8..=u8::MAX).cycle().take(1024).collect::<Vec<_>>());
Mock::given(method("HEAD"))
.and(path("/head-no-range.bin"))
@ -1112,19 +1112,12 @@ async fn test_head_without_accept_ranges_stores_length_without_range_support() {
Mock::given(method("GET"))
.and(path("/head-no-range.bin"))
.and(header("Range", "bytes=0-1023"))
.respond_with(ResponseTemplate::new(500))
.expect(0)
.mount(&mock_server)
.await;
Mock::given(method("GET"))
.and(path("/head-no-range.bin"))
.and(HeaderAbsent("Range"))
.and(header("Range", "bytes=2048-3071"))
.respond_with(
ResponseTemplate::new(200)
.set_body_bytes(full_body.clone())
.insert_header("Content-Length", total_size.to_string()),
ResponseTemplate::new(206)
.set_body_bytes(slice_body.clone())
.insert_header("Content-Range", format!("bytes 2048-3071/{total_size}"))
.insert_header("Content-Length", "1024"),
)
.expect(1)
.mount(&mock_server)
@ -1155,17 +1148,21 @@ async fn test_head_without_accept_ranges_stores_length_without_range_support() {
"Content-Length alone must not prove range support"
);
let range = proxy_slice(&cache, Some("bytes=0-511"), &url, &provider_headers)
let range = proxy_slice(&cache, Some("bytes=2304-2559"), &url, &provider_headers)
.await
.unwrap();
assert_eq!(range.status(), StatusCode::OK);
assert_eq!(range.status(), StatusCode::PARTIAL_CONTENT);
assert_eq!(
range.headers().get("Content-Range").unwrap(),
"bytes 2304-2559/4096"
);
assert_eq!(
range.headers().get("X-Cache-Status").unwrap(),
CacheStatus::Bypass.as_str()
CacheStatus::Miss.as_str()
);
assert_eq!(
range.into_body().collect().await.unwrap().to_bytes(),
full_body
slice_body.slice(256..512)
);
}
@ -2369,8 +2366,13 @@ async fn test_suffix_range_with_head_length_bypasses_when_origin_ignores_aligned
Mock::given(method("GET"))
.and(path("/suffix-no-ranges.bin"))
.and(header("Range", "bytes=3072-4095"))
.respond_with(ResponseTemplate::new(500))
.expect(0)
.respond_with(
ResponseTemplate::new(200)
.set_body_bytes(full_body.clone())
.insert_header("Content-Length", total_size.to_string())
.insert_header("Content-Type", "application/octet-stream"),
)
.expect(1)
.mount(&mock_server)
.await;
@ -2383,7 +2385,7 @@ async fn test_suffix_range_with_head_length_bypasses_when_origin_ignores_aligned
.insert_header("Content-Length", total_size.to_string())
.insert_header("Content-Type", "application/octet-stream"),
)
.expect(1)
.expect(0)
.mount(&mock_server)
.await;

Loading…
Cancel
Save