Repository: sanic-org/sanic

Issue:
Support status code for file response
## TL;DR
It is not possible to set response status for `response.file` and `response.file_stream` as currently suggested in documentation

## Issue
Current documentation e.g. at http://sanic.readthedocs.io/en/latest/sanic/response.html#modify-headers-or-status states that you simply need to add the `status` argument to set a custom status code for a response. This is not the case however for `response.file` and `response.file_stream` which always return status 200 by default (`sanic/response.py:263`, `sanic/response.py:318`).

This became an issue today where I wanted to show a login page for an unauthorized user but still return status 401.

Expected code:
```python
@app.exception(Unauthorized)
async def handle_401(request, exception):
    return response.file(web_dir / "login.html", status=401)
```

Actual code:
```python
@app.exception(Unauthorized)
async def handle_401(request, exception):
    response = await response.file(web_dir / "login.html")
    response.status = 401
    return response
```
## Solution
Looking at the source, it seems simple enough to add a `status` argument and use its value for `response.file` and `response.file_stream` like so:

```python
# response.py:236
async def file(
        location, status=200, mime_type=None, headers=None, filename=None, _range=None):
    """Return a response object with file data.
    :param location: Location of file on system.
    :param status: Response code.
    :param mime_type: Specific mime_type.
    :param headers: Custom Headers.
    :param filename: Override filename.
    :param _range:

    # [...]

    # response.py:263
    return HTTPResponse(status=status,
                        headers=headers,
                        content_type=mime_type,
                        body_bytes=out_stream)
```

However, if there are other considerations involved it would be good to explicitly state `status` is not available for `response.file` and `response.file_stream` in documentation



Discussion:

Resolving patch:
diff --git a/sanic/response.py b/sanic/response.py
--- a/sanic/response.py
+++ b/sanic/response.py
@@ -233,8 +233,8 @@ def html(body, status=200, headers=None):
                         content_type="text/html; charset=utf-8")
 
 
-async def file(
-        location, mime_type=None, headers=None, filename=None, _range=None):
+async def file(location, status=200, mime_type=None, headers=None,
+               filename=None, _range=None):
     """Return a response object with file data.
 
     :param location: Location of file on system.
@@ -260,15 +260,14 @@ async def file(
             out_stream = await _file.read()
 
     mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
-    return HTTPResponse(status=200,
+    return HTTPResponse(status=status,
                         headers=headers,
                         content_type=mime_type,
                         body_bytes=out_stream)
 
 
-async def file_stream(
-        location, chunk_size=4096, mime_type=None, headers=None,
-        filename=None, _range=None):
+async def file_stream(location, status=200, chunk_size=4096, mime_type=None,
+                      headers=None, filename=None, _range=None):
     """Return a streaming response object with file data.
 
     :param location: Location of file on system.
@@ -315,7 +314,7 @@ async def _streaming_fn(response):
         headers['Content-Range'] = 'bytes %s-%s/%s' % (
             _range.start, _range.end, _range.total)
     return StreamingHTTPResponse(streaming_fn=_streaming_fn,
-                                 status=200,
+                                 status=status,
                                  headers=headers,
                                  content_type=mime_type)
 


Test patch:
diff --git a/tests/test_response.py b/tests/test_response.py
--- a/tests/test_response.py
+++ b/tests/test_response.py
@@ -227,17 +227,19 @@ def get_file_content(static_file_directory, file_name):
 
 
 @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
-def test_file_response(file_name, static_file_directory):
+@pytest.mark.parametrize('status', [200, 401])
+def test_file_response(file_name, static_file_directory, status):
     app = Sanic('test_file_helper')
 
     @app.route('/files/<filename>', methods=['GET'])
     def file_route(request, filename):
         file_path = os.path.join(static_file_directory, filename)
         file_path = os.path.abspath(unquote(file_path))
-        return file(file_path, mime_type=guess_type(file_path)[0] or 'text/plain')
+        return file(file_path, status=status,
+                    mime_type=guess_type(file_path)[0] or 'text/plain')
 
     request, response = app.test_client.get('/files/{}'.format(file_name))
-    assert response.status == 200
+    assert response.status == status
     assert response.body == get_file_content(static_file_directory, file_name)
     assert 'Content-Disposition' not in response.headers