Repository: tornadoweb/tornado

Issue:
When HttpResponse body is empty it returns a ValueError when it should not
In checkin https://github.com/tornadoweb/tornado/pull/2514/commits/7b846ea56bff1892a4d4d05206210b4d234e292b the code for httpclient.HttpResponse.body was changed to throw a ValueError when the body is empty.

But since the message body is optional throwing an ValueError seems not right because it is not an error. Can it be reverted back to the 5 behavior of just returning a None when the body is empty.


Discussion:
That's a kinda big commit, so just in case it helps others identify the bit you're talking about:

```diff
--- a/tornado/httpclient.py
+++ b/tornado/httpclient.py
@@ -610,9 +610,9 @@ def __init__(self, request: HTTPRequest, code: int,
         self.time_info = time_info or {}
 
     @property
-    def body(self) -> Optional[bytes]:
+    def body(self) -> bytes:
         if self.buffer is None:
-            return None
+            raise ValueError("body not set")
         elif self._body is None:
             self._body = self.buffer.getvalue()
```
I think the reason was that there's a bunch of places in tornado, and presumably many tornado-using applications, that only access the body when they need data from it, for example:

```python
    def _on_authentication_verified(self, response: httpclient.HTTPResponse) -> Dict[str, Any]:
        handler = cast(RequestHandler, self)
        if b"is_valid:true" not in response.body:
            raise AuthError("Invalid OpenID response: %s" % response.body)
```

In cases like this, it just results in a different kind of exception, and it's more convenient than checking for `None` in many places in order to satisfy the mypy type analysis.

There could be applications which sometimes do semi-generic request proxying, and might use logic like:

```python
if response.body:
    self.write(response.body)
```

that could be changed to do something like:

```python
try:
    self.write(response.body)
except ValueError:
    pass
```

Since tornado-6.0 is a new major version, some incompatibilities are to be allowed for. But I can understand debate on this particular issue, it does seem like something many would run into. It might be a bit too late to change it back though ...
Returning None is awkward because you have to explicitly check for it. All non-error HTTPResponses will have a non-None body (and by "error", I just mean things like timeouts and socket errors. HTTP error responses like 404 and 503 will still have bodies). And in modern usage, those conditions get raised as errors so you don't even see the HTTP response objects. 

But still, it's possible to get to the response object and I can see how the exception is annoying. I wouldn't want to go back to None, but an empty byte string would be reasonable. I'm going to do a 6.0.2 soon to fix the regression in WebSocketHandler.set_nodelay, so I think it's not too late to fix an edge-case incompatibility like this. 

Resolving patch:
diff --git a/tornado/httpclient.py b/tornado/httpclient.py
--- a/tornado/httpclient.py
+++ b/tornado/httpclient.py
@@ -665,7 +665,7 @@ def __init__(
     @property
     def body(self) -> bytes:
         if self.buffer is None:
-            raise ValueError("body not set")
+            return b""
         elif self._body is None:
             self._body = self.buffer.getvalue()
 


Test patch: