feat: support for `Range` requests out of bounds (#306)
* feat: support for `Range` requests out of bounds
SWS will make sure to return only what's available in that case which
seems to be a very common behavior across web servers.
Previously exceeding the length of a file returning `416 Requested
Range Not Satisfiable`. Now it will return what's available.
```sh
$ curl -IH "Range: bytes=50-9000" http://localhost/index.html
\# HTTP/1.1 206 Partial Content
\# Server: nginx/1.25.3
\# Date: Sun, 28 Jan 2024 22:09:20 GMT
\# Content-Type: text/html
\# Content-Length: 486
\# Last-Modified: Mon, 02 Oct 2023 04:49:01 GMT
\# Connection: keep-alive
\# ETag: "651a4bbd-218"
\# Content-Range: bytes 50-535/536
```
it resolves #295 and relates to https://github.com/orgs/static-web-server/discussions/145
Diff
src/static_files.rs | 43 +++++++++++++++++++++++++-------
tests/static_files.rs | 70 +++++++++++++++++++++++++++++++++++++++-------------
2 files changed, 87 insertions(+), 26 deletions(-)
@@ -672,35 +672,60 @@ fn bytes_range(range: Option<Range>, max_len: u64) -> Result<(u64, u64), BadRang
return Ok((0, max_len));
};
let res = range
let resp = range
.iter()
.map(|(start, end)| {
tracing::trace!("range request received, {:?}-{:?}-{}", start, end, max_len);
let (start, end) = match (start, end) {
(Bound::Unbounded, Bound::Unbounded) => (0, max_len),
(Bound::Included(a), Bound::Included(b)) => {
if a > b {
return Err(BadRange);
}
(a, if b == max_len { b } else { b + 1 })
}
(Bound::Included(a), Bound::Unbounded) => (a, max_len),
(Bound::Unbounded, Bound::Included(b)) => {
if b > max_len {
return Err(BadRange);
tracing::trace!("unsatisfiable byte range: -{}/{}", b, max_len);
tracing::trace!("returning only what's available: 0-{}", max_len);
(0, max_len)
} else {
(max_len - b, max_len)
}
(max_len - b, max_len)
}
_ => unreachable!(),
};
if start < end && end <= max_len {
Ok((start, end))
} else {
tracing::trace!("unsatisfiable byte range: {}-{}/{}", start, end, max_len);
Err(BadRange)
tracing::trace!("range request to return: {}-{}/{}", start, end, max_len);
return Ok((start, end));
}
tracing::trace!("unsatisfiable byte range: {}-{}/{}", start, end, max_len);
if start < end && start <= max_len {
tracing::trace!(
"returning only what's available: {}-{}/{}",
start,
max_len,
max_len
);
return Ok((start, max_len));
}
Err(BadRange)
})
.next()
.unwrap_or(Ok((0, max_len)));
res
.unwrap_or(Err(BadRange));
resp
}
#[cfg(test)]
@@ -830,16 +830,16 @@ mod tests {
.await
{
Ok((mut res, _)) => {
assert_eq!(res.status(), 416);
assert_eq!(res.status(), 206);
assert_eq!(
res.headers()["content-range"],
format!("bytes */{}", buf.len())
format!("bytes 100-{}/{}", buf.len() - 1, buf.len())
);
assert_eq!(res.headers().get("content-length"), None);
assert!(res.headers().get("content-length").is_some());
let body = hyper::body::to_bytes(res.body_mut())
.await
.expect("unexpected bytes error during `body` conversion");
assert_eq!(body, "");
assert!(body.len() > 400);
}
Err(_) => {
panic!("expected a normal response rather than a status error")
@@ -1038,17 +1038,14 @@ mod tests {
}
#[tokio::test]
async fn handle_byte_ranges_bad_2() {
async fn handle_byte_ranges_bad_non_numeric() {
let mut headers = HeaderMap::new();
headers.insert("range", "bytes=xyx-abc".parse().unwrap());
let buf = fs::read(root_dir().join("index.html"))
.expect("unexpected error during index.html reading");
let buf = Bytes::from(buf);
let mut headers = HeaderMap::new();
headers.insert(
"range",
format!("bytes=-{}", buf.len() + 1).parse().unwrap(),
);
for method in [Method::HEAD, Method::GET] {
match static_files::handle(&HandleOpts {
method: &method,
@@ -1075,11 +1072,11 @@ mod tests {
res.headers()["content-range"],
format!("bytes */{}", buf.len())
);
assert_eq!(res.headers().get("content-length"), None);
assert!(res.headers().get("content-length").is_none());
let body = hyper::body::to_bytes(res.body_mut())
.await
.expect("unexpected bytes error during `body` conversion");
assert_eq!(body, "");
assert!(body.is_empty());
}
Err(_) => {
panic!("expected a normal response rather than a status error")
@@ -1089,14 +1086,16 @@ mod tests {
}
#[tokio::test]
async fn handle_byte_ranges_bad_3() {
async fn handle_byte_ranges_bad_2() {
let buf = fs::read(root_dir().join("index.html"))
.expect("unexpected error during index.html reading");
let buf = Bytes::from(buf);
let mut headers = HeaderMap::new();
headers.insert("range", "bytes=".parse().unwrap());
headers.insert(
"range",
format!("bytes=-{}", buf.len() + 1).parse().unwrap(),
);
for method in [Method::HEAD, Method::GET] {
match static_files::handle(&HandleOpts {
@@ -1120,10 +1119,47 @@ mod tests {
{
Ok((mut res, _)) => {
assert_eq!(res.status(), 200);
assert!(res.headers().get("content-length").is_some());
let body = hyper::body::to_bytes(res.body_mut())
.await
.expect("unexpected bytes error during `body` conversion");
assert_eq!(body, buf);
assert!(body.len() > 500);
}
Err(_) => {
panic!("expected a normal response rather than a status error")
}
}
}
}
#[tokio::test]
async fn handle_byte_ranges_bad_3() {
let mut headers = HeaderMap::new();
headers.insert("range", "bytes=".parse().unwrap());
for method in [Method::HEAD, Method::GET] {
match static_files::handle(&HandleOpts {
method: &method,
headers: &headers,
base_path: &root_dir(),
uri_path: "index.html",
uri_query: None,
#[cfg(feature = "directory-listing")]
dir_listing: false,
#[cfg(feature = "directory-listing")]
dir_listing_order: 6,
#[cfg(feature = "directory-listing")]
dir_listing_format: &DirListFmt::Html,
redirect_trailing_slash: true,
compression_static: false,
ignore_hidden_files: false,
index_files: &[],
})
.await
{
Ok((res, _)) => {
assert_eq!(res.status(), 416);
}
Err(_) => {
panic!("expected a normal response rather than a status error")