Repository: sanic-org/sanic

Issue:
Define Static File Content Type
While trying to serve a utf-8 encoded HTML document via `Sanic.static`, I noticed that `content_type` cannot be set using `Sanic.static`.

`Sanic` header content_type [defaults to `text/plain`](https://github.com/channelcat/sanic/blob/master/sanic/static.py#L101)
if the type cannot be guessed by `mime_types.guess_type`, which seems to only analyze the filename.

Currently, the following is not possible (same goes for blueprints):
```python
app.static('/', get_data('index.html'), content_type='text/html; charset=utf-8')
```

Prior to this PR, you need to do the following to serve a document with a custom content-type:
```python
@app.route('/')
async def handle_index(request):
    return await response.file(
        'index.html',
        headers={'Content-Type': 'text/html; charset=utf-8'}
    )
```


I'll be submitting a PR shortly with a proposed fix.



Discussion:

Resolving patch:
diff --git a/sanic/app.py b/sanic/app.py
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -386,13 +386,14 @@ def middleware(self, middleware_or_request):
     def static(self, uri, file_or_directory, pattern=r'/?.+',
                use_modified_since=True, use_content_range=False,
                stream_large_files=False, name='static', host=None,
-               strict_slashes=None):
+               strict_slashes=None, content_type=None):
         """Register a root to serve files from. The input can either be a
         file or a directory. See
         """
         static_register(self, uri, file_or_directory, pattern,
                         use_modified_since, use_content_range,
-                        stream_large_files, name, host, strict_slashes)
+                        stream_large_files, name, host, strict_slashes,
+                        content_type)
 
     def blueprint(self, blueprint, **options):
         """Register a blueprint on the application.
diff --git a/sanic/static.py b/sanic/static.py
--- a/sanic/static.py
+++ b/sanic/static.py
@@ -19,7 +19,7 @@
 def register(app, uri, file_or_directory, pattern,
              use_modified_since, use_content_range,
              stream_large_files, name='static', host=None,
-             strict_slashes=None):
+             strict_slashes=None, content_type=None):
     # TODO: Though sanic is not a file server, I feel like we should at least
     #       make a good effort here.  Modified-since is nice, but we could
     #       also look into etags, expires, and caching
@@ -41,6 +41,7 @@ def register(app, uri, file_or_directory, pattern,
                               If this is an integer, this represents the
                               threshold size to switch to file_stream()
     :param name: user defined name used for url_for
+    :param content_type: user defined content type for header
     """
     # If we're not trying to match a file directly,
     # serve from the folder
@@ -95,10 +96,10 @@ async def _handler(request, file_uri=None):
                         del headers['Content-Length']
                         for key, value in _range.headers.items():
                             headers[key] = value
+            headers['Content-Type'] = content_type \
+                or guess_type(file_path)[0] or 'text/plain'
             if request.method == 'HEAD':
-                return HTTPResponse(
-                    headers=headers,
-                    content_type=guess_type(file_path)[0] or 'text/plain')
+                return HTTPResponse(headers=headers)
             else:
                 if stream_large_files:
                     if isinstance(stream_large_files, int):


Test patch:
diff --git a/tests/static/test.html b/tests/static/test.html
new file mode 100644
--- /dev/null
+++ b/tests/static/test.html
@@ -0,0 +1,26 @@
+<html>
+<body>
+<pre>
+                 ▄▄▄▄▄
+        ▀▀▀██████▄▄▄       _______________
+      ▄▄▄▄▄  █████████▄  /                 \
+     ▀▀▀▀█████▌ ▀▐▄ ▀▐█ |   Gotta go fast!  |
+   ▀▀█████▄▄ ▀██████▄██ | _________________/
+   ▀▄▄▄▄▄  ▀▀█▄▀█════█▀ |/
+        ▀▀▀▄  ▀▀███ ▀       ▄▄
+     ▄███▀▀██▄████████▄ ▄▀▀▀▀▀▀█▌
+   ██▀▄▄▄██▀▄███▀ ▀▀████      ▄██
+▄▀▀▀▄██▄▀▀▌████▒▒▒▒▒▒███     ▌▄▄▀
+▌    ▐▀████▐███▒▒▒▒▒▐██▌
+▀▄▄▄▄▀   ▀▀████▒▒▒▒▄██▀
+          ▀▀█████████▀
+        ▄▄██▀██████▀█
+      ▄██▀     ▀▀▀  █
+     ▄█             ▐▌
+ ▄▄▄▄█▌              ▀█▄▄▄▄▀▀▄
+▌     ▐                ▀▀▄▄▄▀
+ ▀▀▄▄▀
+
+</pre>
+</body>
+</html>
diff --git a/tests/test_blueprints.py b/tests/test_blueprints.py
--- a/tests/test_blueprints.py
+++ b/tests/test_blueprints.py
@@ -1,5 +1,6 @@
 import asyncio
 import inspect
+import os
 import pytest
 
 from sanic import Sanic
@@ -13,6 +14,14 @@
 #  GET
 # ------------------------------------------------------------ #
 
+def get_file_path(static_file_directory, file_name):
+    return os.path.join(static_file_directory, file_name)
+
+def get_file_content(static_file_directory, file_name):
+    """The content of the static file to check"""
+    with open(get_file_path(static_file_directory, file_name), 'rb') as file:
+        return file.read()
+
 @pytest.mark.parametrize('method', HTTP_METHODS)
 def test_versioned_routes_get(method):
     app = Sanic('test_shorhand_routes_get')
@@ -348,6 +357,28 @@ def test_bp_static():
     assert response.status == 200
     assert response.body == current_file_contents
 
+@pytest.mark.parametrize('file_name', ['test.html'])
+def test_bp_static_content_type(file_name):
+    # This is done here, since no other test loads a file here
+    current_file = inspect.getfile(inspect.currentframe())
+    current_directory = os.path.dirname(os.path.abspath(current_file))
+    static_directory = os.path.join(current_directory, 'static')
+
+    app = Sanic('test_static')
+    blueprint = Blueprint('test_static')
+    blueprint.static(
+        '/testing.file',
+        get_file_path(static_directory, file_name),
+        content_type='text/html; charset=utf-8'
+    )
+
+    app.blueprint(blueprint)
+
+    request, response = app.test_client.get('/testing.file')
+    assert response.status == 200
+    assert response.body == get_file_content(static_directory, file_name)
+    assert response.headers['Content-Type'] == 'text/html; charset=utf-8'
+
 def test_bp_shorthand():
     app = Sanic('test_shorhand_routes')
     blueprint = Blueprint('test_shorhand_routes')
@@ -449,41 +480,41 @@ async def handler(request, ws):
 
 def test_bp_group():
     app = Sanic('test_nested_bp_groups')
-    
+
     deep_0 = Blueprint('deep_0', url_prefix='/deep')
     deep_1 = Blueprint('deep_1', url_prefix = '/deep1')
 
     @deep_0.route('/')
     def handler(request):
         return text('D0_OK')
-    
+
     @deep_1.route('/bottom')
     def handler(request):
         return text('D1B_OK')
 
     mid_0 = Blueprint.group(deep_0, deep_1, url_prefix='/mid')
     mid_1 = Blueprint('mid_tier', url_prefix='/mid1')
-    
+
     @mid_1.route('/')
     def handler(request):
         return text('M1_OK')
 
     top = Blueprint.group(mid_0, mid_1)
-    
+
     app.blueprint(top)
-    
+
     @app.route('/')
     def handler(request):
         return text('TOP_OK')
-    
+
     request, response = app.test_client.get('/')
     assert response.text == 'TOP_OK'
-    
+
     request, response = app.test_client.get('/mid1')
     assert response.text == 'M1_OK'
-    
+
     request, response = app.test_client.get('/mid/deep')
     assert response.text == 'D0_OK'
-    
+
     request, response = app.test_client.get('/mid/deep1/bottom')
     assert response.text == 'D1B_OK'
diff --git a/tests/test_static.py b/tests/test_static.py
--- a/tests/test_static.py
+++ b/tests/test_static.py
@@ -36,6 +36,21 @@ def test_static_file(static_file_directory, file_name):
     assert response.body == get_file_content(static_file_directory, file_name)
 
 
+@pytest.mark.parametrize('file_name', ['test.html'])
+def test_static_file_content_type(static_file_directory, file_name):
+    app = Sanic('test_static')
+    app.static(
+        '/testing.file',
+        get_file_path(static_file_directory, file_name),
+        content_type='text/html; charset=utf-8'
+    )
+
+    request, response = app.test_client.get('/testing.file')
+    assert response.status == 200
+    assert response.body == get_file_content(static_file_directory, file_name)
+    assert response.headers['Content-Type'] == 'text/html; charset=utf-8'
+
+
 @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
 @pytest.mark.parametrize('base_uri', ['/static', '', '/dir'])
 def test_static_directory(file_name, base_uri, static_file_directory):