From 1c619d119aa2b7ec01290abaa0e09777b30f7777 Mon Sep 17 00:00:00 2001 From: Michele Carenza Date: Wed, 11 Mar 2026 14:33:22 +0100 Subject: [PATCH 1/4] fix: clamp range end to file size instead of returning 416 RFC 7233 Section 2.1 states that if the last-byte-pos is greater than or equal to the current length of the representation, the byte range should be interpreted as the remainder of the representation, not rejected as unsatisfiable. `bytes=0-999` on a 500-byte file now returns `bytes 0-499/500` (206) instead of 416. --- src/headers/tests.rs | 20 +++++++++++++++++--- src/lib.rs | 4 +++- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/src/headers/tests.rs b/src/headers/tests.rs index 11ac67c..451c62c 100644 --- a/src/headers/tests.rs +++ b/src/headers/tests.rs @@ -428,15 +428,29 @@ mod file_range { } #[test] - fn range_end_at_size() { + fn range_end_at_size_is_clamped() { let range = HttpRange::Range(OrderedRange::new(0..=10).unwrap()); + let result = file_range(size(10), Some(range)).unwrap(); + assert_eq!(result.range(), &(0..=9)); + } + + #[test] + fn range_beyond_size_is_clamped() { + let range = HttpRange::Range(OrderedRange::new(0..=50).unwrap()); + let result = file_range(size(10), Some(range)).unwrap(); + assert_eq!(result.range(), &(0..=9)); + } + + #[test] + fn range_start_at_size_is_unsatisfiable() { + let range = HttpRange::Range(OrderedRange::new(10..=20).unwrap()); let result = file_range(size(10), Some(range)); assert!(result.is_err()); } #[test] - fn range_beyond_size() { - let range = HttpRange::Range(OrderedRange::new(0..=50).unwrap()); + fn range_start_beyond_size_is_unsatisfiable() { + let range = HttpRange::Range(OrderedRange::new(50..=100).unwrap()); let result = file_range(size(10), Some(range)); assert!(result.is_err()); } diff --git a/src/lib.rs b/src/lib.rs index 8a84a5f..7d1040e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -55,7 +55,9 @@ pub fn file_range( let range = match http_range { HttpRange::StartingPoint(start) if start < size => start..=size - 1, - HttpRange::Range(range) if range.end() < size => range.start()..=range.end(), + HttpRange::Range(range) if range.start() < size => { + range.start()..=range.end().min(size - 1) + } HttpRange::Suffix(suffix) if suffix > 0 && suffix <= size => size - suffix..=size - 1, _ => { let content_range = HttpContentRange::Unsatisfiable(Unsatisfiable::new(size)); From 624e2b44d08c4778ebd0ad7edaef15f15a62d09d Mon Sep 17 00:00:00 2001 From: Michele Carenza Date: Wed, 11 Mar 2026 14:44:06 +0100 Subject: [PATCH 2/4] fix: clamp suffix ranges exceeding file size instead of returning 416 RFC 7233 Section 2.1 states that if the selected representation is shorter than the specified suffix-length, the entire representation is used. `bytes=-999` on a 500-byte file now returns `bytes 0-499/500` (206) instead of 416. --- src/headers/tests.rs | 6 +++--- src/lib.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/headers/tests.rs b/src/headers/tests.rs index 451c62c..f7aee49 100644 --- a/src/headers/tests.rs +++ b/src/headers/tests.rs @@ -480,9 +480,9 @@ mod file_range { } #[test] - fn suffix_exceeds_size() { - let result = file_range(size(10), Some(HttpRange::Suffix(11))); - assert!(result.is_err()); + fn suffix_exceeds_size_is_clamped() { + let result = file_range(size(10), Some(HttpRange::Suffix(11))).unwrap(); + assert_eq!(result.range(), &(0..=9)); } #[test] diff --git a/src/lib.rs b/src/lib.rs index 7d1040e..7c3b081 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -58,7 +58,7 @@ pub fn file_range( HttpRange::Range(range) if range.start() < size => { range.start()..=range.end().min(size - 1) } - HttpRange::Suffix(suffix) if suffix > 0 && suffix <= size => size - suffix..=size - 1, + HttpRange::Suffix(suffix) if suffix > 0 => size.saturating_sub(suffix)..=size - 1, _ => { let content_range = HttpContentRange::Unsatisfiable(Unsatisfiable::new(size)); return Err(UnsatisfiableRange(content_range)); From cf6a7d159eb6ba5280d2b9d55ddd89c6572485fb Mon Sep 17 00:00:00 2001 From: Michele Carenza Date: Wed, 11 Mar 2026 14:59:21 +0100 Subject: [PATCH 3/4] fix: correct `matches_requested_range` for suffix ranges With suffix clamping, a non-zero suffix is always satisfiable regardless of file size. The only unsatisfiable suffix is `Suffix(0)`. Previously `Suffix(0)` against `Unsatisfiable` incorrectly returned `false` because `0 > size` is never true. --- src/headers/content_range.rs | 7 +++---- src/headers/tests.rs | 12 ++++++++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/headers/content_range.rs b/src/headers/content_range.rs index 02e7b31..7e00991 100644 --- a/src/headers/content_range.rs +++ b/src/headers/content_range.rs @@ -44,10 +44,9 @@ impl HttpContentRange { HttpRange::Range(OrderedRange { end: n, .. }), HttpContentRange::Unsatisfiable(Unsatisfiable { size }), ) => n >= *size, - ( - HttpRange::Suffix(suffix), - HttpContentRange::Unsatisfiable(Unsatisfiable { size }), - ) => suffix > *size, + (HttpRange::Suffix(suffix), HttpContentRange::Unsatisfiable(Unsatisfiable { .. })) => { + suffix == 0 + } } } } diff --git a/src/headers/tests.rs b/src/headers/tests.rs index f7aee49..2ba696e 100644 --- a/src/headers/tests.rs +++ b/src/headers/tests.rs @@ -146,12 +146,20 @@ mod content_range { } #[test] - fn unsuccessful_range_suffix_range_content_bound() { - let range = HttpRange::Suffix(50); + fn unsuccessful_range_suffix_zero_content_unsatisfiable() { + let range = HttpRange::Suffix(0); let content_range = HttpContentRange::Unsatisfiable(Unsatisfiable::new(20)); assert!(content_range.matches_requested_range(range)); } + + #[test] + fn suffix_exceeding_size_is_not_unsatisfiable() { + let range = HttpRange::Suffix(50); + let content_range = HttpContentRange::Unsatisfiable(Unsatisfiable::new(20)); + + assert!(!content_range.matches_requested_range(range)); + } } } From ceeddfd79034778cd4b5820924e5cba3bb657f54 Mon Sep 17 00:00:00 2001 From: Michele Carenza Date: Wed, 11 Mar 2026 15:38:32 +0100 Subject: [PATCH 4/4] fix: update `matches_requested_range` to handle clamped ranges After clamping range ends and suffixes to file size, the response range can be shorter than what was originally requested. Relax the equality checks to accept clamped responses: - `Range`: accept response end <= requested end - `Suffix`: accept response length <= requested suffix Add tests for suffix edge cases: response not ending at file boundary, and response length exceeding the requested suffix. --- src/headers/content_range.rs | 5 +++-- src/headers/tests.rs | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/headers/content_range.rs b/src/headers/content_range.rs index 7e00991..6f852c4 100644 --- a/src/headers/content_range.rs +++ b/src/headers/content_range.rs @@ -30,9 +30,10 @@ impl HttpContentRange { ( HttpRange::Range(OrderedRange { start, end }), HttpContentRange::Bound(Bound { range, .. }), - ) => start == range.start() && end == range.end(), + ) => start == range.start() && end >= range.end(), (HttpRange::Suffix(suffix), HttpContentRange::Bound(Bound { range, size })) => { - let length_matches = (range.end() - range.start()).checked_add(1) == Some(suffix); + let length = (range.end() - range.start()).checked_add(1); + let length_matches = length.is_some_and(|len| len <= suffix); let ends_at_boundary = size.is_none_or(|size| range.end() + 1 == size); length_matches && ends_at_boundary } diff --git a/src/headers/tests.rs b/src/headers/tests.rs index 2ba696e..1778a2e 100644 --- a/src/headers/tests.rs +++ b/src/headers/tests.rs @@ -160,6 +160,38 @@ mod content_range { assert!(!content_range.matches_requested_range(range)); } + + #[test] + fn range_with_clamped_end_matches() { + let range = HttpRange::Range(OrderedRange::new(0..=999).unwrap()); + let content_range = HttpContentRange::Bound(Bound::new(0..=49, Some(50)).unwrap()); + + assert!(content_range.matches_requested_range(range)); + } + + #[test] + fn suffix_clamped_to_file_size_matches() { + let range = HttpRange::Suffix(999); + let content_range = HttpContentRange::Bound(Bound::new(0..=49, Some(50)).unwrap()); + + assert!(content_range.matches_requested_range(range)); + } + + #[test] + fn suffix_not_at_boundary_does_not_match() { + let range = HttpRange::Suffix(5); + let content_range = HttpContentRange::Bound(Bound::new(0..=4, Some(20)).unwrap()); + + assert!(!content_range.matches_requested_range(range)); + } + + #[test] + fn suffix_length_mismatch_does_not_match() { + let range = HttpRange::Suffix(3); + let content_range = HttpContentRange::Bound(Bound::new(10..=19, Some(20)).unwrap()); + + assert!(!content_range.matches_requested_range(range)); + } } }