Repository: static-web-server/static-web-server (language: rust)

Issue:
Range requests do not allow overshoot (but is commonly accepted)
### Search for duplicate feature request

- [X] I already searched, and this feature request or improvement is not a duplicate.

### Feature scope

Improve existing functionality

### Feature request related to a problem

Most servers, fx. nginx, S3, CloudFront, will accept HTTP range requests that are outside the length of the actual content.

While that can perhaps be regarded as bad/unfortunate it is more or less defacto behaviour and as such perhaps it would be best if also sws behave that way.

Because sws does not currently accept these requests I have seen existing solutions that cannot be hosted by sws due to their dependency on servers gracefully handling this.

### Describe the solution you'd like

Allow range request that are out of bounds.

### Describe alternatives you've considered

Make sure implementation(s) are not attempting to do range requests that are out of bounds.

### Build target

All targets

### Additional context

_No response_
Range requests do not allow overshoot (but is commonly accepted)
### Search for duplicate feature request

- [X] I already searched, and this feature request or improvement is not a duplicate.

### Feature scope

Improve existing functionality

### Feature request related to a problem

Most servers, fx. nginx, S3, CloudFront, will accept HTTP range requests that are outside the length of the actual content.

While that can perhaps be regarded as bad/unfortunate it is more or less defacto behaviour and as such perhaps it would be best if also sws behave that way.

Because sws does not currently accept these requests I have seen existing solutions that cannot be hosted by sws due to their dependency on servers gracefully handling this.

### Describe the solution you'd like

Allow range request that are out of bounds.

### Describe alternatives you've considered

Make sure implementation(s) are not attempting to do range requests that are out of bounds.

### Build target

All targets

### Additional context

_No response_


Discussion / hints:
Just say that I am not opposed to the enhancement.
However, help me to understand it a bit more.
- Can you please share an use case or example(s) when a range request out of bounds could be useful?
- Can you also share a client request example(s) and the expected response(s) using curl?
I'll see if I can provide some requests examples at some point but to give example of a use case is when you want to use http range requests to stream content that consist of multiple parts with unknown size. If you want to do that with minimal amount of roundtrips you would fetch up to a buffer size that might overshoot the size of the content. Current popular web servers and CDN distributions allows this and returns what is available. SWS responds with a HTTP 416 error.
I understand. It can be useful then.
Basically, the idea is to allow range requests out of bounds but make sure to return what is available in this case.
Just say that I am not opposed to the enhancement.
However, help me to understand it a bit more.
- Can you please share an use case or example(s) when a range request out of bounds could be useful?
- Can you also share a client request example(s) and the expected response(s) using curl?
I'll see if I can provide some requests examples at some point but to give example of a use case is when you want to use http range requests to stream content that consist of multiple parts with unknown size. If you want to do that with minimal amount of roundtrips you would fetch up to a buffer size that might overshoot the size of the content. Current popular web servers and CDN distributions allows this and returns what is available. SWS responds with a HTTP 416 error.
I understand. It can be useful then.
Basically, the idea is to allow range requests out of bounds but make sure to return what is available in this case.

Fix patch:
diff --git a/src/static_files.rs b/src/static_files.rs
index 6165696a..82423a79 100644
--- a/src/static_files.rs
+++ b/src/static_files.rs
@@ -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)) => {
+                    // `start` can not be greater than `end`
+                    if a > b {
+                        return Err(BadRange);
+                    }
                     // For the special case where b == the file size
                     (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);
+                        // `Range` request out of bounds, return only what's available
+                        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 {
+                // `Range` request out of bounds, return only what's available
+                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
+        // NOTE: default to `BadRange` in case of wrong `Range` bytes format
+        .unwrap_or(Err(BadRange));
+
+    resp
 }
 
 #[cfg(test)]


Test patch:
diff --git a/tests/static_files.rs b/tests/static_files.rs
index 0c55a540..b7e0c74b 100644
--- a/tests/static_files.rs
+++ b/tests/static_files.rs
@@ -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();
-        // Range::Unbounded for beginning and end
-        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();
+        // Range::Unbounded for beginning and end
+        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")