diff --git a/docs/conf.py b/docs/conf.py
index e254c183..7dd7462c 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -25,7 +25,7 @@ import sanic
# -- General configuration ------------------------------------------------
-extensions = ['sphinx.ext.autodoc']
+extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.asyncio']
templates_path = ['_templates']
diff --git a/docs/sanic/blueprints.md b/docs/sanic/blueprints.md
index 5be33cb6..1a7c5293 100644
--- a/docs/sanic/blueprints.md
+++ b/docs/sanic/blueprints.md
@@ -93,7 +93,14 @@ def ignore_404s(request, exception):
Static files can be served globally, under the blueprint prefix.
```python
-bp.static('/folder/to/serve', '/web/path')
+
+# suppose bp.name == 'bp'
+
+bp.static('/web/path', '/folder/to/serve')
+# also you can pass name parameter to it for url_for
+bp.static('/web/path', '/folder/to/server', name='uploads')
+app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/web/path/file.txt'
+
```
## Start and stop
diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md
index 5643f4fc..ad9b8156 100644
--- a/docs/sanic/extensions.md
+++ b/docs/sanic/extensions.md
@@ -7,6 +7,7 @@ A list of Sanic extensions created by the community.
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
- [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress.
- [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template.
+- [JWT](https://github.com/ahopkins/sanic-jwt): Authentication extension for JSON Web Tokens (JWT).
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support.
- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper.
diff --git a/docs/sanic/getting_started.md b/docs/sanic/getting_started.md
index 04d22248..3e89cc3e 100644
--- a/docs/sanic/getting_started.md
+++ b/docs/sanic/getting_started.md
@@ -9,15 +9,16 @@ syntax, so earlier versions of python won't work.
```python
from sanic import Sanic
- from sanic.response import text
+ from sanic.response import json
- app = Sanic(__name__)
+ app = Sanic()
@app.route("/")
async def test(request):
- return text('Hello world!')
+ return json({"hello": "world"})
- app.run(host="0.0.0.0", port=8000, debug=True)
+ if __name__ == "__main__":
+ app.run(host="0.0.0.0", port=8000)
```
3. Run the server: `python3 main.py`
diff --git a/docs/sanic/logging.md b/docs/sanic/logging.md
index eb807388..49805d0e 100644
--- a/docs/sanic/logging.md
+++ b/docs/sanic/logging.md
@@ -9,12 +9,6 @@ A simple example using default settings would be like this:
```python
from sanic import Sanic
-from sanic.config import LOGGING
-
-# The default logging handlers are ['accessStream', 'errorStream']
-# but we change it to use other handlers here for demo purpose
-LOGGING['loggers']['network']['handlers'] = [
- 'accessSysLog', 'errorSysLog']
app = Sanic('test')
@@ -23,14 +17,21 @@ async def test(request):
return response.text('Hello World!')
if __name__ == "__main__":
- app.run(log_config=LOGGING)
+ app.run(debug=True, access_log=True)
```
-And to close logging, simply assign log_config=None:
+To use your own logging config, simply use `logging.config.dictConfig`, or
+pass `log_config` when you initialize `Sanic` app:
+
+```python
+app = Sanic('test', log_config=LOGGING_CONFIG)
+```
+
+And to close logging, simply assign access_log=False:
```python
if __name__ == "__main__":
- app.run(log_config=None)
+ app.run(access_log=False)
```
This would skip calling logging functions when handling requests.
@@ -38,64 +39,29 @@ And you could even do further in production to gain extra speed:
```python
if __name__ == "__main__":
- # disable internal messages
- app.run(debug=False, log_config=None)
+ # disable debug messages
+ app.run(debug=False, access_log=False)
```
### Configuration
-By default, log_config parameter is set to use sanic.config.LOGGING dictionary for configuration. The default configuration provides several predefined `handlers`:
+By default, log_config parameter is set to use sanic.log.LOGGING_CONFIG_DEFAULTS dictionary for configuration.
-- internal (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))
- For internal information console outputs.
+There are three `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**:
-
-- accessStream (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))
- For requests information logging in console
-
-
-- errorStream (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))
- For error message and traceback logging in console.
-
-
-- accessSysLog (using [logging.handlers.SysLogHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler))
- For requests information logging to syslog.
- Currently supports Windows (via localhost:514), Darwin (/var/run/syslog),
- Linux (/dev/log) and FreeBSD (/dev/log).
- You would not be able to access this property if the directory doesn't exist.
- (Notice that in Docker you have to enable everything by yourself)
-
-
-- errorSysLog (using [logging.handlers.SysLogHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler))
- For error message and traceback logging to syslog.
- Currently supports Windows (via localhost:514), Darwin (/var/run/syslog),
- Linux (/dev/log) and FreeBSD (/dev/log).
- You would not be able to access this property if the directory doesn't exist.
- (Notice that in Docker you have to enable everything by yourself)
-
-
-And `filters`:
-
-- accessFilter (using sanic.log.DefaultFilter)
- The filter that allows only levels in `DEBUG`, `INFO`, and `NONE(0)`
-
-
-- errorFilter (using sanic.log.DefaultFilter)
- The filter that allows only levels in `WARNING`, `ERROR`, and `CRITICAL`
-
-There are two `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**:
-
-- sanic:
+- root:
Used to log internal messages.
+- sanic.error:
+ Used to log error logs.
-- network:
- Used to log requests from network, and any information from those requests.
+- sanic.access:
+ Used to log access logs.
#### Log format:
In addition to default parameters provided by python (asctime, levelname, message),
-Sanic provides additional parameters for network logger with accessFilter:
+Sanic provides additional parameters for access logger with:
- host (str)
request.ip
diff --git a/docs/sanic/routing.md b/docs/sanic/routing.md
index 49b7c0b8..98179e17 100644
--- a/docs/sanic/routing.md
+++ b/docs/sanic/routing.md
@@ -301,3 +301,34 @@ def handler(request):
# app.url_for('handler') == '/get'
# app.url_for('post_handler') == '/post'
```
+
+## Build URL for static files
+
+You can use `url_for` for static file url building now.
+If it's for file directly, `filename` can be ignored.
+
+```python
+
+app = Sanic('test_static')
+app.static('/static', './static')
+app.static('/uploads', './uploads', name='uploads')
+app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
+
+bp = Blueprint('bp', url_prefix='bp')
+bp.static('/static', './static')
+bp.static('/uploads', './uploads', name='uploads')
+bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
+app.blueprint(bp)
+
+# then build the url
+app.url_for('static', filename='file.txt') == '/static/file.txt'
+app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
+app.url_for('static', name='uploads', filename='file.txt') == '/uploads/file.txt'
+app.url_for('static', name='best_png') == '/the_best.png'
+
+# blueprint url building
+app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
+app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/uploads/file.txt'
+app.url_for('static', name='bp.best_png') == '/bp/static/the_best.png'
+
+```
diff --git a/docs/sanic/static_files.md b/docs/sanic/static_files.md
index f0ce9d78..3419cad1 100644
--- a/docs/sanic/static_files.md
+++ b/docs/sanic/static_files.md
@@ -6,16 +6,40 @@ filename. The file specified will then be accessible via the given endpoint.
```python
from sanic import Sanic
+from sanic.blueprints import Blueprint
+
app = Sanic(__name__)
# Serves files from the static folder to the URL /static
app.static('/static', './static')
+# use url_for to build the url, name defaults to 'static' and can be ignored
+app.url_for('static', filename='file.txt') == '/static/file.txt'
+app.url_for('static', name='static', filename='file.txt') == '/static/file.txt'
# Serves the file /home/ubuntu/test.png when the URL /the_best.png
# is requested
-app.static('/the_best.png', '/home/ubuntu/test.png')
+app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
+
+# you can use url_for to build the static file url
+# you can ignore name and filename parameters if you don't define it
+app.url_for('static', name='best_png') == '/the_best.png'
+app.url_for('static', name='best_png', filename='any') == '/the_best.png'
+
+# you need define the name for other static files
+app.static('/another.png', '/home/ubuntu/another.png', name='another')
+app.url_for('static', name='another') == '/another.png'
+app.url_for('static', name='another', filename='any') == '/another.png'
+
+# also, you can use static for blueprint
+bp = Blueprint('bp', url_prefix='/bp')
+bp.static('/static', './static')
+
+# servers the file directly
+bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png')
+app.blueprint(bp)
+
+app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt'
+app.url_for('static', name='bp.best_png') == '/bp/test_best.png'
app.run(host="0.0.0.0", port=8000)
```
-
-Note: currently you cannot build a URL for a static file using `url_for`.
diff --git a/examples/teapot.py b/examples/teapot.py
new file mode 100644
index 00000000..897f7836
--- /dev/null
+++ b/examples/teapot.py
@@ -0,0 +1,13 @@
+from sanic import Sanic
+from sanic import response as res
+
+app = Sanic(__name__)
+
+
+@app.route("/")
+async def test(req):
+ return res.text("I\'m a teapot", status=418)
+
+
+if __name__ == '__main__':
+ app.run(host="0.0.0.0", port=8000)
diff --git a/requirements-docs.txt b/requirements-docs.txt
index efa74079..e12c1846 100644
--- a/requirements-docs.txt
+++ b/requirements-docs.txt
@@ -1,3 +1,4 @@
sphinx
sphinx_rtd_theme
recommonmark
+sphinxcontrib-asyncio
diff --git a/sanic/__main__.py b/sanic/__main__.py
index cc580566..594256f8 100644
--- a/sanic/__main__.py
+++ b/sanic/__main__.py
@@ -1,7 +1,7 @@
from argparse import ArgumentParser
from importlib import import_module
-from sanic.log import log
+from sanic.log import logger
from sanic.app import Sanic
if __name__ == "__main__":
@@ -36,9 +36,9 @@ if __name__ == "__main__":
app.run(host=args.host, port=args.port,
workers=args.workers, debug=args.debug, ssl=ssl)
except ImportError as e:
- log.error("No module named {} found.\n"
- " Example File: project/sanic_server.py -> app\n"
- " Example Module: project.sanic_server.app"
- .format(e.name))
+ logger.error("No module named {} found.\n"
+ " Example File: project/sanic_server.py -> app\n"
+ " Example Module: project.sanic_server.app"
+ .format(e.name))
except ValueError as e:
- log.error("{}".format(e))
+ logger.error("{}".format(e))
diff --git a/sanic/app.py b/sanic/app.py
index 20c02a5c..8f1e0b90 100644
--- a/sanic/app.py
+++ b/sanic/app.py
@@ -10,11 +10,11 @@ from traceback import format_exc
from urllib.parse import urlencode, urlunparse
from ssl import create_default_context, Purpose
-from sanic.config import Config, LOGGING
+from sanic.config import Config
from sanic.constants import HTTP_METHODS
from sanic.exceptions import ServerError, URLBuildError, SanicException
from sanic.handlers import ErrorHandler
-from sanic.log import log
+from sanic.log import logger, error_logger, LOGGING_CONFIG_DEFAULTS
from sanic.response import HTTPResponse, StreamingHTTPResponse
from sanic.router import Router
from sanic.server import serve, serve_multiple, HttpProtocol, Signal
@@ -28,30 +28,21 @@ class Sanic:
def __init__(self, name=None, router=None, error_handler=None,
load_env=True, request_class=None,
- log_config=LOGGING, strict_slashes=False):
- if log_config:
- logging.config.dictConfig(log_config)
- # Only set up a default log handler if the
- # end-user application didn't set anything up.
- if not logging.root.handlers and log.level == logging.NOTSET:
- formatter = logging.Formatter(
- "%(asctime)s: %(levelname)s: %(message)s")
- handler = logging.StreamHandler()
- handler.setFormatter(formatter)
- log.addHandler(handler)
- log.setLevel(logging.INFO)
+ strict_slashes=False, log_config=None):
# Get name from previous stack frame
if name is None:
frame_records = stack()[1]
name = getmodulename(frame_records[1])
+ # logging
+ logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
+
self.name = name
self.router = router or Router()
self.request_class = request_class
self.error_handler = error_handler or ErrorHandler()
self.config = Config(load_env=load_env)
- self.log_config = log_config
self.request_middleware = deque()
self.response_middleware = deque()
self.blueprints = {}
@@ -354,13 +345,13 @@ class Sanic:
# Static Files
def static(self, uri, file_or_directory, pattern=r'/?.+',
use_modified_since=True, use_content_range=False,
- stream_large_files=False):
+ stream_large_files=False, name='static', host=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)
+ stream_large_files, name, host)
def blueprint(self, blueprint, **options):
"""Register a blueprint on the application.
@@ -410,12 +401,32 @@ class Sanic:
URLBuildError
"""
# find the route by the supplied view name
- uri, route = self.router.find_route_by_view_name(view_name)
+ kw = {}
+ # special static files url_for
+ if view_name == 'static':
+ kw.update(name=kwargs.pop('name', 'static'))
+ elif view_name.endswith('.static'): # blueprint.static
+ kwargs.pop('name', None)
+ kw.update(name=view_name)
- if not uri or not route:
+ uri, route = self.router.find_route_by_view_name(view_name, **kw)
+ if not (uri and route):
raise URLBuildError('Endpoint with name `{}` was not found'.format(
view_name))
+ if view_name == 'static' or view_name.endswith('.static'):
+ filename = kwargs.pop('filename', None)
+ # it's static folder
+ if ' If the media type remains unknown, the recipient SHOULD treat it
# > as type "application/octet-stream"
@@ -68,15 +69,27 @@ class Request(dict):
self._cookies = None
self.stream = None
+ def __repr__(self):
+ if self.method is None or not self.path:
+ return '<{0}>'.format(self.__class__.__name__)
+ return '<{0}: {1} {2}>'.format(self.__class__.__name__,
+ self.method,
+ self.path)
+
@property
def json(self):
if self.parsed_json is None:
- try:
- self.parsed_json = json_loads(self.body)
- except Exception:
- if not self.body:
- return None
- raise InvalidUsage("Failed when parsing body as json")
+ self.load_json()
+
+ return self.parsed_json
+
+ def load_json(self, loads=json_loads):
+ try:
+ self.parsed_json = loads(self.body)
+ except Exception:
+ if not self.body:
+ return None
+ raise InvalidUsage("Failed when parsing body as json")
return self.parsed_json
@@ -114,7 +127,7 @@ class Request(dict):
self.parsed_form, self.parsed_files = (
parse_multipart_form(self.body, boundary))
except Exception:
- log.exception("Failed when parsing form")
+ error_logger.exception("Failed when parsing form")
return self.parsed_form
@@ -170,8 +183,8 @@ class Request(dict):
remote_addrs = [
addr for addr in [
addr.strip() for addr in forwarded_for
- ] if addr
- ]
+ ] if addr
+ ]
if len(remote_addrs) > 0:
self._remote_addr = remote_addrs[0]
else:
diff --git a/sanic/response.py b/sanic/response.py
index 902b21c6..f661758b 100644
--- a/sanic/response.py
+++ b/sanic/response.py
@@ -56,6 +56,7 @@ ALL_STATUS_CODES = {
415: b'Unsupported Media Type',
416: b'Requested Range Not Satisfiable',
417: b'Expectation Failed',
+ 418: b'I\'m a teapot',
422: b'Unprocessable Entity',
423: b'Locked',
424: b'Failed Dependency',
@@ -63,6 +64,7 @@ ALL_STATUS_CODES = {
428: b'Precondition Required',
429: b'Too Many Requests',
431: b'Request Header Fields Too Large',
+ 451: b'Unavailable For Legal Reasons',
500: b'Internal Server Error',
501: b'Not Implemented',
502: b'Bad Gateway',
@@ -235,7 +237,8 @@ class HTTPResponse(BaseHTTPResponse):
def json(body, status=200, headers=None,
- content_type="application/json", **kwargs):
+ content_type="application/json", dumps=json_dumps,
+ **kwargs):
"""
Returns response object with body in json format.
@@ -244,7 +247,7 @@ def json(body, status=200, headers=None,
:param headers: Custom Headers.
:param kwargs: Remaining arguments that are passed to the json encoder.
"""
- return HTTPResponse(json_dumps(body, **kwargs), headers=headers,
+ return HTTPResponse(dumps(body, **kwargs), headers=headers,
status=status, content_type=content_type)
diff --git a/sanic/router.py b/sanic/router.py
index 79faaf1e..f943bc19 100644
--- a/sanic/router.py
+++ b/sanic/router.py
@@ -68,6 +68,7 @@ class Router:
def __init__(self):
self.routes_all = {}
self.routes_names = {}
+ self.routes_static_files = {}
self.routes_static = {}
self.routes_dynamic = defaultdict(list)
self.routes_always_check = []
@@ -148,6 +149,7 @@ class Router:
provided, any method is allowed
:param handler: request handler function.
When executed, it should provide a response object.
+ :param name: user defined route name for url_for
:return: Nothing
"""
if host is not None:
@@ -231,6 +233,12 @@ class Router:
# prefix the handler name with the blueprint name
# if available
+ # special prefix for static files
+ is_static = False
+ if name and name.startswith('_static_'):
+ is_static = True
+ name = name.split('_static_', 1)[-1]
+
if hasattr(handler, '__blueprintname__'):
handler_name = '{}.{}'.format(
handler.__blueprintname__, name or handler.__name__)
@@ -245,9 +253,15 @@ class Router:
parameters=parameters, name=handler_name, uri=uri)
self.routes_all[uri] = route
- pairs = self.routes_names.get(handler_name)
- if not (pairs and (pairs[0] + '/' == uri or uri + '/' == pairs[0])):
- self.routes_names[handler_name] = (uri, route)
+ if is_static:
+ pair = self.routes_static_files.get(handler_name)
+ if not (pair and (pair[0] + '/' == uri or uri + '/' == pair[0])):
+ self.routes_static_files[handler_name] = (uri, route)
+
+ else:
+ pair = self.routes_names.get(handler_name)
+ if not (pair and (pair[0] + '/' == uri or uri + '/' == pair[0])):
+ self.routes_names[handler_name] = (uri, route)
if properties['unhashable']:
self.routes_always_check.append(route)
@@ -274,6 +288,11 @@ class Router:
self.routes_names.pop(handler_name)
break
+ for handler_name, pairs in self.routes_static_files.items():
+ if pairs[0] == uri:
+ self.routes_static_files.pop(handler_name)
+ break
+
except KeyError:
raise RouteDoesNotExist("Route was not registered: {}".format(uri))
@@ -289,15 +308,19 @@ class Router:
self._get.cache_clear()
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
- def find_route_by_view_name(self, view_name):
+ def find_route_by_view_name(self, view_name, name=None):
"""Find a route in the router based on the specified view name.
:param view_name: string of view name to search by
+ :param kwargs: additional params, usually for static files
:return: tuple containing (uri, Route)
"""
if not view_name:
return (None, None)
+ if view_name == 'static' or view_name.endswith('.static'):
+ return self.routes_static_files.get(name, (None, None))
+
return self.routes_names.get(view_name, (None, None))
def get(self, request):
diff --git a/sanic/server.py b/sanic/server.py
index f62ba654..8f60a864 100644
--- a/sanic/server.py
+++ b/sanic/server.py
@@ -24,11 +24,12 @@ try:
except ImportError:
async_loop = asyncio
-from sanic.log import log, netlog
+from sanic.log import logger, access_logger
from sanic.response import HTTPResponse
from sanic.request import Request
from sanic.exceptions import (
- RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError)
+ RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError,
+ ServiceUnavailable)
current_time = None
@@ -63,17 +64,20 @@ class HttpProtocol(asyncio.Protocol):
# request params
'parser', 'request', 'url', 'headers',
# request config
- 'request_handler', 'request_timeout', 'request_max_size',
- 'request_class', 'is_request_stream', 'router',
- # enable or disable access log / error log purpose
- 'has_log',
+ 'request_handler', 'request_timeout', 'response_timeout',
+ 'keep_alive_timeout', 'request_max_size', 'request_class',
+ 'is_request_stream', 'router',
+ # enable or disable access log purpose
+ 'access_log',
# connection management
- '_total_request_size', '_timeout_handler', '_last_communication_time',
- '_is_stream_handler')
+ '_total_request_size', '_request_timeout_handler',
+ '_response_timeout_handler', '_keep_alive_timeout_handler',
+ '_last_request_time', '_last_response_time', '_is_stream_handler')
def __init__(self, *, loop, request_handler, error_handler,
signal=Signal(), connections=set(), request_timeout=60,
- request_max_size=None, request_class=None, has_log=True,
+ response_timeout=60, keep_alive_timeout=15,
+ request_max_size=None, request_class=None, access_log=True,
keep_alive=True, is_request_stream=False, router=None,
state=None, debug=False, **kwargs):
self.loop = loop
@@ -84,18 +88,23 @@ class HttpProtocol(asyncio.Protocol):
self.headers = None
self.router = router
self.signal = signal
- self.has_log = has_log
+ self.access_log = access_log
self.connections = connections
self.request_handler = request_handler
self.error_handler = error_handler
self.request_timeout = request_timeout
+ self.response_timeout = response_timeout
+ self.keep_alive_timeout = keep_alive_timeout
self.request_max_size = request_max_size
self.request_class = request_class or Request
self.is_request_stream = is_request_stream
self._is_stream_handler = False
self._total_request_size = 0
- self._timeout_handler = None
+ self._request_timeout_handler = None
+ self._response_timeout_handler = None
+ self._keep_alive_timeout_handler = None
self._last_request_time = None
+ self._last_response_time = None
self._request_handler_task = None
self._request_stream_task = None
self._keep_alive = keep_alive
@@ -118,22 +127,32 @@ class HttpProtocol(asyncio.Protocol):
def connection_made(self, transport):
self.connections.add(self)
- self._timeout_handler = self.loop.call_later(
- self.request_timeout, self.connection_timeout)
+ self._request_timeout_handler = self.loop.call_later(
+ self.request_timeout, self.request_timeout_callback)
self.transport = transport
self._last_request_time = current_time
def connection_lost(self, exc):
self.connections.discard(self)
- self._timeout_handler.cancel()
+ if self._request_timeout_handler:
+ self._request_timeout_handler.cancel()
+ if self._response_timeout_handler:
+ self._response_timeout_handler.cancel()
+ if self._keep_alive_timeout_handler:
+ self._keep_alive_timeout_handler.cancel()
- def connection_timeout(self):
- # Check if
+ def request_timeout_callback(self):
+ # See the docstring in the RequestTimeout exception, to see
+ # exactly what this timeout is checking for.
+ # Check if elapsed time since request initiated exceeds our
+ # configured maximum request timeout value
time_elapsed = current_time - self._last_request_time
if time_elapsed < self.request_timeout:
time_left = self.request_timeout - time_elapsed
- self._timeout_handler = (
- self.loop.call_later(time_left, self.connection_timeout))
+ self._request_timeout_handler = (
+ self.loop.call_later(time_left,
+ self.request_timeout_callback)
+ )
else:
if self._request_stream_task:
self._request_stream_task.cancel()
@@ -144,6 +163,36 @@ class HttpProtocol(asyncio.Protocol):
except RequestTimeout as exception:
self.write_error(exception)
+ def response_timeout_callback(self):
+ # Check if elapsed time since response was initiated exceeds our
+ # configured maximum request timeout value
+ time_elapsed = current_time - self._last_request_time
+ if time_elapsed < self.response_timeout:
+ time_left = self.response_timeout - time_elapsed
+ self._response_timeout_handler = (
+ self.loop.call_later(time_left,
+ self.response_timeout_callback)
+ )
+ else:
+ try:
+ raise ServiceUnavailable('Response Timeout')
+ except ServiceUnavailable as exception:
+ self.write_error(exception)
+
+ def keep_alive_timeout_callback(self):
+ # Check if elapsed time since last response exceeds our configured
+ # maximum keep alive timeout value
+ time_elapsed = current_time - self._last_response_time
+ if time_elapsed < self.keep_alive_timeout:
+ time_left = self.keep_alive_timeout - time_elapsed
+ self._keep_alive_timeout_handler = (
+ self.loop.call_later(time_left,
+ self.keep_alive_timeout_callback)
+ )
+ else:
+ logger.info('KeepAlive Timeout. Closing connection.')
+ self.transport.close()
+
# -------------------------------------------- #
# Parsing
# -------------------------------------------- #
@@ -189,10 +238,12 @@ class HttpProtocol(asyncio.Protocol):
and int(value) > self.request_max_size:
exception = PayloadTooLarge('Payload Too Large')
self.write_error(exception)
-
+ try:
+ value = value.decode()
+ except UnicodeDecodeError:
+ value = value.decode('latin_1')
self.headers.append(
- (self._header_fragment.decode().casefold(),
- value.decode()))
+ (self._header_fragment.decode().casefold(), value))
self._header_fragment = b''
@@ -204,6 +255,11 @@ class HttpProtocol(asyncio.Protocol):
method=self.parser.get_method().decode(),
transport=self.transport
)
+ # Remove any existing KeepAlive handler here,
+ # It will be recreated if required on the new request.
+ if self._keep_alive_timeout_handler:
+ self._keep_alive_timeout_handler.cancel()
+ self._keep_alive_timeout_handler = None
if self.is_request_stream:
self._is_stream_handler = self.router.is_stream_handler(
self.request)
@@ -219,6 +275,11 @@ class HttpProtocol(asyncio.Protocol):
self.request.body.append(body)
def on_message_complete(self):
+ # Entire request (headers and whole body) is received.
+ # We can cancel and remove the request timeout handler now.
+ if self._request_timeout_handler:
+ self._request_timeout_handler.cancel()
+ self._request_timeout_handler = None
if self.is_request_stream and self._is_stream_handler:
self._request_stream_task = self.loop.create_task(
self.request.stream.put(None))
@@ -227,6 +288,9 @@ class HttpProtocol(asyncio.Protocol):
self.execute_request_handler()
def execute_request_handler(self):
+ self._response_timeout_handler = self.loop.call_later(
+ self.response_timeout, self.response_timeout_callback)
+ self._last_request_time = current_time
self._request_handler_task = self.loop.create_task(
self.request_handler(
self.request,
@@ -236,35 +300,50 @@ class HttpProtocol(asyncio.Protocol):
# -------------------------------------------- #
# Responding
# -------------------------------------------- #
+ def log_response(self, response):
+ if self.access_log:
+ extra = {
+ 'status': getattr(response, 'status', 0),
+ }
+
+ if isinstance(response, HTTPResponse):
+ extra['byte'] = len(response.body)
+ else:
+ extra['byte'] = -1
+
+ if self.request:
+ extra['host'] = '{0}:{1}'.format(self.request.ip[0],
+ self.request.ip[1])
+ extra['request'] = '{0} {1}'.format(self.request.method,
+ self.request.url)
+ else:
+ extra['host'] = 'UNKNOWN'
+ extra['request'] = 'nil'
+
+ access_logger.info('', extra=extra)
+
def write_response(self, response):
"""
Writes response content synchronously to the transport.
"""
+ if self._response_timeout_handler:
+ self._response_timeout_handler.cancel()
+ self._response_timeout_handler = None
try:
keep_alive = self.keep_alive
self.transport.write(
response.output(
self.request.version, keep_alive,
- self.request_timeout))
- if self.has_log:
- netlog.info('', extra={
- 'status': response.status,
- 'byte': len(response.body),
- 'host': '{0}:{1}'.format(self.request.ip[0],
- self.request.ip[1]),
- 'request': '{0} {1}'.format(self.request.method,
- self.request.url)
- })
+ self.keep_alive_timeout))
+ self.log_response(response)
except AttributeError:
- log.error(
- ('Invalid response object for url {}, '
- 'Expected Type: HTTPResponse, Actual Type: {}').format(
- self.url, type(response)))
+ logger.error('Invalid response object for url %s, '
+ 'Expected Type: HTTPResponse, Actual Type: %s',
+ self.url, type(response))
self.write_error(ServerError('Invalid response type'))
except RuntimeError:
- log.error(
- 'Connection lost before response written @ {}'.format(
- self.request.ip))
+ logger.error('Connection lost before response written @ %s',
+ self.request.ip)
except Exception as e:
self.bail_out(
"Writing response failed, connection closed {}".format(
@@ -273,7 +352,10 @@ class HttpProtocol(asyncio.Protocol):
if not keep_alive:
self.transport.close()
else:
- self._last_request_time = current_time
+ self._keep_alive_timeout_handler = self.loop.call_later(
+ self.keep_alive_timeout,
+ self.keep_alive_timeout_callback)
+ self._last_response_time = current_time
self.cleanup()
async def stream_response(self, response):
@@ -282,31 +364,23 @@ class HttpProtocol(asyncio.Protocol):
the transport to the response so the response consumer can
write to the response as needed.
"""
-
+ if self._response_timeout_handler:
+ self._response_timeout_handler.cancel()
+ self._response_timeout_handler = None
try:
keep_alive = self.keep_alive
response.transport = self.transport
await response.stream(
- self.request.version, keep_alive, self.request_timeout)
- if self.has_log:
- netlog.info('', extra={
- 'status': response.status,
- 'byte': -1,
- 'host': '{0}:{1}'.format(self.request.ip[0],
- self.request.ip[1]),
- 'request': '{0} {1}'.format(self.request.method,
- self.request.url)
- })
+ self.request.version, keep_alive, self.keep_alive_timeout)
+ self.log_response(response)
except AttributeError:
- log.error(
- ('Invalid response object for url {}, '
- 'Expected Type: HTTPResponse, Actual Type: {}').format(
- self.url, type(response)))
+ logger.error('Invalid response object for url %s, '
+ 'Expected Type: HTTPResponse, Actual Type: %s',
+ self.url, type(response))
self.write_error(ServerError('Invalid response type'))
except RuntimeError:
- log.error(
- 'Connection lost before response written @ {}'.format(
- self.request.ip))
+ logger.error('Connection lost before response written @ %s',
+ self.request.ip)
except Exception as e:
self.bail_out(
"Writing response failed, connection closed {}".format(
@@ -315,58 +389,52 @@ class HttpProtocol(asyncio.Protocol):
if not keep_alive:
self.transport.close()
else:
- self._last_request_time = current_time
+ self._keep_alive_timeout_handler = self.loop.call_later(
+ self.keep_alive_timeout,
+ self.keep_alive_timeout_callback)
+ self._last_response_time = current_time
self.cleanup()
def write_error(self, exception):
+ # An error _is_ a response.
+ # Don't throw a response timeout, when a response _is_ given.
+ if self._response_timeout_handler:
+ self._response_timeout_handler.cancel()
+ self._response_timeout_handler = None
response = None
try:
response = self.error_handler.response(self.request, exception)
version = self.request.version if self.request else '1.1'
self.transport.write(response.output(version))
except RuntimeError:
- log.error(
- 'Connection lost before error written @ {}'.format(
- self.request.ip if self.request else 'Unknown'))
+ logger.error('Connection lost before error written @ %s',
+ self.request.ip if self.request else 'Unknown')
except Exception as e:
self.bail_out(
- "Writing error failed, connection closed {}".format(repr(e)),
- from_error=True)
+ "Writing error failed, connection closed {}".format(
+ repr(e)), from_error=True
+ )
finally:
- if self.has_log:
- extra = dict()
- if isinstance(response, HTTPResponse):
- extra['status'] = response.status
- extra['byte'] = len(response.body)
- else:
- extra['status'] = 0
- extra['byte'] = -1
- if self.request:
- extra['host'] = '%s:%d' % self.request.ip,
- extra['request'] = '%s %s' % (self.request.method,
- self.url)
- else:
- extra['host'] = 'UNKNOWN'
- extra['request'] = 'nil'
- if self.parser and not (self.keep_alive
- and extra['status'] == 408):
- netlog.info('', extra=extra)
+ if self.parser and (self.keep_alive
+ or getattr(response, 'status', 0) == 408):
+ self.log_response(response)
self.transport.close()
def bail_out(self, message, from_error=False):
if from_error or self.transport.is_closing():
- log.error(
- ("Transport closed @ {} and exception "
- "experienced during error handling").format(
- self.transport.get_extra_info('peername')))
- log.debug(
- 'Exception:\n{}'.format(traceback.format_exc()))
+ logger.error("Transport closed @ %s and exception "
+ "experienced during error handling",
+ self.transport.get_extra_info('peername'))
+ logger.debug('Exception:\n%s', traceback.format_exc())
else:
exception = ServerError(message)
self.write_error(exception)
- log.error(message)
+ logger.error(message)
def cleanup(self):
+ """This is called when KeepAlive feature is used,
+ it resets the connection in order for it to be able
+ to handle receiving another request on the same connection."""
self.parser = None
self.request = None
self.url = None
@@ -421,12 +489,13 @@ def trigger_events(events, loop):
def serve(host, port, request_handler, error_handler, before_start=None,
after_start=None, before_stop=None, after_stop=None, debug=False,
- request_timeout=60, ssl=None, sock=None, request_max_size=None,
- reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
+ request_timeout=60, response_timeout=60, keep_alive_timeout=60,
+ ssl=None, sock=None, request_max_size=None, reuse_port=False,
+ loop=None, protocol=HttpProtocol, backlog=100,
register_sys_signals=True, run_async=False, connections=None,
- signal=Signal(), request_class=None, has_log=True, keep_alive=True,
- is_request_stream=False, router=None, websocket_max_size=None,
- websocket_max_queue=None, state=None,
+ signal=Signal(), request_class=None, access_log=True,
+ keep_alive=True, is_request_stream=False, router=None,
+ websocket_max_size=None, websocket_max_queue=None, state=None,
graceful_shutdown_timeout=15.0):
"""Start asynchronous HTTP Server on an individual process.
@@ -453,7 +522,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
:param loop: asyncio compatible event loop
:param protocol: subclass of asyncio protocol class
:param request_class: Request class to use
- :param has_log: disable/enable access log and error log
+ :param access_log: disable/enable access log
:param is_request_stream: disable/enable Request.stream
:param router: Router object
:return: Nothing
@@ -474,9 +543,11 @@ def serve(host, port, request_handler, error_handler, before_start=None,
request_handler=request_handler,
error_handler=error_handler,
request_timeout=request_timeout,
+ response_timeout=response_timeout,
+ keep_alive_timeout=keep_alive_timeout,
request_max_size=request_max_size,
request_class=request_class,
- has_log=has_log,
+ access_log=access_log,
keep_alive=keep_alive,
is_request_stream=is_request_stream,
router=router,
@@ -508,7 +579,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
try:
http_server = loop.run_until_complete(server_coroutine)
except:
- log.exception("Unable to start server")
+ logger.exception("Unable to start server")
return
trigger_events(after_start, loop)
@@ -519,14 +590,14 @@ def serve(host, port, request_handler, error_handler, before_start=None,
try:
loop.add_signal_handler(_signal, loop.stop)
except NotImplementedError:
- log.warn('Sanic tried to use loop.add_signal_handler but it is'
- ' not implemented on this platform.')
+ logger.warning('Sanic tried to use loop.add_signal_handler '
+ 'but it is not implemented on this platform.')
pid = os.getpid()
try:
- log.info('Starting worker [{}]'.format(pid))
+ logger.info('Starting worker [%s]', pid)
loop.run_forever()
finally:
- log.info("Stopping worker [{}]".format(pid))
+ logger.info("Stopping worker [%s]", pid)
# Run the on_stop function if provided
trigger_events(before_stop, loop)
@@ -588,8 +659,7 @@ def serve_multiple(server_settings, workers):
server_settings['port'] = None
def sig_handler(signal, frame):
- log.info("Received signal {}. Shutting down.".format(
- Signals(signal).name))
+ logger.info("Received signal %s. Shutting down.", Signals(signal).name)
for process in processes:
os.kill(process.pid, SIGINT)
diff --git a/sanic/static.py b/sanic/static.py
index 36cb47db..1ebd7291 100644
--- a/sanic/static.py
+++ b/sanic/static.py
@@ -18,7 +18,7 @@ from sanic.response import file, file_stream, HTTPResponse
def register(app, uri, file_or_directory, pattern,
use_modified_since, use_content_range,
- stream_large_files):
+ stream_large_files, name='static', host=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
@@ -39,6 +39,7 @@ def register(app, uri, file_or_directory, pattern,
than the file() handler to send the file
If this is an integer, this represents the
threshold size to switch to file_stream()
+ :param name: user defined name used for url_for
"""
# If we're not trying to match a file directly,
# serve from the folder
@@ -117,4 +118,8 @@ def register(app, uri, file_or_directory, pattern,
path=file_or_directory,
relative_url=file_uri)
- app.route(uri, methods=['GET', 'HEAD'])(_handler)
+ # special prefix for static files
+ if not name.startswith('_static_'):
+ name = '_static_{}'.format(name)
+
+ app.route(uri, methods=['GET', 'HEAD'], name=name, host=host)(_handler)
diff --git a/sanic/testing.py b/sanic/testing.py
index de26d025..5d233d7b 100644
--- a/sanic/testing.py
+++ b/sanic/testing.py
@@ -1,7 +1,7 @@
import traceback
from json import JSONDecodeError
-from sanic.log import log
+from sanic.log import logger
HOST = '127.0.0.1'
PORT = 42101
@@ -19,7 +19,7 @@ class SanicTestClient:
url = 'http://{host}:{port}{uri}'.format(
host=HOST, port=PORT, uri=uri)
- log.info(url)
+ logger.info(url)
conn = aiohttp.TCPConnector(verify_ssl=False)
async with aiohttp.ClientSession(
cookies=cookies, connector=conn) as session:
@@ -61,7 +61,7 @@ class SanicTestClient:
**request_kwargs)
results[-1] = response
except Exception as e:
- log.error(
+ logger.error(
'Exception:\n{}'.format(traceback.format_exc()))
exceptions.append(e)
self.app.stop()
diff --git a/sanic/worker.py b/sanic/worker.py
index 9f950c34..811c7e5c 100644
--- a/sanic/worker.py
+++ b/sanic/worker.py
@@ -142,9 +142,8 @@ class GunicornWorker(base.Worker):
)
if self.max_requests and req_count > self.max_requests:
self.alive = False
- self.log.info(
- "Max requests exceeded, shutting down: %s", self
- )
+ self.log.info("Max requests exceeded, shutting down: %s",
+ self)
elif pid == os.getpid() and self.ppid != os.getppid():
self.alive = False
self.log.info("Parent changed, shutting down: %s", self)
diff --git a/tests/static/bp/decode me.txt b/tests/static/bp/decode me.txt
new file mode 100644
index 00000000..b1c36682
--- /dev/null
+++ b/tests/static/bp/decode me.txt
@@ -0,0 +1 @@
+I am just a regular static file that needs to have its uri decoded
diff --git a/tests/static/bp/python.png b/tests/static/bp/python.png
new file mode 100644
index 00000000..52fda109
Binary files /dev/null and b/tests/static/bp/python.png differ
diff --git a/tests/static/bp/test.file b/tests/static/bp/test.file
new file mode 100644
index 00000000..0725a6ef
--- /dev/null
+++ b/tests/static/bp/test.file
@@ -0,0 +1 @@
+I am just a regular static file
diff --git a/tests/test_cookies.py b/tests/test_cookies.py
index d88288ee..84b493cb 100644
--- a/tests/test_cookies.py
+++ b/tests/test_cookies.py
@@ -25,6 +25,25 @@ def test_cookies():
assert response.text == 'Cookies are: working!'
assert response_cookies['right_back'].value == 'at you'
+@pytest.mark.parametrize("httponly,expected", [
+ (False, False),
+ (True, True),
+])
+def test_false_cookies_encoded(httponly, expected):
+ app = Sanic('test_text')
+
+ @app.route('/')
+ def handler(request):
+ response = text('hello cookies')
+ response.cookies['hello'] = 'world'
+ response.cookies['hello']['httponly'] = httponly
+ return text(response.cookies['hello'].encode('utf8'))
+
+ request, response = app.test_client.get('/')
+
+ assert ('HttpOnly' in response.text) == expected
+
+
@pytest.mark.parametrize("httponly,expected", [
(False, False),
(True, True),
@@ -34,7 +53,7 @@ def test_false_cookies(httponly, expected):
@app.route('/')
def handler(request):
- response = text('Cookies are: {}'.format(request.cookies['test']))
+ response = text('hello cookies')
response.cookies['right_back'] = 'at you'
response.cookies['right_back']['httponly'] = httponly
return response
@@ -43,7 +62,7 @@ def test_false_cookies(httponly, expected):
response_cookies = SimpleCookie()
response_cookies.load(response.headers.get('Set-Cookie', {}))
- 'HttpOnly' in response_cookies == expected
+ assert ('HttpOnly' in response_cookies['right_back'].output()) == expected
def test_http2_cookies():
app = Sanic('test_http2_cookies')
diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py
new file mode 100644
index 00000000..15f6d705
--- /dev/null
+++ b/tests/test_keep_alive_timeout.py
@@ -0,0 +1,269 @@
+from json import JSONDecodeError
+from sanic import Sanic
+import asyncio
+from asyncio import sleep as aio_sleep
+from sanic.response import text
+from sanic.config import Config
+from sanic import server
+import aiohttp
+from aiohttp import TCPConnector
+from sanic.testing import SanicTestClient, HOST, PORT
+
+
+class ReuseableTCPConnector(TCPConnector):
+ def __init__(self, *args, **kwargs):
+ super(ReuseableTCPConnector, self).__init__(*args, **kwargs)
+ self.old_proto = None
+
+ @asyncio.coroutine
+ def connect(self, req):
+ new_conn = yield from super(ReuseableTCPConnector, self)\
+ .connect(req)
+ if self.old_proto is not None:
+ if self.old_proto != new_conn._protocol:
+ raise RuntimeError(
+ "We got a new connection, wanted the same one!")
+ print(new_conn.__dict__)
+ self.old_proto = new_conn._protocol
+ return new_conn
+
+
+class ReuseableSanicTestClient(SanicTestClient):
+ def __init__(self, app, loop=None):
+ super(ReuseableSanicTestClient, self).__init__(app)
+ if loop is None:
+ loop = asyncio.get_event_loop()
+ self._loop = loop
+ self._server = None
+ self._tcp_connector = None
+ self._session = None
+
+ # Copied from SanicTestClient, but with some changes to reuse the
+ # same loop for the same app.
+ def _sanic_endpoint_test(
+ self, method='get', uri='/', gather_request=True,
+ debug=False, server_kwargs={},
+ *request_args, **request_kwargs):
+ loop = self._loop
+ results = [None, None]
+ exceptions = []
+ do_kill_server = request_kwargs.pop('end_server', False)
+ if gather_request:
+ def _collect_request(request):
+ if results[0] is None:
+ results[0] = request
+
+ self.app.request_middleware.appendleft(_collect_request)
+
+ @self.app.listener('after_server_start')
+ async def _collect_response(loop):
+ try:
+ if do_kill_server:
+ request_kwargs['end_session'] = True
+ response = await self._local_request(
+ method, uri, *request_args,
+ **request_kwargs)
+ results[-1] = response
+ except Exception as e2:
+ import traceback
+ traceback.print_tb(e2.__traceback__)
+ exceptions.append(e2)
+ #Don't stop here! self.app.stop()
+
+ if self._server is not None:
+ _server = self._server
+ else:
+ _server_co = self.app.create_server(host=HOST, debug=debug,
+ port=PORT, **server_kwargs)
+
+ server.trigger_events(
+ self.app.listeners['before_server_start'], loop)
+
+ try:
+ loop._stopping = False
+ http_server = loop.run_until_complete(_server_co)
+ except Exception as e1:
+ import traceback
+ traceback.print_tb(e1.__traceback__)
+ raise e1
+ self._server = _server = http_server
+ server.trigger_events(
+ self.app.listeners['after_server_start'], loop)
+ self.app.listeners['after_server_start'].pop()
+
+ if do_kill_server:
+ try:
+ _server.close()
+ self._server = None
+ loop.run_until_complete(_server.wait_closed())
+ self.app.stop()
+ except Exception as e3:
+ import traceback
+ traceback.print_tb(e3.__traceback__)
+ exceptions.append(e3)
+ if exceptions:
+ raise ValueError(
+ "Exception during request: {}".format(exceptions))
+
+ if gather_request:
+ self.app.request_middleware.pop()
+ try:
+ request, response = results
+ return request, response
+ except:
+ raise ValueError(
+ "Request and response object expected, got ({})".format(
+ results))
+ else:
+ try:
+ return results[-1]
+ except:
+ raise ValueError(
+ "Request object expected, got ({})".format(results))
+
+ # Copied from SanicTestClient, but with some changes to reuse the
+ # same TCPConnection and the sane ClientSession more than once.
+ # Note, you cannot use the same session if you are in a _different_
+ # loop, so the changes above are required too.
+ async def _local_request(self, method, uri, cookies=None, *args,
+ **kwargs):
+ request_keepalive = kwargs.pop('request_keepalive',
+ Config.KEEP_ALIVE_TIMEOUT)
+ if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
+ url = uri
+ else:
+ url = 'http://{host}:{port}{uri}'.format(
+ host=HOST, port=PORT, uri=uri)
+ do_kill_session = kwargs.pop('end_session', False)
+ if self._session:
+ session = self._session
+ else:
+ if self._tcp_connector:
+ conn = self._tcp_connector
+ else:
+ conn = ReuseableTCPConnector(verify_ssl=False,
+ loop=self._loop,
+ keepalive_timeout=
+ request_keepalive)
+ self._tcp_connector = conn
+ session = aiohttp.ClientSession(cookies=cookies,
+ connector=conn,
+ loop=self._loop)
+ self._session = session
+
+ async with getattr(session, method.lower())(
+ url, *args, **kwargs) as response:
+ try:
+ response.text = await response.text()
+ except UnicodeDecodeError:
+ response.text = None
+
+ try:
+ response.json = await response.json()
+ except (JSONDecodeError,
+ UnicodeDecodeError,
+ aiohttp.ClientResponseError):
+ response.json = None
+
+ response.body = await response.read()
+ if do_kill_session:
+ session.close()
+ self._session = None
+ return response
+
+
+Config.KEEP_ALIVE_TIMEOUT = 2
+Config.KEEP_ALIVE = True
+keep_alive_timeout_app_reuse = Sanic('test_ka_timeout_reuse')
+keep_alive_app_client_timeout = Sanic('test_ka_client_timeout')
+keep_alive_app_server_timeout = Sanic('test_ka_server_timeout')
+
+
+@keep_alive_timeout_app_reuse.route('/1')
+async def handler1(request):
+ return text('OK')
+
+
+@keep_alive_app_client_timeout.route('/1')
+async def handler2(request):
+ return text('OK')
+
+
+@keep_alive_app_server_timeout.route('/1')
+async def handler3(request):
+ return text('OK')
+
+
+def test_keep_alive_timeout_reuse():
+ """If the server keep-alive timeout and client keep-alive timeout are
+ both longer than the delay, the client _and_ server will successfully
+ reuse the existing connection."""
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ client = ReuseableSanicTestClient(keep_alive_timeout_app_reuse, loop)
+ headers = {
+ 'Connection': 'keep-alive'
+ }
+ request, response = client.get('/1', headers=headers)
+ assert response.status == 200
+ assert response.text == 'OK'
+ loop.run_until_complete(aio_sleep(1))
+ request, response = client.get('/1', end_server=True)
+ assert response.status == 200
+ assert response.text == 'OK'
+
+
+def test_keep_alive_client_timeout():
+ """If the server keep-alive timeout is longer than the client
+ keep-alive timeout, client will try to create a new connection here."""
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ client = ReuseableSanicTestClient(keep_alive_app_client_timeout,
+ loop)
+ headers = {
+ 'Connection': 'keep-alive'
+ }
+ request, response = client.get('/1', headers=headers,
+ request_keepalive=1)
+ assert response.status == 200
+ assert response.text == 'OK'
+ loop.run_until_complete(aio_sleep(2))
+ exception = None
+ try:
+ request, response = client.get('/1', end_server=True,
+ request_keepalive=1)
+ except ValueError as e:
+ exception = e
+ assert exception is not None
+ assert isinstance(exception, ValueError)
+ assert "got a new connection" in exception.args[0]
+
+
+def test_keep_alive_server_timeout():
+ """If the client keep-alive timeout is longer than the server
+ keep-alive timeout, the client will either a 'Connection reset' error
+ _or_ a new connection. Depending on how the event-loop handles the
+ broken server connection."""
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+ client = ReuseableSanicTestClient(keep_alive_app_server_timeout,
+ loop)
+ headers = {
+ 'Connection': 'keep-alive'
+ }
+ request, response = client.get('/1', headers=headers,
+ request_keepalive=60)
+ assert response.status == 200
+ assert response.text == 'OK'
+ loop.run_until_complete(aio_sleep(3))
+ exception = None
+ try:
+ request, response = client.get('/1', request_keepalive=60,
+ end_server=True)
+ except ValueError as e:
+ exception = e
+ assert exception is not None
+ assert isinstance(exception, ValueError)
+ assert "Connection reset" in exception.args[0] or \
+ "got a new connection" in exception.args[0]
+
diff --git a/tests/test_logging.py b/tests/test_logging.py
index d6911d86..112c94a0 100644
--- a/tests/test_logging.py
+++ b/tests/test_logging.py
@@ -1,8 +1,8 @@
import uuid
from importlib import reload
-from sanic.config import LOGGING
from sanic.response import text
+from sanic.log import LOGGING_CONFIG_DEFAULTS
from sanic import Sanic
from io import StringIO
import logging
@@ -40,18 +40,34 @@ def test_log():
assert rand_string in log_text
-def test_default_log_fmt():
-
+def test_logging_defaults():
reset_logging()
- Sanic()
- for fmt in [h.formatter for h in logging.getLogger('sanic').handlers]:
- assert fmt._fmt == LOGGING['formatters']['simple']['format']
+ app = Sanic("test_logging")
+ for fmt in [h.formatter for h in logging.getLogger('root').handlers]:
+ assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['generic']['format']
+
+ for fmt in [h.formatter for h in logging.getLogger('sanic.error').handlers]:
+ assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['generic']['format']
+
+ for fmt in [h.formatter for h in logging.getLogger('sanic.access').handlers]:
+ assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['access']['format']
+
+
+def test_logging_pass_customer_logconfig():
reset_logging()
- Sanic(log_config=None)
- for fmt in [h.formatter for h in logging.getLogger('sanic').handlers]:
- assert fmt._fmt == "%(asctime)s: %(levelname)s: %(message)s"
+ modified_config = LOGGING_CONFIG_DEFAULTS
+ modified_config['formatters']['generic']['format'] = '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s'
+ modified_config['formatters']['access']['format'] = '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s'
-if __name__ == "__main__":
- test_log()
+ app = Sanic("test_logging", log_config=modified_config)
+
+ for fmt in [h.formatter for h in logging.getLogger('root').handlers]:
+ assert fmt._fmt == modified_config['formatters']['generic']['format']
+
+ for fmt in [h.formatter for h in logging.getLogger('sanic.error').handlers]:
+ assert fmt._fmt == modified_config['formatters']['generic']['format']
+
+ for fmt in [h.formatter for h in logging.getLogger('sanic.access').handlers]:
+ assert fmt._fmt == modified_config['formatters']['access']['format']
diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py
index 404aec12..a1d8a885 100644
--- a/tests/test_request_timeout.py
+++ b/tests/test_request_timeout.py
@@ -1,38 +1,163 @@
+from json import JSONDecodeError
+
from sanic import Sanic
import asyncio
from sanic.response import text
-from sanic.exceptions import RequestTimeout
from sanic.config import Config
+import aiohttp
+from aiohttp import TCPConnector
+from sanic.testing import SanicTestClient, HOST, PORT
-Config.REQUEST_TIMEOUT = 1
-request_timeout_app = Sanic('test_request_timeout')
+
+class DelayableTCPConnector(TCPConnector):
+
+ class RequestContextManager(object):
+ def __new__(cls, req, delay):
+ cls = super(DelayableTCPConnector.RequestContextManager, cls).\
+ __new__(cls)
+ cls.req = req
+ cls.send_task = None
+ cls.resp = None
+ cls.orig_send = getattr(req, 'send')
+ cls.orig_start = None
+ cls.delay = delay
+ cls._acting_as = req
+ return cls
+
+ def __getattr__(self, item):
+ acting_as = self._acting_as
+ return getattr(acting_as, item)
+
+ @asyncio.coroutine
+ def start(self, connection, read_until_eof=False):
+ if self.send_task is None:
+ raise RuntimeError("do a send() before you do a start()")
+ resp = yield from self.send_task
+ self.send_task = None
+ self.resp = resp
+ self._acting_as = self.resp
+ self.orig_start = getattr(resp, 'start')
+
+ try:
+ ret = yield from self.orig_start(connection,
+ read_until_eof)
+ except Exception as e:
+ raise e
+ return ret
+
+ def close(self):
+ if self.resp is not None:
+ self.resp.close()
+ if self.send_task is not None:
+ self.send_task.cancel()
+
+ @asyncio.coroutine
+ def delayed_send(self, *args, **kwargs):
+ req = self.req
+ if self.delay and self.delay > 0:
+ #sync_sleep(self.delay)
+ _ = yield from asyncio.sleep(self.delay)
+ t = req.loop.time()
+ print("sending at {}".format(t), flush=True)
+ conn = next(iter(args)) # first arg is connection
+ try:
+ delayed_resp = self.orig_send(*args, **kwargs)
+ except Exception as e:
+ return aiohttp.ClientResponse(req.method, req.url)
+ return delayed_resp
+
+ def send(self, *args, **kwargs):
+ gen = self.delayed_send(*args, **kwargs)
+ task = self.req.loop.create_task(gen)
+ self.send_task = task
+ self._acting_as = task
+ return self
+
+ def __init__(self, *args, **kwargs):
+ _post_connect_delay = kwargs.pop('post_connect_delay', 0)
+ _pre_request_delay = kwargs.pop('pre_request_delay', 0)
+ super(DelayableTCPConnector, self).__init__(*args, **kwargs)
+ self._post_connect_delay = _post_connect_delay
+ self._pre_request_delay = _pre_request_delay
+
+ @asyncio.coroutine
+ def connect(self, req):
+ d_req = DelayableTCPConnector.\
+ RequestContextManager(req, self._pre_request_delay)
+ conn = yield from super(DelayableTCPConnector, self).connect(req)
+ if self._post_connect_delay and self._post_connect_delay > 0:
+ _ = yield from asyncio.sleep(self._post_connect_delay,
+ loop=self._loop)
+ req.send = d_req.send
+ t = req.loop.time()
+ print("Connected at {}".format(t), flush=True)
+ return conn
+
+
+class DelayableSanicTestClient(SanicTestClient):
+ def __init__(self, app, loop, request_delay=1):
+ super(DelayableSanicTestClient, self).__init__(app)
+ self._request_delay = request_delay
+ self._loop = None
+
+ async def _local_request(self, method, uri, cookies=None, *args,
+ **kwargs):
+ if self._loop is None:
+ self._loop = asyncio.get_event_loop()
+ if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')):
+ url = uri
+ else:
+ url = 'http://{host}:{port}{uri}'.format(
+ host=HOST, port=PORT, uri=uri)
+ conn = DelayableTCPConnector(pre_request_delay=self._request_delay,
+ verify_ssl=False, loop=self._loop)
+ async with aiohttp.ClientSession(cookies=cookies, connector=conn,
+ loop=self._loop) as session:
+ # Insert a delay after creating the connection
+ # But before sending the request.
+
+ async with getattr(session, method.lower())(
+ url, *args, **kwargs) as response:
+ try:
+ response.text = await response.text()
+ except UnicodeDecodeError:
+ response.text = None
+
+ try:
+ response.json = await response.json()
+ except (JSONDecodeError,
+ UnicodeDecodeError,
+ aiohttp.ClientResponseError):
+ response.json = None
+
+ response.body = await response.read()
+ return response
+
+
+Config.REQUEST_TIMEOUT = 2
request_timeout_default_app = Sanic('test_request_timeout_default')
-
-
-@request_timeout_app.route('/1')
-async def handler_1(request):
- await asyncio.sleep(2)
- return text('OK')
-
-
-@request_timeout_app.exception(RequestTimeout)
-def handler_exception(request, exception):
- return text('Request Timeout from error_handler.', 408)
-
-
-def test_server_error_request_timeout():
- request, response = request_timeout_app.test_client.get('/1')
- assert response.status == 408
- assert response.text == 'Request Timeout from error_handler.'
+request_no_timeout_app = Sanic('test_request_no_timeout')
@request_timeout_default_app.route('/1')
-async def handler_2(request):
- await asyncio.sleep(2)
+async def handler1(request):
+ return text('OK')
+
+
+@request_no_timeout_app.route('/1')
+async def handler2(request):
return text('OK')
def test_default_server_error_request_timeout():
- request, response = request_timeout_default_app.test_client.get('/1')
+ client = DelayableSanicTestClient(request_timeout_default_app, None, 3)
+ request, response = client.get('/1')
assert response.status == 408
assert response.text == 'Error: Request Timeout'
+
+
+def test_default_server_error_request_dont_timeout():
+ client = DelayableSanicTestClient(request_no_timeout_app, None, 1)
+ request, response = client.get('/1')
+ assert response.status == 200
+ assert response.text == 'OK'
diff --git a/tests/test_response_timeout.py b/tests/test_response_timeout.py
new file mode 100644
index 00000000..bf55a42e
--- /dev/null
+++ b/tests/test_response_timeout.py
@@ -0,0 +1,38 @@
+from sanic import Sanic
+import asyncio
+from sanic.response import text
+from sanic.exceptions import ServiceUnavailable
+from sanic.config import Config
+
+Config.RESPONSE_TIMEOUT = 1
+response_timeout_app = Sanic('test_response_timeout')
+response_timeout_default_app = Sanic('test_response_timeout_default')
+
+
+@response_timeout_app.route('/1')
+async def handler_1(request):
+ await asyncio.sleep(2)
+ return text('OK')
+
+
+@response_timeout_app.exception(ServiceUnavailable)
+def handler_exception(request, exception):
+ return text('Response Timeout from error_handler.', 503)
+
+
+def test_server_error_response_timeout():
+ request, response = response_timeout_app.test_client.get('/1')
+ assert response.status == 503
+ assert response.text == 'Response Timeout from error_handler.'
+
+
+@response_timeout_default_app.route('/1')
+async def handler_2(request):
+ await asyncio.sleep(2)
+ return text('OK')
+
+
+def test_default_server_error_response_timeout():
+ request, response = response_timeout_default_app.test_client.get('/1')
+ assert response.status == 503
+ assert response.text == 'Error: Response Timeout'
diff --git a/tests/test_static.py b/tests/test_static.py
index 091d63a4..6252b1c1 100644
--- a/tests/test_static.py
+++ b/tests/test_static.py
@@ -161,3 +161,20 @@ def test_static_content_range_error(file_name, static_file_directory):
assert 'Content-Range' in response.headers
assert response.headers['Content-Range'] == "bytes */%s" % (
len(get_file_content(static_file_directory, file_name)),)
+
+
+@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
+def test_static_file(static_file_directory, file_name):
+ app = Sanic('test_static')
+ app.static(
+ '/testing.file',
+ get_file_path(static_file_directory, file_name),
+ host="www.example.com"
+ )
+
+ headers = {"Host": "www.example.com"}
+ request, response = app.test_client.get('/testing.file', headers=headers)
+ assert response.status == 200
+ assert response.body == get_file_content(static_file_directory, file_name)
+ request, response = app.test_client.get('/testing.file')
+ assert response.status == 404
diff --git a/tests/test_url_building.py b/tests/test_url_building.py
index f234efda..fe31f658 100644
--- a/tests/test_url_building.py
+++ b/tests/test_url_building.py
@@ -17,6 +17,9 @@ URL_FOR_VALUE2 = '/myurl?arg1=v1&arg1=v2#anchor'
URL_FOR_ARGS3 = dict(arg1='v1', _anchor='anchor', _scheme='http',
_server='localhost:{}'.format(test_port), _external=True)
URL_FOR_VALUE3 = 'http://localhost:{}/myurl?arg1=v1#anchor'.format(test_port)
+URL_FOR_ARGS4 = dict(arg1='v1', _anchor='anchor', _external=True,
+ _server='http://localhost:{}'.format(test_port),)
+URL_FOR_VALUE4 = 'http://localhost:{}/myurl?arg1=v1#anchor'.format(test_port)
def _generate_handlers_from_names(app, l):
@@ -49,7 +52,8 @@ def test_simple_url_for_getting(simple_app):
@pytest.mark.parametrize('args,url',
[(URL_FOR_ARGS1, URL_FOR_VALUE1),
(URL_FOR_ARGS2, URL_FOR_VALUE2),
- (URL_FOR_ARGS3, URL_FOR_VALUE3)])
+ (URL_FOR_ARGS3, URL_FOR_VALUE3),
+ (URL_FOR_ARGS4, URL_FOR_VALUE4)])
def test_simple_url_for_getting_with_more_params(args, url):
app = Sanic('more_url_build')
diff --git a/tests/test_url_for_static.py b/tests/test_url_for_static.py
new file mode 100644
index 00000000..d1d8fc9b
--- /dev/null
+++ b/tests/test_url_for_static.py
@@ -0,0 +1,446 @@
+import inspect
+import os
+
+import pytest
+
+from sanic import Sanic
+from sanic.blueprints import Blueprint
+
+
+@pytest.fixture(scope='module')
+def static_file_directory():
+ """The static directory to serve"""
+ current_file = inspect.getfile(inspect.currentframe())
+ current_directory = os.path.dirname(os.path.abspath(current_file))
+ static_directory = os.path.join(current_directory, 'static')
+ return static_directory
+
+
+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('file_name', ['test.file', 'decode me.txt', 'python.png'])
+def test_static_file(static_file_directory, file_name):
+ app = Sanic('test_static')
+ app.static(
+ '/testing.file', get_file_path(static_file_directory, file_name))
+ app.static(
+ '/testing2.file', get_file_path(static_file_directory, file_name),
+ name='testing_file')
+
+ uri = app.url_for('static')
+ uri2 = app.url_for('static', filename='any')
+ uri3 = app.url_for('static', name='static', filename='any')
+
+ assert uri == '/testing.file'
+ assert uri == uri2
+ assert uri2 == uri3
+
+ request, response = app.test_client.get(uri)
+ assert response.status == 200
+ assert response.body == get_file_content(static_file_directory, file_name)
+
+ bp = Blueprint('test_bp_static', url_prefix='/bp')
+
+ bp.static('/testing.file', get_file_path(static_file_directory, file_name))
+ bp.static('/testing2.file',
+ get_file_path(static_file_directory, file_name),
+ name='testing_file')
+
+ app.blueprint(bp)
+
+ uri = app.url_for('static', name='test_bp_static.static')
+ uri2 = app.url_for('static', name='test_bp_static.static', filename='any')
+ uri3 = app.url_for('test_bp_static.static')
+ uri4 = app.url_for('test_bp_static.static', name='any')
+ uri5 = app.url_for('test_bp_static.static', filename='any')
+ uri6 = app.url_for('test_bp_static.static', name='any', filename='any')
+
+ assert uri == '/bp/testing.file'
+ assert uri == uri2
+ assert uri2 == uri3
+ assert uri3 == uri4
+ assert uri4 == uri5
+ assert uri5 == uri6
+
+ request, response = app.test_client.get(uri)
+ assert response.status == 200
+ assert response.body == get_file_content(static_file_directory, file_name)
+
+ # test for other parameters
+ uri = app.url_for('static', _external=True, _server='http://localhost')
+ assert uri == 'http://localhost/testing.file'
+
+ uri = app.url_for('static', name='test_bp_static.static',
+ _external=True, _server='http://localhost')
+ assert uri == 'http://localhost/bp/testing.file'
+
+ # test for defined name
+ uri = app.url_for('static', name='testing_file')
+ assert uri == '/testing2.file'
+
+ request, response = app.test_client.get(uri)
+ assert response.status == 200
+ assert response.body == get_file_content(static_file_directory, file_name)
+
+ uri = app.url_for('static', name='test_bp_static.testing_file')
+ assert uri == '/bp/testing2.file'
+ assert uri == app.url_for('static', name='test_bp_static.testing_file',
+ filename='any')
+
+ request, response = app.test_client.get(uri)
+ assert response.status == 200
+ assert response.body == get_file_content(static_file_directory, file_name)
+
+
+@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):
+
+ app = Sanic('test_static')
+ app.static(base_uri, static_file_directory)
+ base_uri2 = base_uri + '/2'
+ app.static(base_uri2, static_file_directory, name='uploads')
+
+ uri = app.url_for('static', name='static', filename=file_name)
+ assert uri == '{}/{}'.format(base_uri, file_name)
+
+ request, response = app.test_client.get(uri)
+ assert response.status == 200
+ assert response.body == get_file_content(static_file_directory, file_name)
+
+ uri2 = app.url_for('static', name='static', filename='/' + file_name)
+ uri3 = app.url_for('static', filename=file_name)
+ uri4 = app.url_for('static', filename='/' + file_name)
+ uri5 = app.url_for('static', name='uploads', filename=file_name)
+ uri6 = app.url_for('static', name='uploads', filename='/' + file_name)
+
+ assert uri == uri2
+ assert uri2 == uri3
+ assert uri3 == uri4
+
+ assert uri5 == '{}/{}'.format(base_uri2, file_name)
+ assert uri5 == uri6
+
+ bp = Blueprint('test_bp_static', url_prefix='/bp')
+
+ bp.static(base_uri, static_file_directory)
+ bp.static(base_uri2, static_file_directory, name='uploads')
+ app.blueprint(bp)
+
+ uri = app.url_for('static', name='test_bp_static.static',
+ filename=file_name)
+ uri2 = app.url_for('static', name='test_bp_static.static',
+ filename='/' + file_name)
+
+ uri4 = app.url_for('static', name='test_bp_static.uploads',
+ filename=file_name)
+ uri5 = app.url_for('static', name='test_bp_static.uploads',
+ filename='/' + file_name)
+
+ assert uri == '/bp{}/{}'.format(base_uri, file_name)
+ assert uri == uri2
+
+ assert uri4 == '/bp{}/{}'.format(base_uri2, file_name)
+ assert uri4 == uri5
+
+ request, response = app.test_client.get(uri)
+ assert response.status == 200
+ assert response.body == get_file_content(static_file_directory, file_name)
+
+
+
+@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
+def test_static_head_request(file_name, static_file_directory):
+ app = Sanic('test_static')
+ app.static(
+ '/testing.file', get_file_path(static_file_directory, file_name),
+ use_content_range=True)
+
+ bp = Blueprint('test_bp_static', url_prefix='/bp')
+ bp.static('/testing.file', get_file_path(static_file_directory, file_name),
+ use_content_range=True)
+ app.blueprint(bp)
+
+ uri = app.url_for('static')
+ assert uri == '/testing.file'
+ assert uri == app.url_for('static', name='static')
+ assert uri == app.url_for('static', name='static', filename='any')
+
+ request, response = app.test_client.head(uri)
+ assert response.status == 200
+ assert 'Accept-Ranges' in response.headers
+ assert 'Content-Length' in response.headers
+ assert int(response.headers[
+ 'Content-Length']) == len(
+ get_file_content(static_file_directory, file_name))
+
+ # blueprint
+ uri = app.url_for('static', name='test_bp_static.static')
+ assert uri == '/bp/testing.file'
+ assert uri == app.url_for('static', name='test_bp_static.static',
+ filename='any')
+
+ request, response = app.test_client.head(uri)
+ assert response.status == 200
+ assert 'Accept-Ranges' in response.headers
+ assert 'Content-Length' in response.headers
+ assert int(response.headers[
+ 'Content-Length']) == len(
+ get_file_content(static_file_directory, file_name))
+
+
+@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
+def test_static_content_range_correct(file_name, static_file_directory):
+ app = Sanic('test_static')
+ app.static(
+ '/testing.file', get_file_path(static_file_directory, file_name),
+ use_content_range=True)
+
+ bp = Blueprint('test_bp_static', url_prefix='/bp')
+ bp.static('/testing.file', get_file_path(static_file_directory, file_name),
+ use_content_range=True)
+ app.blueprint(bp)
+
+ headers = {
+ 'Range': 'bytes=12-19'
+ }
+ uri = app.url_for('static')
+ assert uri == '/testing.file'
+ assert uri == app.url_for('static', name='static')
+ assert uri == app.url_for('static', name='static', filename='any')
+
+ request, response = app.test_client.get(uri, headers=headers)
+ assert response.status == 200
+ assert 'Content-Length' in response.headers
+ assert 'Content-Range' in response.headers
+ static_content = bytes(get_file_content(
+ static_file_directory, file_name))[12:19]
+ assert int(response.headers[
+ 'Content-Length']) == len(static_content)
+ assert response.body == static_content
+
+ # blueprint
+ uri = app.url_for('static', name='test_bp_static.static')
+ assert uri == '/bp/testing.file'
+ assert uri == app.url_for('static', name='test_bp_static.static',
+ filename='any')
+ assert uri == app.url_for('test_bp_static.static')
+ assert uri == app.url_for('test_bp_static.static', name='any')
+ assert uri == app.url_for('test_bp_static.static', filename='any')
+ assert uri == app.url_for('test_bp_static.static', name='any',
+ filename='any')
+
+ request, response = app.test_client.get(uri, headers=headers)
+ assert response.status == 200
+ assert 'Content-Length' in response.headers
+ assert 'Content-Range' in response.headers
+ static_content = bytes(get_file_content(
+ static_file_directory, file_name))[12:19]
+ assert int(response.headers[
+ 'Content-Length']) == len(static_content)
+ assert response.body == static_content
+
+
+@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
+def test_static_content_range_front(file_name, static_file_directory):
+ app = Sanic('test_static')
+ app.static(
+ '/testing.file', get_file_path(static_file_directory, file_name),
+ use_content_range=True)
+
+ bp = Blueprint('test_bp_static', url_prefix='/bp')
+ bp.static('/testing.file', get_file_path(static_file_directory, file_name),
+ use_content_range=True)
+ app.blueprint(bp)
+
+ headers = {
+ 'Range': 'bytes=12-'
+ }
+ uri = app.url_for('static')
+ assert uri == '/testing.file'
+ assert uri == app.url_for('static', name='static')
+ assert uri == app.url_for('static', name='static', filename='any')
+
+ request, response = app.test_client.get(uri, headers=headers)
+ assert response.status == 200
+ assert 'Content-Length' in response.headers
+ assert 'Content-Range' in response.headers
+ static_content = bytes(get_file_content(
+ static_file_directory, file_name))[12:]
+ assert int(response.headers[
+ 'Content-Length']) == len(static_content)
+ assert response.body == static_content
+
+ # blueprint
+ uri = app.url_for('static', name='test_bp_static.static')
+ assert uri == '/bp/testing.file'
+ assert uri == app.url_for('static', name='test_bp_static.static',
+ filename='any')
+ assert uri == app.url_for('test_bp_static.static')
+ assert uri == app.url_for('test_bp_static.static', name='any')
+ assert uri == app.url_for('test_bp_static.static', filename='any')
+ assert uri == app.url_for('test_bp_static.static', name='any',
+ filename='any')
+
+ request, response = app.test_client.get(uri, headers=headers)
+ assert response.status == 200
+ assert 'Content-Length' in response.headers
+ assert 'Content-Range' in response.headers
+ static_content = bytes(get_file_content(
+ static_file_directory, file_name))[12:]
+ assert int(response.headers[
+ 'Content-Length']) == len(static_content)
+ assert response.body == static_content
+
+
+@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
+def test_static_content_range_back(file_name, static_file_directory):
+ app = Sanic('test_static')
+ app.static(
+ '/testing.file', get_file_path(static_file_directory, file_name),
+ use_content_range=True)
+
+ bp = Blueprint('test_bp_static', url_prefix='/bp')
+ bp.static('/testing.file', get_file_path(static_file_directory, file_name),
+ use_content_range=True)
+ app.blueprint(bp)
+
+ headers = {
+ 'Range': 'bytes=-12'
+ }
+ uri = app.url_for('static')
+ assert uri == '/testing.file'
+ assert uri == app.url_for('static', name='static')
+ assert uri == app.url_for('static', name='static', filename='any')
+
+ request, response = app.test_client.get(uri, headers=headers)
+ assert response.status == 200
+ assert 'Content-Length' in response.headers
+ assert 'Content-Range' in response.headers
+ static_content = bytes(get_file_content(
+ static_file_directory, file_name))[-12:]
+ assert int(response.headers[
+ 'Content-Length']) == len(static_content)
+ assert response.body == static_content
+
+ # blueprint
+ uri = app.url_for('static', name='test_bp_static.static')
+ assert uri == '/bp/testing.file'
+ assert uri == app.url_for('static', name='test_bp_static.static',
+ filename='any')
+ assert uri == app.url_for('test_bp_static.static')
+ assert uri == app.url_for('test_bp_static.static', name='any')
+ assert uri == app.url_for('test_bp_static.static', filename='any')
+ assert uri == app.url_for('test_bp_static.static', name='any',
+ filename='any')
+
+ request, response = app.test_client.get(uri, headers=headers)
+ assert response.status == 200
+ assert 'Content-Length' in response.headers
+ assert 'Content-Range' in response.headers
+ static_content = bytes(get_file_content(
+ static_file_directory, file_name))[-12:]
+ assert int(response.headers[
+ 'Content-Length']) == len(static_content)
+ assert response.body == static_content
+
+
+@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
+def test_static_content_range_empty(file_name, static_file_directory):
+ app = Sanic('test_static')
+ app.static(
+ '/testing.file', get_file_path(static_file_directory, file_name),
+ use_content_range=True)
+
+ bp = Blueprint('test_bp_static', url_prefix='/bp')
+ bp.static('/testing.file', get_file_path(static_file_directory, file_name),
+ use_content_range=True)
+ app.blueprint(bp)
+
+ uri = app.url_for('static')
+ assert uri == '/testing.file'
+ assert uri == app.url_for('static', name='static')
+ assert uri == app.url_for('static', name='static', filename='any')
+
+ request, response = app.test_client.get(uri)
+ assert response.status == 200
+ assert 'Content-Length' in response.headers
+ assert 'Content-Range' not in response.headers
+ assert int(response.headers[
+ 'Content-Length']) == len(get_file_content(static_file_directory, file_name))
+ assert response.body == bytes(
+ get_file_content(static_file_directory, file_name))
+
+ # blueprint
+ uri = app.url_for('static', name='test_bp_static.static')
+ assert uri == '/bp/testing.file'
+ assert uri == app.url_for('static', name='test_bp_static.static',
+ filename='any')
+ assert uri == app.url_for('test_bp_static.static')
+ assert uri == app.url_for('test_bp_static.static', name='any')
+ assert uri == app.url_for('test_bp_static.static', filename='any')
+ assert uri == app.url_for('test_bp_static.static', name='any',
+ filename='any')
+
+ request, response = app.test_client.get(uri)
+ assert response.status == 200
+ assert 'Content-Length' in response.headers
+ assert 'Content-Range' not in response.headers
+ assert int(response.headers[
+ 'Content-Length']) == len(get_file_content(static_file_directory, file_name))
+ assert response.body == bytes(
+ get_file_content(static_file_directory, file_name))
+
+
+@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
+def test_static_content_range_error(file_name, static_file_directory):
+ app = Sanic('test_static')
+ app.static(
+ '/testing.file', get_file_path(static_file_directory, file_name),
+ use_content_range=True)
+
+ bp = Blueprint('test_bp_static', url_prefix='/bp')
+ bp.static('/testing.file', get_file_path(static_file_directory, file_name),
+ use_content_range=True)
+ app.blueprint(bp)
+
+ headers = {
+ 'Range': 'bytes=1-0'
+ }
+ uri = app.url_for('static')
+ assert uri == '/testing.file'
+ assert uri == app.url_for('static', name='static')
+ assert uri == app.url_for('static', name='static', filename='any')
+
+ request, response = app.test_client.get(uri, headers=headers)
+ assert response.status == 416
+ assert 'Content-Length' in response.headers
+ assert 'Content-Range' in response.headers
+ assert response.headers['Content-Range'] == "bytes */%s" % (
+ len(get_file_content(static_file_directory, file_name)),)
+
+ # blueprint
+ uri = app.url_for('static', name='test_bp_static.static')
+ assert uri == '/bp/testing.file'
+ assert uri == app.url_for('static', name='test_bp_static.static',
+ filename='any')
+ assert uri == app.url_for('test_bp_static.static')
+ assert uri == app.url_for('test_bp_static.static', name='any')
+ assert uri == app.url_for('test_bp_static.static', filename='any')
+ assert uri == app.url_for('test_bp_static.static', name='any',
+ filename='any')
+
+ request, response = app.test_client.get(uri, headers=headers)
+ assert response.status == 416
+ assert 'Content-Length' in response.headers
+ assert 'Content-Range' in response.headers
+ assert response.headers['Content-Range'] == "bytes */%s" % (
+ len(get_file_content(static_file_directory, file_name)),)