Repository: sanic-org/sanic

Issue:
New style streaming route handler improvements
**Is your feature request related to a problem? Please describe.**
The new streaming API allows streaming responses in the route handler without having to use a callback:

```python
@app.route("/")
async def test(request):
    response = await request.respond(content_type="text/csv")
    await response.send("foo,")
    await response.send("bar")
    await response.send("", True)
    return response
```

**Describe the solution you'd like**
A simpler method to closing the stream before returning in place of `await response.send("", True)`

```python
await response.eof()
```

Under the hood, `eof` should simply just be a convenience call to `send("", True)`.


Discussion:
this should work in `sanic -> response -> BaseHTTPResponse`, right?
```
    async def eof(self):
        await self.send("", True)
```

@ahopkins 
Yup. Eventually, we probably can merge `HTTPResponse` right into `BaseHTTPResponse`. Until we do that, the `eof()`  should be in the same place as `send()` since it would simply be a shortcut.
@ajay1mg Are you working on a PR for this?

If yes, then add a check for `self.streaming_fn` inside `eof()`. If it exists, that we should raise an exception since `eof` should only ever be called when there is no `streaming_fn`. And, maybe it should be `NotImplementedError` on `StreamingHTTPResponse`.
I am working on the PR for this but I am confused with your second comment. `HttpResponse` class doesn't have any attribute  `streaming_fn` nor does `BaseHTTPResponse`
Sorry for not being clear. This is what I meant:

```python
class StreamingHTTPResponse(BaseHTTPResponse):
    def eof(...):
        raise NotImplementedError
```

This class should not have it.

Resolving patch:
diff --git a/sanic/response.py b/sanic/response.py
--- a/sanic/response.py
+++ b/sanic/response.py
@@ -203,6 +203,9 @@ async def send(self, *args, **kwargs):
             self.streaming_fn = None
         await super().send(*args, **kwargs)
 
+    async def eof(self):
+        raise NotImplementedError
+
 
 class HTTPResponse(BaseHTTPResponse):
     """
@@ -235,6 +238,9 @@ def __init__(
         self.headers = Header(headers or {})
         self._cookies = None
 
+    async def eof(self):
+        await self.send("", True)
+
 
 def empty(
     status=204, headers: Optional[Dict[str, str]] = None


Test patch:
diff --git a/tests/test_response.py b/tests/test_response.py
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -529,3 +529,19 @@ def handler(request):
     request, response = app.test_client.get("/test")
     assert response.content_type is None
     assert response.body == b""
+
+
+def test_direct_response_stream(app):
+    @app.route("/")
+    async def test(request):
+        response = await request.respond(content_type="text/csv")
+        await response.send("foo,")
+        await response.send("bar")
+        await response.eof()
+        return response
+
+    _, response = app.test_client.get("/")
+    assert response.text == "foo,bar"
+    assert response.headers["Transfer-Encoding"] == "chunked"
+    assert response.headers["Content-Type"] == "text/csv"
+    assert "Content-Length" not in response.headers