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)),)