Repository: pallets/werkzeug

Issue:
make_conditional(..., accept_ranges=True) respond range request with 200 OK
## Description

`make_conditional(request_or_environ, accept_ranges=True)` responds range requests with `206 Partial Content` when the request does not fetch the entire content, which is fine; when the range specified in the request covers the entire content, the server respond with `200 OK`, which may cause various problems.

## Expected Behavior

Server should respond with `206 Partial Content` in case of a successful range request, regardless of whether the requested range covers the entire content or not. Responding with `200 OK` indicates that the server has ignored the range header, thus client might consider that the server does not support range requests, which is not feasible when `accept_ranges` was specified as `True`.

## Impact

HTML5 video playbacks on old versions of safari (<= 12) does not work due to this problem, since safari has been strict in checking the status code of range requests. Request with `Range: bytes=0-` was responded with `200 OK` where `206 Partial Content` was expected. After patching the `ETagResponseMixin._process_range_request()` method to make it always respond with `206 Partial Content`, the video playback worked properly.

## Behavior of other webservers

* Web Servers: Both Nginx/1.14.0 and Apache/2.4.29 respond `Range: bytes=0-` with `206 Partial Content`.
* Object Stores: Both Amazon S3 and Baidu's BOS respond `Range: bytes=0-` with `206 Partial Content`.

## Possible Solution

Maybe we could remove the special treatment for the `content_length != complete_length` case in  `_process_range_request` method defined in `werkzeug/wrappers/etag.py`. I am not familiar with the codebase so I'd like to wait for professionals to come up with a correct fix.


Discussion:
Is this the same as #410? I don't have a Mac, but if always responding 206 to range requests solves the issue, that sounds acceptable to me.
The RFC does seem to support this interpretation. https://tools.ietf.org/html/rfc7233#section-3.1

> If ... the specified range(s) are valid and satisfiable (as defined in Section 2.1), the server SHOULD send a 206 (Partial Content) response...

It doesn't say anything about returning a normal 200 response if the range covers the entire contents.
I think it should change to 206 as well, this [from here](https://developer.mozilla.org/en-US/docs/Web/HTTP/Range_requests), 
> In case of no support of range requests, the 200 OK status is sent back from a server.

is what convinces me. I'll draw up a PR.
Interestingly the implementation is intentionally [returning 200](https://github.com/pallets/werkzeug/commit/0a23c94a352649655e2b1691c9008dd8082e752d#diff-67ab56f47231589fb9c088d78b68854dR1471), maybe @magne4000 could say why?

Resolving patch:
diff --git a/src/werkzeug/wrappers/etag.py b/src/werkzeug/wrappers/etag.py
--- a/src/werkzeug/wrappers/etag.py
+++ b/src/werkzeug/wrappers/etag.py
@@ -142,28 +142,31 @@ def _process_range_request(self, environ, complete_length=None, accept_ranges=No
         """
         from ..exceptions import RequestedRangeNotSatisfiable
 
-        if accept_ranges is None:
-            return False
-        self.headers["Accept-Ranges"] = accept_ranges
-        if not self._is_range_request_processable(environ) or complete_length is None:
+        if (
+            accept_ranges is None
+            or complete_length is None
+            or not self._is_range_request_processable(environ)
+        ):
             return False
+
         parsed_range = parse_range_header(environ.get("HTTP_RANGE"))
+
         if parsed_range is None:
             raise RequestedRangeNotSatisfiable(complete_length)
+
         range_tuple = parsed_range.range_for_length(complete_length)
         content_range_header = parsed_range.to_content_range_header(complete_length)
+
         if range_tuple is None or content_range_header is None:
             raise RequestedRangeNotSatisfiable(complete_length)
+
         content_length = range_tuple[1] - range_tuple[0]
-        # Be sure not to send 206 response
-        # if requested range is the full content.
-        if content_length != complete_length:
-            self.headers["Content-Length"] = content_length
-            self.content_range = content_range_header
-            self.status_code = 206
-            self._wrap_response(range_tuple[0], content_length)
-            return True
-        return False
+        self.headers["Content-Length"] = content_length
+        self.headers["Accept-Ranges"] = accept_ranges
+        self.content_range = content_range_header
+        self.status_code = 206
+        self._wrap_response(range_tuple[0], content_length)
+        return True
 
     def make_conditional(
         self, request_or_environ, accept_ranges=False, complete_length=None


Test patch:
diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py
--- a/tests/test_wrappers.py
+++ b/tests/test_wrappers.py
@@ -878,9 +878,9 @@ def test_range_request_with_complete_file():
         response = wrappers.Response(wrap_file(env, f))
         env["HTTP_RANGE"] = "bytes=0-%d" % (fsize - 1)
         response.make_conditional(env, accept_ranges=True, complete_length=fsize)
-        assert response.status_code == 200
+        assert response.status_code == 206
         assert response.headers["Accept-Ranges"] == "bytes"
-        assert "Content-Range" not in response.headers
+        assert response.headers["Content-Range"] == "bytes 0-%d/%d" % (fsize - 1, fsize)
         assert response.headers["Content-Length"] == str(fsize)
         assert response.data == fcontent