Merge pull request #14 from channelcat/master
merge upstream master branch
This commit is contained in:
commit
9bc1abcd00
|
@ -25,7 +25,7 @@ import sanic
|
||||||
|
|
||||||
# -- General configuration ------------------------------------------------
|
# -- General configuration ------------------------------------------------
|
||||||
|
|
||||||
extensions = ['sphinx.ext.autodoc']
|
extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.asyncio']
|
||||||
|
|
||||||
templates_path = ['_templates']
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
|
|
@ -93,7 +93,14 @@ def ignore_404s(request, exception):
|
||||||
Static files can be served globally, under the blueprint prefix.
|
Static files can be served globally, under the blueprint prefix.
|
||||||
|
|
||||||
```python
|
```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
|
## Start and stop
|
||||||
|
|
|
@ -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.
|
- [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.
|
- [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.
|
- [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.
|
- [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI.
|
||||||
- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support.
|
- [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support.
|
||||||
- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper.
|
- [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper.
|
||||||
|
|
|
@ -9,15 +9,16 @@ syntax, so earlier versions of python won't work.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from sanic.response import text
|
from sanic.response import json
|
||||||
|
|
||||||
app = Sanic(__name__)
|
app = Sanic()
|
||||||
|
|
||||||
@app.route("/")
|
@app.route("/")
|
||||||
async def test(request):
|
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`
|
3. Run the server: `python3 main.py`
|
||||||
|
|
|
@ -9,12 +9,6 @@ A simple example using default settings would be like this:
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sanic import Sanic
|
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')
|
app = Sanic('test')
|
||||||
|
|
||||||
|
@ -23,14 +17,21 @@ async def test(request):
|
||||||
return response.text('Hello World!')
|
return response.text('Hello World!')
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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
|
```python
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app.run(log_config=None)
|
app.run(access_log=False)
|
||||||
```
|
```
|
||||||
|
|
||||||
This would skip calling logging functions when handling requests.
|
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
|
```python
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
# disable internal messages
|
# disable debug messages
|
||||||
app.run(debug=False, log_config=None)
|
app.run(debug=False, access_log=False)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Configuration
|
### 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))<br>
|
There are three `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**:
|
||||||
For internal information console outputs.
|
|
||||||
|
|
||||||
|
- root:<br>
|
||||||
- accessStream (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))<br>
|
|
||||||
For requests information logging in console
|
|
||||||
|
|
||||||
|
|
||||||
- errorStream (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))<br>
|
|
||||||
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))<br>
|
|
||||||
For requests information logging to syslog.
|
|
||||||
Currently supports Windows (via localhost:514), Darwin (/var/run/syslog),
|
|
||||||
Linux (/dev/log) and FreeBSD (/dev/log).<br>
|
|
||||||
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))<br>
|
|
||||||
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).<br>
|
|
||||||
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)<br>
|
|
||||||
The filter that allows only levels in `DEBUG`, `INFO`, and `NONE(0)`
|
|
||||||
|
|
||||||
|
|
||||||
- errorFilter (using sanic.log.DefaultFilter)<br>
|
|
||||||
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:<br>
|
|
||||||
Used to log internal messages.
|
Used to log internal messages.
|
||||||
|
|
||||||
|
- sanic.error:<br>
|
||||||
|
Used to log error logs.
|
||||||
|
|
||||||
- network:<br>
|
- sanic.access:<br>
|
||||||
Used to log requests from network, and any information from those requests.
|
Used to log access logs.
|
||||||
|
|
||||||
#### Log format:
|
#### Log format:
|
||||||
|
|
||||||
In addition to default parameters provided by python (asctime, levelname, message),
|
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)<br>
|
- host (str)<br>
|
||||||
request.ip
|
request.ip
|
||||||
|
|
|
@ -301,3 +301,34 @@ def handler(request):
|
||||||
# app.url_for('handler') == '/get'
|
# app.url_for('handler') == '/get'
|
||||||
# app.url_for('post_handler') == '/post'
|
# 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'
|
||||||
|
|
||||||
|
```
|
||||||
|
|
|
@ -6,16 +6,40 @@ filename. The file specified will then be accessible via the given endpoint.
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
|
from sanic.blueprints import Blueprint
|
||||||
|
|
||||||
app = Sanic(__name__)
|
app = Sanic(__name__)
|
||||||
|
|
||||||
# Serves files from the static folder to the URL /static
|
# Serves files from the static folder to the URL /static
|
||||||
app.static('/static', './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
|
# Serves the file /home/ubuntu/test.png when the URL /the_best.png
|
||||||
# is requested
|
# 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)
|
app.run(host="0.0.0.0", port=8000)
|
||||||
```
|
```
|
||||||
|
|
||||||
Note: currently you cannot build a URL for a static file using `url_for`.
|
|
||||||
|
|
13
examples/teapot.py
Normal file
13
examples/teapot.py
Normal file
|
@ -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)
|
|
@ -1,3 +1,4 @@
|
||||||
sphinx
|
sphinx
|
||||||
sphinx_rtd_theme
|
sphinx_rtd_theme
|
||||||
recommonmark
|
recommonmark
|
||||||
|
sphinxcontrib-asyncio
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
from argparse import ArgumentParser
|
from argparse import ArgumentParser
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
|
||||||
from sanic.log import log
|
from sanic.log import logger
|
||||||
from sanic.app import Sanic
|
from sanic.app import Sanic
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
@ -36,9 +36,9 @@ if __name__ == "__main__":
|
||||||
app.run(host=args.host, port=args.port,
|
app.run(host=args.host, port=args.port,
|
||||||
workers=args.workers, debug=args.debug, ssl=ssl)
|
workers=args.workers, debug=args.debug, ssl=ssl)
|
||||||
except ImportError as e:
|
except ImportError as e:
|
||||||
log.error("No module named {} found.\n"
|
logger.error("No module named {} found.\n"
|
||||||
" Example File: project/sanic_server.py -> app\n"
|
" Example File: project/sanic_server.py -> app\n"
|
||||||
" Example Module: project.sanic_server.app"
|
" Example Module: project.sanic_server.app"
|
||||||
.format(e.name))
|
.format(e.name))
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
log.error("{}".format(e))
|
logger.error("{}".format(e))
|
||||||
|
|
92
sanic/app.py
92
sanic/app.py
|
@ -10,11 +10,11 @@ from traceback import format_exc
|
||||||
from urllib.parse import urlencode, urlunparse
|
from urllib.parse import urlencode, urlunparse
|
||||||
from ssl import create_default_context, Purpose
|
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.constants import HTTP_METHODS
|
||||||
from sanic.exceptions import ServerError, URLBuildError, SanicException
|
from sanic.exceptions import ServerError, URLBuildError, SanicException
|
||||||
from sanic.handlers import ErrorHandler
|
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.response import HTTPResponse, StreamingHTTPResponse
|
||||||
from sanic.router import Router
|
from sanic.router import Router
|
||||||
from sanic.server import serve, serve_multiple, HttpProtocol, Signal
|
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,
|
def __init__(self, name=None, router=None, error_handler=None,
|
||||||
load_env=True, request_class=None,
|
load_env=True, request_class=None,
|
||||||
log_config=LOGGING, strict_slashes=False):
|
strict_slashes=False, log_config=None):
|
||||||
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)
|
|
||||||
|
|
||||||
# Get name from previous stack frame
|
# Get name from previous stack frame
|
||||||
if name is None:
|
if name is None:
|
||||||
frame_records = stack()[1]
|
frame_records = stack()[1]
|
||||||
name = getmodulename(frame_records[1])
|
name = getmodulename(frame_records[1])
|
||||||
|
|
||||||
|
# logging
|
||||||
|
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS)
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.router = router or Router()
|
self.router = router or Router()
|
||||||
self.request_class = request_class
|
self.request_class = request_class
|
||||||
self.error_handler = error_handler or ErrorHandler()
|
self.error_handler = error_handler or ErrorHandler()
|
||||||
self.config = Config(load_env=load_env)
|
self.config = Config(load_env=load_env)
|
||||||
self.log_config = log_config
|
|
||||||
self.request_middleware = deque()
|
self.request_middleware = deque()
|
||||||
self.response_middleware = deque()
|
self.response_middleware = deque()
|
||||||
self.blueprints = {}
|
self.blueprints = {}
|
||||||
|
@ -354,13 +345,13 @@ class Sanic:
|
||||||
# Static Files
|
# Static Files
|
||||||
def static(self, uri, file_or_directory, pattern=r'/?.+',
|
def static(self, uri, file_or_directory, pattern=r'/?.+',
|
||||||
use_modified_since=True, use_content_range=False,
|
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
|
"""Register a root to serve files from. The input can either be a
|
||||||
file or a directory. See
|
file or a directory. See
|
||||||
"""
|
"""
|
||||||
static_register(self, uri, file_or_directory, pattern,
|
static_register(self, uri, file_or_directory, pattern,
|
||||||
use_modified_since, use_content_range,
|
use_modified_since, use_content_range,
|
||||||
stream_large_files)
|
stream_large_files, name, host)
|
||||||
|
|
||||||
def blueprint(self, blueprint, **options):
|
def blueprint(self, blueprint, **options):
|
||||||
"""Register a blueprint on the application.
|
"""Register a blueprint on the application.
|
||||||
|
@ -410,12 +401,32 @@ class Sanic:
|
||||||
URLBuildError
|
URLBuildError
|
||||||
"""
|
"""
|
||||||
# find the route by the supplied view name
|
# 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(
|
raise URLBuildError('Endpoint with name `{}` was not found'.format(
|
||||||
view_name))
|
view_name))
|
||||||
|
|
||||||
|
if view_name == 'static' or view_name.endswith('.static'):
|
||||||
|
filename = kwargs.pop('filename', None)
|
||||||
|
# it's static folder
|
||||||
|
if '<file_uri:' in uri:
|
||||||
|
folder_ = uri.split('<file_uri:', 1)[0]
|
||||||
|
if folder_.endswith('/'):
|
||||||
|
folder_ = folder_[:-1]
|
||||||
|
|
||||||
|
if filename.startswith('/'):
|
||||||
|
filename = filename[1:]
|
||||||
|
|
||||||
|
uri = '{}/{}'.format(folder_, filename)
|
||||||
|
|
||||||
if uri != '/' and uri.endswith('/'):
|
if uri != '/' and uri.endswith('/'):
|
||||||
uri = uri[:-1]
|
uri = uri[:-1]
|
||||||
|
|
||||||
|
@ -438,6 +449,16 @@ class Sanic:
|
||||||
if netloc is None and external:
|
if netloc is None and external:
|
||||||
netloc = self.config.get('SERVER_NAME', '')
|
netloc = self.config.get('SERVER_NAME', '')
|
||||||
|
|
||||||
|
if external:
|
||||||
|
if not scheme:
|
||||||
|
if ':' in netloc[:8]:
|
||||||
|
scheme = netloc[:8].split(':', 1)[0]
|
||||||
|
else:
|
||||||
|
scheme = 'http'
|
||||||
|
|
||||||
|
if '://' in netloc[:8]:
|
||||||
|
netloc = netloc.split('://', 1)[-1]
|
||||||
|
|
||||||
for match in matched_params:
|
for match in matched_params:
|
||||||
name, _type, pattern = self.router.parse_parameter_string(
|
name, _type, pattern = self.router.parse_parameter_string(
|
||||||
match)
|
match)
|
||||||
|
@ -554,7 +575,7 @@ class Sanic:
|
||||||
response = await self._run_response_middleware(request,
|
response = await self._run_response_middleware(request,
|
||||||
response)
|
response)
|
||||||
except:
|
except:
|
||||||
log.exception(
|
error_logger.exception(
|
||||||
'Exception occured in one of response middleware handlers'
|
'Exception occured in one of response middleware handlers'
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -579,7 +600,7 @@ class Sanic:
|
||||||
def run(self, host=None, port=None, debug=False, ssl=None,
|
def run(self, host=None, port=None, debug=False, ssl=None,
|
||||||
sock=None, workers=1, protocol=None,
|
sock=None, workers=1, protocol=None,
|
||||||
backlog=100, stop_event=None, register_sys_signals=True,
|
backlog=100, stop_event=None, register_sys_signals=True,
|
||||||
log_config=None):
|
access_log=True):
|
||||||
"""Run the HTTP Server and listen until keyboard interrupt or term
|
"""Run the HTTP Server and listen until keyboard interrupt or term
|
||||||
signal. On termination, drain connections before closing.
|
signal. On termination, drain connections before closing.
|
||||||
|
|
||||||
|
@ -597,12 +618,10 @@ class Sanic:
|
||||||
:param protocol: Subclass of asyncio protocol class
|
:param protocol: Subclass of asyncio protocol class
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if sock is None:
|
if sock is None:
|
||||||
host, port = host or "127.0.0.1", port or 8000
|
host, port = host or "127.0.0.1", port or 8000
|
||||||
|
|
||||||
if log_config:
|
|
||||||
self.log_config = log_config
|
|
||||||
logging.config.dictConfig(log_config)
|
|
||||||
if protocol is None:
|
if protocol is None:
|
||||||
protocol = (WebSocketProtocol if self.websocket_enabled
|
protocol = (WebSocketProtocol if self.websocket_enabled
|
||||||
else HttpProtocol)
|
else HttpProtocol)
|
||||||
|
@ -615,7 +634,7 @@ class Sanic:
|
||||||
host=host, port=port, debug=debug, ssl=ssl, sock=sock,
|
host=host, port=port, debug=debug, ssl=ssl, sock=sock,
|
||||||
workers=workers, protocol=protocol, backlog=backlog,
|
workers=workers, protocol=protocol, backlog=backlog,
|
||||||
register_sys_signals=register_sys_signals,
|
register_sys_signals=register_sys_signals,
|
||||||
has_log=self.log_config is not None)
|
access_log=access_log)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.is_running = True
|
self.is_running = True
|
||||||
|
@ -624,12 +643,12 @@ class Sanic:
|
||||||
else:
|
else:
|
||||||
serve_multiple(server_settings, workers)
|
serve_multiple(server_settings, workers)
|
||||||
except:
|
except:
|
||||||
log.exception(
|
error_logger.exception(
|
||||||
'Experienced exception while trying to serve')
|
'Experienced exception while trying to serve')
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
log.info("Server Stopped")
|
logger.info("Server Stopped")
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
"""This kills the Sanic"""
|
"""This kills the Sanic"""
|
||||||
|
@ -642,17 +661,16 @@ class Sanic:
|
||||||
async def create_server(self, host=None, port=None, debug=False,
|
async def create_server(self, host=None, port=None, debug=False,
|
||||||
ssl=None, sock=None, protocol=None,
|
ssl=None, sock=None, protocol=None,
|
||||||
backlog=100, stop_event=None,
|
backlog=100, stop_event=None,
|
||||||
log_config=LOGGING):
|
access_log=True):
|
||||||
"""Asynchronous version of `run`.
|
"""Asynchronous version of `run`.
|
||||||
|
|
||||||
NOTE: This does not support multiprocessing and is not the preferred
|
NOTE: This does not support multiprocessing and is not the preferred
|
||||||
way to run a Sanic application.
|
way to run a Sanic application.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if sock is None:
|
if sock is None:
|
||||||
host, port = host or "127.0.0.1", port or 8000
|
host, port = host or "127.0.0.1", port or 8000
|
||||||
|
|
||||||
if log_config:
|
|
||||||
logging.config.dictConfig(log_config)
|
|
||||||
if protocol is None:
|
if protocol is None:
|
||||||
protocol = (WebSocketProtocol if self.websocket_enabled
|
protocol = (WebSocketProtocol if self.websocket_enabled
|
||||||
else HttpProtocol)
|
else HttpProtocol)
|
||||||
|
@ -666,7 +684,7 @@ class Sanic:
|
||||||
host=host, port=port, debug=debug, ssl=ssl, sock=sock,
|
host=host, port=port, debug=debug, ssl=ssl, sock=sock,
|
||||||
loop=get_event_loop(), protocol=protocol,
|
loop=get_event_loop(), protocol=protocol,
|
||||||
backlog=backlog, run_async=True,
|
backlog=backlog, run_async=True,
|
||||||
has_log=log_config is not None)
|
access_log=access_log)
|
||||||
|
|
||||||
# Trigger before_start events
|
# Trigger before_start events
|
||||||
await self.trigger_events(
|
await self.trigger_events(
|
||||||
|
@ -711,7 +729,7 @@ class Sanic:
|
||||||
def _helper(self, host=None, port=None, debug=False,
|
def _helper(self, host=None, port=None, debug=False,
|
||||||
ssl=None, sock=None, workers=1, loop=None,
|
ssl=None, sock=None, workers=1, loop=None,
|
||||||
protocol=HttpProtocol, backlog=100, stop_event=None,
|
protocol=HttpProtocol, backlog=100, stop_event=None,
|
||||||
register_sys_signals=True, run_async=False, has_log=True):
|
register_sys_signals=True, run_async=False, access_log=True):
|
||||||
"""Helper function used by `run` and `create_server`."""
|
"""Helper function used by `run` and `create_server`."""
|
||||||
if isinstance(ssl, dict):
|
if isinstance(ssl, dict):
|
||||||
# try common aliaseses
|
# try common aliaseses
|
||||||
|
@ -745,12 +763,14 @@ class Sanic:
|
||||||
'request_handler': self.handle_request,
|
'request_handler': self.handle_request,
|
||||||
'error_handler': self.error_handler,
|
'error_handler': self.error_handler,
|
||||||
'request_timeout': self.config.REQUEST_TIMEOUT,
|
'request_timeout': self.config.REQUEST_TIMEOUT,
|
||||||
|
'response_timeout': self.config.RESPONSE_TIMEOUT,
|
||||||
|
'keep_alive_timeout': self.config.KEEP_ALIVE_TIMEOUT,
|
||||||
'request_max_size': self.config.REQUEST_MAX_SIZE,
|
'request_max_size': self.config.REQUEST_MAX_SIZE,
|
||||||
'keep_alive': self.config.KEEP_ALIVE,
|
'keep_alive': self.config.KEEP_ALIVE,
|
||||||
'loop': loop,
|
'loop': loop,
|
||||||
'register_sys_signals': register_sys_signals,
|
'register_sys_signals': register_sys_signals,
|
||||||
'backlog': backlog,
|
'backlog': backlog,
|
||||||
'has_log': has_log,
|
'access_log': access_log,
|
||||||
'websocket_max_size': self.config.WEBSOCKET_MAX_SIZE,
|
'websocket_max_size': self.config.WEBSOCKET_MAX_SIZE,
|
||||||
'websocket_max_queue': self.config.WEBSOCKET_MAX_QUEUE,
|
'websocket_max_queue': self.config.WEBSOCKET_MAX_QUEUE,
|
||||||
'graceful_shutdown_timeout': self.config.GRACEFUL_SHUTDOWN_TIMEOUT
|
'graceful_shutdown_timeout': self.config.GRACEFUL_SHUTDOWN_TIMEOUT
|
||||||
|
@ -774,9 +794,9 @@ class Sanic:
|
||||||
server_settings[settings_name] = listeners
|
server_settings[settings_name] = listeners
|
||||||
|
|
||||||
if debug:
|
if debug:
|
||||||
log.setLevel(logging.DEBUG)
|
logger.setLevel(logging.DEBUG)
|
||||||
if self.config.LOGO is not None:
|
if self.config.LOGO is not None:
|
||||||
log.debug(self.config.LOGO)
|
logger.debug(self.config.LOGO)
|
||||||
|
|
||||||
if run_async:
|
if run_async:
|
||||||
server_settings['run_async'] = True
|
server_settings['run_async'] = True
|
||||||
|
@ -786,6 +806,6 @@ class Sanic:
|
||||||
proto = "http"
|
proto = "http"
|
||||||
if ssl is not None:
|
if ssl is not None:
|
||||||
proto = "https"
|
proto = "https"
|
||||||
log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
logger.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port))
|
||||||
|
|
||||||
return server_settings
|
return server_settings
|
||||||
|
|
|
@ -218,6 +218,11 @@ class Blueprint:
|
||||||
:param uri: endpoint at which the route will be accessible.
|
:param uri: endpoint at which the route will be accessible.
|
||||||
:param file_or_directory: Static asset.
|
:param file_or_directory: Static asset.
|
||||||
"""
|
"""
|
||||||
|
name = kwargs.pop('name', 'static')
|
||||||
|
if not name.startswith(self.name + '.'):
|
||||||
|
name = '{}.{}'.format(self.name, name)
|
||||||
|
|
||||||
|
kwargs.update(name=name)
|
||||||
static = FutureStatic(uri, file_or_directory, args, kwargs)
|
static = FutureStatic(uri, file_or_directory, args, kwargs)
|
||||||
self.statics.append(static)
|
self.statics.append(static)
|
||||||
|
|
||||||
|
|
102
sanic/config.py
102
sanic/config.py
|
@ -1,103 +1,9 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
|
||||||
import syslog
|
|
||||||
import platform
|
|
||||||
import types
|
import types
|
||||||
|
|
||||||
from sanic.log import DefaultFilter
|
|
||||||
|
|
||||||
SANIC_PREFIX = 'SANIC_'
|
SANIC_PREFIX = 'SANIC_'
|
||||||
|
|
||||||
_address_dict = {
|
|
||||||
'Windows': ('localhost', 514),
|
|
||||||
'Darwin': '/var/run/syslog',
|
|
||||||
'Linux': '/dev/log',
|
|
||||||
'FreeBSD': '/var/run/log'
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGGING = {
|
|
||||||
'version': 1,
|
|
||||||
'disable_existing_loggers': False,
|
|
||||||
'filters': {
|
|
||||||
'accessFilter': {
|
|
||||||
'()': DefaultFilter,
|
|
||||||
'param': [0, 10, 20]
|
|
||||||
},
|
|
||||||
'errorFilter': {
|
|
||||||
'()': DefaultFilter,
|
|
||||||
'param': [30, 40, 50]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'formatters': {
|
|
||||||
'simple': {
|
|
||||||
'format': '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s',
|
|
||||||
'datefmt': '%Y-%m-%d %H:%M:%S'
|
|
||||||
},
|
|
||||||
'access': {
|
|
||||||
'format': '%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: ' +
|
|
||||||
'%(request)s %(message)s %(status)d %(byte)d',
|
|
||||||
'datefmt': '%Y-%m-%d %H:%M:%S'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
'handlers': {
|
|
||||||
'internal': {
|
|
||||||
'class': 'logging.StreamHandler',
|
|
||||||
'filters': ['accessFilter'],
|
|
||||||
'formatter': 'simple',
|
|
||||||
'stream': sys.stderr
|
|
||||||
},
|
|
||||||
'accessStream': {
|
|
||||||
'class': 'logging.StreamHandler',
|
|
||||||
'filters': ['accessFilter'],
|
|
||||||
'formatter': 'access',
|
|
||||||
'stream': sys.stderr
|
|
||||||
},
|
|
||||||
'errorStream': {
|
|
||||||
'class': 'logging.StreamHandler',
|
|
||||||
'filters': ['errorFilter'],
|
|
||||||
'formatter': 'simple',
|
|
||||||
'stream': sys.stderr
|
|
||||||
},
|
|
||||||
# before you use accessSysLog, be sure that log levels
|
|
||||||
# 0, 10, 20 have been enabled in you syslog configuration
|
|
||||||
# otherwise you won't be able to see the output in syslog
|
|
||||||
# logging file.
|
|
||||||
'accessSysLog': {
|
|
||||||
'class': 'logging.handlers.SysLogHandler',
|
|
||||||
'address': _address_dict.get(platform.system(),
|
|
||||||
('localhost', 514)),
|
|
||||||
'facility': syslog.LOG_DAEMON,
|
|
||||||
'filters': ['accessFilter'],
|
|
||||||
'formatter': 'access'
|
|
||||||
},
|
|
||||||
'errorSysLog': {
|
|
||||||
'class': 'logging.handlers.SysLogHandler',
|
|
||||||
'address': _address_dict.get(platform.system(),
|
|
||||||
('localhost', 514)),
|
|
||||||
'facility': syslog.LOG_DAEMON,
|
|
||||||
'filters': ['errorFilter'],
|
|
||||||
'formatter': 'simple'
|
|
||||||
},
|
|
||||||
},
|
|
||||||
'loggers': {
|
|
||||||
'sanic': {
|
|
||||||
'level': 'INFO',
|
|
||||||
'handlers': ['internal', 'errorStream']
|
|
||||||
},
|
|
||||||
'network': {
|
|
||||||
'level': 'INFO',
|
|
||||||
'handlers': ['accessStream', 'errorStream']
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# this happens when using container or systems without syslog
|
|
||||||
# keep things in config would cause file not exists error
|
|
||||||
_addr = LOGGING['handlers']['accessSysLog']['address']
|
|
||||||
if type(_addr) is str and not os.path.exists(_addr):
|
|
||||||
LOGGING['handlers'].pop('accessSysLog')
|
|
||||||
LOGGING['handlers'].pop('errorSysLog')
|
|
||||||
|
|
||||||
|
|
||||||
class Config(dict):
|
class Config(dict):
|
||||||
def __init__(self, defaults=None, load_env=True, keep_alive=True):
|
def __init__(self, defaults=None, load_env=True, keep_alive=True):
|
||||||
|
@ -125,7 +31,15 @@ class Config(dict):
|
||||||
"""
|
"""
|
||||||
self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes
|
self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes
|
||||||
self.REQUEST_TIMEOUT = 60 # 60 seconds
|
self.REQUEST_TIMEOUT = 60 # 60 seconds
|
||||||
|
self.RESPONSE_TIMEOUT = 60 # 60 seconds
|
||||||
self.KEEP_ALIVE = keep_alive
|
self.KEEP_ALIVE = keep_alive
|
||||||
|
# Apache httpd server default keepalive timeout = 5 seconds
|
||||||
|
# Nginx server default keepalive timeout = 75 seconds
|
||||||
|
# Nginx performance tuning guidelines uses keepalive = 15 seconds
|
||||||
|
# IE client hard keepalive limit = 60 seconds
|
||||||
|
# Firefox client hard keepalive limit = 115 seconds
|
||||||
|
|
||||||
|
self.KEEP_ALIVE_TIMEOUT = 5 # 5 seconds
|
||||||
self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes
|
self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes
|
||||||
self.WEBSOCKET_MAX_QUEUE = 32
|
self.WEBSOCKET_MAX_QUEUE = 32
|
||||||
self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec
|
self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec
|
||||||
|
|
|
@ -98,6 +98,7 @@ class Cookie(dict):
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
if key not in self._keys:
|
if key not in self._keys:
|
||||||
raise KeyError("Unknown cookie property")
|
raise KeyError("Unknown cookie property")
|
||||||
|
if value is not False:
|
||||||
return super().__setitem__(key, value)
|
return super().__setitem__(key, value)
|
||||||
|
|
||||||
def encode(self, encoding):
|
def encode(self, encoding):
|
||||||
|
|
|
@ -155,6 +155,13 @@ class ServerError(SanicException):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@add_status_code(503)
|
||||||
|
class ServiceUnavailable(SanicException):
|
||||||
|
"""The server is currently unavailable (because it is overloaded or
|
||||||
|
down for maintenance). Generally, this is a temporary state."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class URLBuildError(ServerError):
|
class URLBuildError(ServerError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -170,6 +177,13 @@ class FileNotFound(NotFound):
|
||||||
|
|
||||||
@add_status_code(408)
|
@add_status_code(408)
|
||||||
class RequestTimeout(SanicException):
|
class RequestTimeout(SanicException):
|
||||||
|
"""The Web server (running the Web site) thinks that there has been too
|
||||||
|
long an interval of time between 1) the establishment of an IP
|
||||||
|
connection (socket) between the client and the server and
|
||||||
|
2) the receipt of any data on that socket, so the server has dropped
|
||||||
|
the connection. The socket connection has actually been lost - the Web
|
||||||
|
server has 'timed out' on that particular socket connection.
|
||||||
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,7 @@ from sanic.exceptions import (
|
||||||
TRACEBACK_WRAPPER_HTML,
|
TRACEBACK_WRAPPER_HTML,
|
||||||
TRACEBACK_WRAPPER_INNER_HTML,
|
TRACEBACK_WRAPPER_INNER_HTML,
|
||||||
TRACEBACK_BORDER)
|
TRACEBACK_BORDER)
|
||||||
from sanic.log import log
|
from sanic.log import logger
|
||||||
from sanic.response import text, html
|
from sanic.response import text, html
|
||||||
|
|
||||||
|
|
||||||
|
@ -86,12 +86,13 @@ class ErrorHandler:
|
||||||
self.log(format_exc())
|
self.log(format_exc())
|
||||||
if self.debug:
|
if self.debug:
|
||||||
url = getattr(request, 'url', 'unknown')
|
url = getattr(request, 'url', 'unknown')
|
||||||
response_message = (
|
response_message = ('Exception raised in exception handler '
|
||||||
'Exception raised in exception handler "{}" '
|
'"%s" for uri: "%s"\n%s')
|
||||||
'for uri: "{}"\n{}').format(
|
logger.error(response_message,
|
||||||
handler.__name__, url, format_exc())
|
handler.__name__, url, format_exc())
|
||||||
log.error(response_message)
|
|
||||||
return text(response_message, 500)
|
return text(response_message % (
|
||||||
|
handler.__name__, url, format_exc()), 500)
|
||||||
else:
|
else:
|
||||||
return text('An error occurred while handling an error', 500)
|
return text('An error occurred while handling an error', 500)
|
||||||
return response
|
return response
|
||||||
|
@ -101,7 +102,7 @@ class ErrorHandler:
|
||||||
Override this method in an ErrorHandler subclass to prevent
|
Override this method in an ErrorHandler subclass to prevent
|
||||||
logging exceptions.
|
logging exceptions.
|
||||||
"""
|
"""
|
||||||
getattr(log, level)(message)
|
getattr(logger, level)(message)
|
||||||
|
|
||||||
def default(self, request, exception):
|
def default(self, request, exception):
|
||||||
self.log(format_exc())
|
self.log(format_exc())
|
||||||
|
@ -114,10 +115,9 @@ class ErrorHandler:
|
||||||
elif self.debug:
|
elif self.debug:
|
||||||
html_output = self._render_traceback_html(exception, request)
|
html_output = self._render_traceback_html(exception, request)
|
||||||
|
|
||||||
response_message = (
|
response_message = ('Exception occurred while handling uri: '
|
||||||
'Exception occurred while handling uri: "{}"\n{}'.format(
|
'"%s"\n%s')
|
||||||
request.url, format_exc()))
|
logger.error(response_message, request.url, format_exc())
|
||||||
log.error(response_message)
|
|
||||||
return html(html_output, status=500)
|
return html(html_output, status=500)
|
||||||
else:
|
else:
|
||||||
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
|
return html(INTERNAL_SERVER_ERROR_HTML, status=500)
|
||||||
|
|
67
sanic/log.py
67
sanic/log.py
|
@ -1,18 +1,63 @@
|
||||||
import logging
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
class DefaultFilter(logging.Filter):
|
LOGGING_CONFIG_DEFAULTS = dict(
|
||||||
|
version=1,
|
||||||
|
disable_existing_loggers=False,
|
||||||
|
|
||||||
def __init__(self, param=None):
|
loggers={
|
||||||
self.param = param
|
"root": {
|
||||||
|
"level": "INFO",
|
||||||
|
"handlers": ["console"]
|
||||||
|
},
|
||||||
|
"sanic.error": {
|
||||||
|
"level": "INFO",
|
||||||
|
"handlers": ["error_console"],
|
||||||
|
"propagate": True,
|
||||||
|
"qualname": "sanic.error"
|
||||||
|
},
|
||||||
|
|
||||||
def filter(self, record):
|
"sanic.access": {
|
||||||
if self.param is None:
|
"level": "INFO",
|
||||||
return True
|
"handlers": ["access_console"],
|
||||||
if record.levelno in self.param:
|
"propagate": True,
|
||||||
return True
|
"qualname": "sanic.access"
|
||||||
return False
|
}
|
||||||
|
},
|
||||||
|
handlers={
|
||||||
|
"console": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"formatter": "generic",
|
||||||
|
"stream": sys.stdout
|
||||||
|
},
|
||||||
|
"error_console": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"formatter": "generic",
|
||||||
|
"stream": sys.stderr
|
||||||
|
},
|
||||||
|
"access_console": {
|
||||||
|
"class": "logging.StreamHandler",
|
||||||
|
"formatter": "access",
|
||||||
|
"stream": sys.stdout
|
||||||
|
},
|
||||||
|
},
|
||||||
|
formatters={
|
||||||
|
"generic": {
|
||||||
|
"format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s",
|
||||||
|
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
||||||
|
"class": "logging.Formatter"
|
||||||
|
},
|
||||||
|
"access": {
|
||||||
|
"format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: " +
|
||||||
|
"%(request)s %(message)s %(status)d %(byte)d",
|
||||||
|
"datefmt": "[%Y-%m-%d %H:%M:%S %z]",
|
||||||
|
"class": "logging.Formatter"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
log = logging.getLogger('sanic')
|
logger = logging.getLogger('root')
|
||||||
netlog = logging.getLogger('network')
|
error_logger = logging.getLogger('sanic.error')
|
||||||
|
access_logger = logging.getLogger('sanic.access')
|
||||||
|
|
|
@ -17,10 +17,11 @@ except ImportError:
|
||||||
json_loads = json.loads
|
json_loads = json.loads
|
||||||
|
|
||||||
from sanic.exceptions import InvalidUsage
|
from sanic.exceptions import InvalidUsage
|
||||||
from sanic.log import log
|
from sanic.log import error_logger
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream"
|
||||||
|
|
||||||
|
|
||||||
# HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
|
# HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1
|
||||||
# > If the media type remains unknown, the recipient SHOULD treat it
|
# > If the media type remains unknown, the recipient SHOULD treat it
|
||||||
# > as type "application/octet-stream"
|
# > as type "application/octet-stream"
|
||||||
|
@ -68,11 +69,23 @@ class Request(dict):
|
||||||
self._cookies = None
|
self._cookies = None
|
||||||
self.stream = 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
|
@property
|
||||||
def json(self):
|
def json(self):
|
||||||
if self.parsed_json is None:
|
if self.parsed_json is None:
|
||||||
|
self.load_json()
|
||||||
|
|
||||||
|
return self.parsed_json
|
||||||
|
|
||||||
|
def load_json(self, loads=json_loads):
|
||||||
try:
|
try:
|
||||||
self.parsed_json = json_loads(self.body)
|
self.parsed_json = loads(self.body)
|
||||||
except Exception:
|
except Exception:
|
||||||
if not self.body:
|
if not self.body:
|
||||||
return None
|
return None
|
||||||
|
@ -114,7 +127,7 @@ class Request(dict):
|
||||||
self.parsed_form, self.parsed_files = (
|
self.parsed_form, self.parsed_files = (
|
||||||
parse_multipart_form(self.body, boundary))
|
parse_multipart_form(self.body, boundary))
|
||||||
except Exception:
|
except Exception:
|
||||||
log.exception("Failed when parsing form")
|
error_logger.exception("Failed when parsing form")
|
||||||
|
|
||||||
return self.parsed_form
|
return self.parsed_form
|
||||||
|
|
||||||
|
|
|
@ -56,6 +56,7 @@ ALL_STATUS_CODES = {
|
||||||
415: b'Unsupported Media Type',
|
415: b'Unsupported Media Type',
|
||||||
416: b'Requested Range Not Satisfiable',
|
416: b'Requested Range Not Satisfiable',
|
||||||
417: b'Expectation Failed',
|
417: b'Expectation Failed',
|
||||||
|
418: b'I\'m a teapot',
|
||||||
422: b'Unprocessable Entity',
|
422: b'Unprocessable Entity',
|
||||||
423: b'Locked',
|
423: b'Locked',
|
||||||
424: b'Failed Dependency',
|
424: b'Failed Dependency',
|
||||||
|
@ -63,6 +64,7 @@ ALL_STATUS_CODES = {
|
||||||
428: b'Precondition Required',
|
428: b'Precondition Required',
|
||||||
429: b'Too Many Requests',
|
429: b'Too Many Requests',
|
||||||
431: b'Request Header Fields Too Large',
|
431: b'Request Header Fields Too Large',
|
||||||
|
451: b'Unavailable For Legal Reasons',
|
||||||
500: b'Internal Server Error',
|
500: b'Internal Server Error',
|
||||||
501: b'Not Implemented',
|
501: b'Not Implemented',
|
||||||
502: b'Bad Gateway',
|
502: b'Bad Gateway',
|
||||||
|
@ -235,7 +237,8 @@ class HTTPResponse(BaseHTTPResponse):
|
||||||
|
|
||||||
|
|
||||||
def json(body, status=200, headers=None,
|
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.
|
Returns response object with body in json format.
|
||||||
|
|
||||||
|
@ -244,7 +247,7 @@ def json(body, status=200, headers=None,
|
||||||
:param headers: Custom Headers.
|
:param headers: Custom Headers.
|
||||||
:param kwargs: Remaining arguments that are passed to the json encoder.
|
: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)
|
status=status, content_type=content_type)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -68,6 +68,7 @@ class Router:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.routes_all = {}
|
self.routes_all = {}
|
||||||
self.routes_names = {}
|
self.routes_names = {}
|
||||||
|
self.routes_static_files = {}
|
||||||
self.routes_static = {}
|
self.routes_static = {}
|
||||||
self.routes_dynamic = defaultdict(list)
|
self.routes_dynamic = defaultdict(list)
|
||||||
self.routes_always_check = []
|
self.routes_always_check = []
|
||||||
|
@ -148,6 +149,7 @@ class Router:
|
||||||
provided, any method is allowed
|
provided, any method is allowed
|
||||||
:param handler: request handler function.
|
:param handler: request handler function.
|
||||||
When executed, it should provide a response object.
|
When executed, it should provide a response object.
|
||||||
|
:param name: user defined route name for url_for
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
"""
|
"""
|
||||||
if host is not None:
|
if host is not None:
|
||||||
|
@ -231,6 +233,12 @@ class Router:
|
||||||
|
|
||||||
# prefix the handler name with the blueprint name
|
# prefix the handler name with the blueprint name
|
||||||
# if available
|
# 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__'):
|
if hasattr(handler, '__blueprintname__'):
|
||||||
handler_name = '{}.{}'.format(
|
handler_name = '{}.{}'.format(
|
||||||
handler.__blueprintname__, name or handler.__name__)
|
handler.__blueprintname__, name or handler.__name__)
|
||||||
|
@ -245,8 +253,14 @@ class Router:
|
||||||
parameters=parameters, name=handler_name, uri=uri)
|
parameters=parameters, name=handler_name, uri=uri)
|
||||||
|
|
||||||
self.routes_all[uri] = route
|
self.routes_all[uri] = route
|
||||||
pairs = self.routes_names.get(handler_name)
|
if is_static:
|
||||||
if not (pairs and (pairs[0] + '/' == uri or uri + '/' == pairs[0])):
|
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)
|
self.routes_names[handler_name] = (uri, route)
|
||||||
|
|
||||||
if properties['unhashable']:
|
if properties['unhashable']:
|
||||||
|
@ -274,6 +288,11 @@ class Router:
|
||||||
self.routes_names.pop(handler_name)
|
self.routes_names.pop(handler_name)
|
||||||
break
|
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:
|
except KeyError:
|
||||||
raise RouteDoesNotExist("Route was not registered: {}".format(uri))
|
raise RouteDoesNotExist("Route was not registered: {}".format(uri))
|
||||||
|
|
||||||
|
@ -289,15 +308,19 @@ class Router:
|
||||||
self._get.cache_clear()
|
self._get.cache_clear()
|
||||||
|
|
||||||
@lru_cache(maxsize=ROUTER_CACHE_SIZE)
|
@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.
|
"""Find a route in the router based on the specified view name.
|
||||||
|
|
||||||
:param view_name: string of view name to search by
|
:param view_name: string of view name to search by
|
||||||
|
:param kwargs: additional params, usually for static files
|
||||||
:return: tuple containing (uri, Route)
|
:return: tuple containing (uri, Route)
|
||||||
"""
|
"""
|
||||||
if not view_name:
|
if not view_name:
|
||||||
return (None, None)
|
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))
|
return self.routes_names.get(view_name, (None, None))
|
||||||
|
|
||||||
def get(self, request):
|
def get(self, request):
|
||||||
|
|
274
sanic/server.py
274
sanic/server.py
|
@ -24,11 +24,12 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
async_loop = asyncio
|
async_loop = asyncio
|
||||||
|
|
||||||
from sanic.log import log, netlog
|
from sanic.log import logger, access_logger
|
||||||
from sanic.response import HTTPResponse
|
from sanic.response import HTTPResponse
|
||||||
from sanic.request import Request
|
from sanic.request import Request
|
||||||
from sanic.exceptions import (
|
from sanic.exceptions import (
|
||||||
RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError)
|
RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError,
|
||||||
|
ServiceUnavailable)
|
||||||
|
|
||||||
current_time = None
|
current_time = None
|
||||||
|
|
||||||
|
@ -63,17 +64,20 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
# request params
|
# request params
|
||||||
'parser', 'request', 'url', 'headers',
|
'parser', 'request', 'url', 'headers',
|
||||||
# request config
|
# request config
|
||||||
'request_handler', 'request_timeout', 'request_max_size',
|
'request_handler', 'request_timeout', 'response_timeout',
|
||||||
'request_class', 'is_request_stream', 'router',
|
'keep_alive_timeout', 'request_max_size', 'request_class',
|
||||||
# enable or disable access log / error log purpose
|
'is_request_stream', 'router',
|
||||||
'has_log',
|
# enable or disable access log purpose
|
||||||
|
'access_log',
|
||||||
# connection management
|
# connection management
|
||||||
'_total_request_size', '_timeout_handler', '_last_communication_time',
|
'_total_request_size', '_request_timeout_handler',
|
||||||
'_is_stream_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,
|
def __init__(self, *, loop, request_handler, error_handler,
|
||||||
signal=Signal(), connections=set(), request_timeout=60,
|
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,
|
keep_alive=True, is_request_stream=False, router=None,
|
||||||
state=None, debug=False, **kwargs):
|
state=None, debug=False, **kwargs):
|
||||||
self.loop = loop
|
self.loop = loop
|
||||||
|
@ -84,18 +88,23 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self.headers = None
|
self.headers = None
|
||||||
self.router = router
|
self.router = router
|
||||||
self.signal = signal
|
self.signal = signal
|
||||||
self.has_log = has_log
|
self.access_log = access_log
|
||||||
self.connections = connections
|
self.connections = connections
|
||||||
self.request_handler = request_handler
|
self.request_handler = request_handler
|
||||||
self.error_handler = error_handler
|
self.error_handler = error_handler
|
||||||
self.request_timeout = request_timeout
|
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_max_size = request_max_size
|
||||||
self.request_class = request_class or Request
|
self.request_class = request_class or Request
|
||||||
self.is_request_stream = is_request_stream
|
self.is_request_stream = is_request_stream
|
||||||
self._is_stream_handler = False
|
self._is_stream_handler = False
|
||||||
self._total_request_size = 0
|
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_request_time = None
|
||||||
|
self._last_response_time = None
|
||||||
self._request_handler_task = None
|
self._request_handler_task = None
|
||||||
self._request_stream_task = None
|
self._request_stream_task = None
|
||||||
self._keep_alive = keep_alive
|
self._keep_alive = keep_alive
|
||||||
|
@ -118,22 +127,32 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
|
|
||||||
def connection_made(self, transport):
|
def connection_made(self, transport):
|
||||||
self.connections.add(self)
|
self.connections.add(self)
|
||||||
self._timeout_handler = self.loop.call_later(
|
self._request_timeout_handler = self.loop.call_later(
|
||||||
self.request_timeout, self.connection_timeout)
|
self.request_timeout, self.request_timeout_callback)
|
||||||
self.transport = transport
|
self.transport = transport
|
||||||
self._last_request_time = current_time
|
self._last_request_time = current_time
|
||||||
|
|
||||||
def connection_lost(self, exc):
|
def connection_lost(self, exc):
|
||||||
self.connections.discard(self)
|
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):
|
def request_timeout_callback(self):
|
||||||
# Check if
|
# 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
|
time_elapsed = current_time - self._last_request_time
|
||||||
if time_elapsed < self.request_timeout:
|
if time_elapsed < self.request_timeout:
|
||||||
time_left = self.request_timeout - time_elapsed
|
time_left = self.request_timeout - time_elapsed
|
||||||
self._timeout_handler = (
|
self._request_timeout_handler = (
|
||||||
self.loop.call_later(time_left, self.connection_timeout))
|
self.loop.call_later(time_left,
|
||||||
|
self.request_timeout_callback)
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
if self._request_stream_task:
|
if self._request_stream_task:
|
||||||
self._request_stream_task.cancel()
|
self._request_stream_task.cancel()
|
||||||
|
@ -144,6 +163,36 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
except RequestTimeout as exception:
|
except RequestTimeout as exception:
|
||||||
self.write_error(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
|
# Parsing
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
|
@ -189,10 +238,12 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
and int(value) > self.request_max_size:
|
and int(value) > self.request_max_size:
|
||||||
exception = PayloadTooLarge('Payload Too Large')
|
exception = PayloadTooLarge('Payload Too Large')
|
||||||
self.write_error(exception)
|
self.write_error(exception)
|
||||||
|
try:
|
||||||
|
value = value.decode()
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
value = value.decode('latin_1')
|
||||||
self.headers.append(
|
self.headers.append(
|
||||||
(self._header_fragment.decode().casefold(),
|
(self._header_fragment.decode().casefold(), value))
|
||||||
value.decode()))
|
|
||||||
|
|
||||||
self._header_fragment = b''
|
self._header_fragment = b''
|
||||||
|
|
||||||
|
@ -204,6 +255,11 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
method=self.parser.get_method().decode(),
|
method=self.parser.get_method().decode(),
|
||||||
transport=self.transport
|
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:
|
if self.is_request_stream:
|
||||||
self._is_stream_handler = self.router.is_stream_handler(
|
self._is_stream_handler = self.router.is_stream_handler(
|
||||||
self.request)
|
self.request)
|
||||||
|
@ -219,6 +275,11 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self.request.body.append(body)
|
self.request.body.append(body)
|
||||||
|
|
||||||
def on_message_complete(self):
|
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:
|
if self.is_request_stream and self._is_stream_handler:
|
||||||
self._request_stream_task = self.loop.create_task(
|
self._request_stream_task = self.loop.create_task(
|
||||||
self.request.stream.put(None))
|
self.request.stream.put(None))
|
||||||
|
@ -227,6 +288,9 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self.execute_request_handler()
|
self.execute_request_handler()
|
||||||
|
|
||||||
def execute_request_handler(self):
|
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_task = self.loop.create_task(
|
||||||
self.request_handler(
|
self.request_handler(
|
||||||
self.request,
|
self.request,
|
||||||
|
@ -236,35 +300,50 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
# -------------------------------------------- #
|
# -------------------------------------------- #
|
||||||
# Responding
|
# 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):
|
def write_response(self, response):
|
||||||
"""
|
"""
|
||||||
Writes response content synchronously to the transport.
|
Writes response content synchronously to the transport.
|
||||||
"""
|
"""
|
||||||
|
if self._response_timeout_handler:
|
||||||
|
self._response_timeout_handler.cancel()
|
||||||
|
self._response_timeout_handler = None
|
||||||
try:
|
try:
|
||||||
keep_alive = self.keep_alive
|
keep_alive = self.keep_alive
|
||||||
self.transport.write(
|
self.transport.write(
|
||||||
response.output(
|
response.output(
|
||||||
self.request.version, keep_alive,
|
self.request.version, keep_alive,
|
||||||
self.request_timeout))
|
self.keep_alive_timeout))
|
||||||
if self.has_log:
|
self.log_response(response)
|
||||||
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)
|
|
||||||
})
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
log.error(
|
logger.error('Invalid response object for url %s, '
|
||||||
('Invalid response object for url {}, '
|
'Expected Type: HTTPResponse, Actual Type: %s',
|
||||||
'Expected Type: HTTPResponse, Actual Type: {}').format(
|
self.url, type(response))
|
||||||
self.url, type(response)))
|
|
||||||
self.write_error(ServerError('Invalid response type'))
|
self.write_error(ServerError('Invalid response type'))
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
log.error(
|
logger.error('Connection lost before response written @ %s',
|
||||||
'Connection lost before response written @ {}'.format(
|
self.request.ip)
|
||||||
self.request.ip))
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.bail_out(
|
self.bail_out(
|
||||||
"Writing response failed, connection closed {}".format(
|
"Writing response failed, connection closed {}".format(
|
||||||
|
@ -273,7 +352,10 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
if not keep_alive:
|
if not keep_alive:
|
||||||
self.transport.close()
|
self.transport.close()
|
||||||
else:
|
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()
|
self.cleanup()
|
||||||
|
|
||||||
async def stream_response(self, response):
|
async def stream_response(self, response):
|
||||||
|
@ -282,31 +364,23 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
the transport to the response so the response consumer can
|
the transport to the response so the response consumer can
|
||||||
write to the response as needed.
|
write to the response as needed.
|
||||||
"""
|
"""
|
||||||
|
if self._response_timeout_handler:
|
||||||
|
self._response_timeout_handler.cancel()
|
||||||
|
self._response_timeout_handler = None
|
||||||
try:
|
try:
|
||||||
keep_alive = self.keep_alive
|
keep_alive = self.keep_alive
|
||||||
response.transport = self.transport
|
response.transport = self.transport
|
||||||
await response.stream(
|
await response.stream(
|
||||||
self.request.version, keep_alive, self.request_timeout)
|
self.request.version, keep_alive, self.keep_alive_timeout)
|
||||||
if self.has_log:
|
self.log_response(response)
|
||||||
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)
|
|
||||||
})
|
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
log.error(
|
logger.error('Invalid response object for url %s, '
|
||||||
('Invalid response object for url {}, '
|
'Expected Type: HTTPResponse, Actual Type: %s',
|
||||||
'Expected Type: HTTPResponse, Actual Type: {}').format(
|
self.url, type(response))
|
||||||
self.url, type(response)))
|
|
||||||
self.write_error(ServerError('Invalid response type'))
|
self.write_error(ServerError('Invalid response type'))
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
log.error(
|
logger.error('Connection lost before response written @ %s',
|
||||||
'Connection lost before response written @ {}'.format(
|
self.request.ip)
|
||||||
self.request.ip))
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.bail_out(
|
self.bail_out(
|
||||||
"Writing response failed, connection closed {}".format(
|
"Writing response failed, connection closed {}".format(
|
||||||
|
@ -315,58 +389,52 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
if not keep_alive:
|
if not keep_alive:
|
||||||
self.transport.close()
|
self.transport.close()
|
||||||
else:
|
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()
|
self.cleanup()
|
||||||
|
|
||||||
def write_error(self, exception):
|
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
|
response = None
|
||||||
try:
|
try:
|
||||||
response = self.error_handler.response(self.request, exception)
|
response = self.error_handler.response(self.request, exception)
|
||||||
version = self.request.version if self.request else '1.1'
|
version = self.request.version if self.request else '1.1'
|
||||||
self.transport.write(response.output(version))
|
self.transport.write(response.output(version))
|
||||||
except RuntimeError:
|
except RuntimeError:
|
||||||
log.error(
|
logger.error('Connection lost before error written @ %s',
|
||||||
'Connection lost before error written @ {}'.format(
|
self.request.ip if self.request else 'Unknown')
|
||||||
self.request.ip if self.request else 'Unknown'))
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.bail_out(
|
self.bail_out(
|
||||||
"Writing error failed, connection closed {}".format(repr(e)),
|
"Writing error failed, connection closed {}".format(
|
||||||
from_error=True)
|
repr(e)), from_error=True
|
||||||
|
)
|
||||||
finally:
|
finally:
|
||||||
if self.has_log:
|
if self.parser and (self.keep_alive
|
||||||
extra = dict()
|
or getattr(response, 'status', 0) == 408):
|
||||||
if isinstance(response, HTTPResponse):
|
self.log_response(response)
|
||||||
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)
|
|
||||||
self.transport.close()
|
self.transport.close()
|
||||||
|
|
||||||
def bail_out(self, message, from_error=False):
|
def bail_out(self, message, from_error=False):
|
||||||
if from_error or self.transport.is_closing():
|
if from_error or self.transport.is_closing():
|
||||||
log.error(
|
logger.error("Transport closed @ %s and exception "
|
||||||
("Transport closed @ {} and exception "
|
"experienced during error handling",
|
||||||
"experienced during error handling").format(
|
self.transport.get_extra_info('peername'))
|
||||||
self.transport.get_extra_info('peername')))
|
logger.debug('Exception:\n%s', traceback.format_exc())
|
||||||
log.debug(
|
|
||||||
'Exception:\n{}'.format(traceback.format_exc()))
|
|
||||||
else:
|
else:
|
||||||
exception = ServerError(message)
|
exception = ServerError(message)
|
||||||
self.write_error(exception)
|
self.write_error(exception)
|
||||||
log.error(message)
|
logger.error(message)
|
||||||
|
|
||||||
def cleanup(self):
|
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.parser = None
|
||||||
self.request = None
|
self.request = None
|
||||||
self.url = None
|
self.url = None
|
||||||
|
@ -421,12 +489,13 @@ def trigger_events(events, loop):
|
||||||
|
|
||||||
def serve(host, port, request_handler, error_handler, before_start=None,
|
def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
after_start=None, before_stop=None, after_stop=None, debug=False,
|
after_start=None, before_stop=None, after_stop=None, debug=False,
|
||||||
request_timeout=60, ssl=None, sock=None, request_max_size=None,
|
request_timeout=60, response_timeout=60, keep_alive_timeout=60,
|
||||||
reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100,
|
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,
|
register_sys_signals=True, run_async=False, connections=None,
|
||||||
signal=Signal(), request_class=None, has_log=True, keep_alive=True,
|
signal=Signal(), request_class=None, access_log=True,
|
||||||
is_request_stream=False, router=None, websocket_max_size=None,
|
keep_alive=True, is_request_stream=False, router=None,
|
||||||
websocket_max_queue=None, state=None,
|
websocket_max_size=None, websocket_max_queue=None, state=None,
|
||||||
graceful_shutdown_timeout=15.0):
|
graceful_shutdown_timeout=15.0):
|
||||||
"""Start asynchronous HTTP Server on an individual process.
|
"""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 loop: asyncio compatible event loop
|
||||||
:param protocol: subclass of asyncio protocol class
|
:param protocol: subclass of asyncio protocol class
|
||||||
:param request_class: Request class to use
|
: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 is_request_stream: disable/enable Request.stream
|
||||||
:param router: Router object
|
:param router: Router object
|
||||||
:return: Nothing
|
:return: Nothing
|
||||||
|
@ -474,9 +543,11 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
request_handler=request_handler,
|
request_handler=request_handler,
|
||||||
error_handler=error_handler,
|
error_handler=error_handler,
|
||||||
request_timeout=request_timeout,
|
request_timeout=request_timeout,
|
||||||
|
response_timeout=response_timeout,
|
||||||
|
keep_alive_timeout=keep_alive_timeout,
|
||||||
request_max_size=request_max_size,
|
request_max_size=request_max_size,
|
||||||
request_class=request_class,
|
request_class=request_class,
|
||||||
has_log=has_log,
|
access_log=access_log,
|
||||||
keep_alive=keep_alive,
|
keep_alive=keep_alive,
|
||||||
is_request_stream=is_request_stream,
|
is_request_stream=is_request_stream,
|
||||||
router=router,
|
router=router,
|
||||||
|
@ -508,7 +579,7 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
try:
|
try:
|
||||||
http_server = loop.run_until_complete(server_coroutine)
|
http_server = loop.run_until_complete(server_coroutine)
|
||||||
except:
|
except:
|
||||||
log.exception("Unable to start server")
|
logger.exception("Unable to start server")
|
||||||
return
|
return
|
||||||
|
|
||||||
trigger_events(after_start, loop)
|
trigger_events(after_start, loop)
|
||||||
|
@ -519,14 +590,14 @@ def serve(host, port, request_handler, error_handler, before_start=None,
|
||||||
try:
|
try:
|
||||||
loop.add_signal_handler(_signal, loop.stop)
|
loop.add_signal_handler(_signal, loop.stop)
|
||||||
except NotImplementedError:
|
except NotImplementedError:
|
||||||
log.warn('Sanic tried to use loop.add_signal_handler but it is'
|
logger.warning('Sanic tried to use loop.add_signal_handler '
|
||||||
' not implemented on this platform.')
|
'but it is not implemented on this platform.')
|
||||||
pid = os.getpid()
|
pid = os.getpid()
|
||||||
try:
|
try:
|
||||||
log.info('Starting worker [{}]'.format(pid))
|
logger.info('Starting worker [%s]', pid)
|
||||||
loop.run_forever()
|
loop.run_forever()
|
||||||
finally:
|
finally:
|
||||||
log.info("Stopping worker [{}]".format(pid))
|
logger.info("Stopping worker [%s]", pid)
|
||||||
|
|
||||||
# Run the on_stop function if provided
|
# Run the on_stop function if provided
|
||||||
trigger_events(before_stop, loop)
|
trigger_events(before_stop, loop)
|
||||||
|
@ -588,8 +659,7 @@ def serve_multiple(server_settings, workers):
|
||||||
server_settings['port'] = None
|
server_settings['port'] = None
|
||||||
|
|
||||||
def sig_handler(signal, frame):
|
def sig_handler(signal, frame):
|
||||||
log.info("Received signal {}. Shutting down.".format(
|
logger.info("Received signal %s. Shutting down.", Signals(signal).name)
|
||||||
Signals(signal).name))
|
|
||||||
for process in processes:
|
for process in processes:
|
||||||
os.kill(process.pid, SIGINT)
|
os.kill(process.pid, SIGINT)
|
||||||
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ from sanic.response import file, file_stream, HTTPResponse
|
||||||
|
|
||||||
def register(app, uri, file_or_directory, pattern,
|
def register(app, uri, file_or_directory, pattern,
|
||||||
use_modified_since, use_content_range,
|
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
|
# 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
|
# make a good effort here. Modified-since is nice, but we could
|
||||||
# also look into etags, expires, and caching
|
# 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
|
than the file() handler to send the file
|
||||||
If this is an integer, this represents the
|
If this is an integer, this represents the
|
||||||
threshold size to switch to file_stream()
|
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,
|
# If we're not trying to match a file directly,
|
||||||
# serve from the folder
|
# serve from the folder
|
||||||
|
@ -117,4 +118,8 @@ def register(app, uri, file_or_directory, pattern,
|
||||||
path=file_or_directory,
|
path=file_or_directory,
|
||||||
relative_url=file_uri)
|
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)
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import traceback
|
import traceback
|
||||||
from json import JSONDecodeError
|
from json import JSONDecodeError
|
||||||
|
|
||||||
from sanic.log import log
|
from sanic.log import logger
|
||||||
|
|
||||||
HOST = '127.0.0.1'
|
HOST = '127.0.0.1'
|
||||||
PORT = 42101
|
PORT = 42101
|
||||||
|
@ -19,7 +19,7 @@ class SanicTestClient:
|
||||||
url = 'http://{host}:{port}{uri}'.format(
|
url = 'http://{host}:{port}{uri}'.format(
|
||||||
host=HOST, port=PORT, uri=uri)
|
host=HOST, port=PORT, uri=uri)
|
||||||
|
|
||||||
log.info(url)
|
logger.info(url)
|
||||||
conn = aiohttp.TCPConnector(verify_ssl=False)
|
conn = aiohttp.TCPConnector(verify_ssl=False)
|
||||||
async with aiohttp.ClientSession(
|
async with aiohttp.ClientSession(
|
||||||
cookies=cookies, connector=conn) as session:
|
cookies=cookies, connector=conn) as session:
|
||||||
|
@ -61,7 +61,7 @@ class SanicTestClient:
|
||||||
**request_kwargs)
|
**request_kwargs)
|
||||||
results[-1] = response
|
results[-1] = response
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.error(
|
logger.error(
|
||||||
'Exception:\n{}'.format(traceback.format_exc()))
|
'Exception:\n{}'.format(traceback.format_exc()))
|
||||||
exceptions.append(e)
|
exceptions.append(e)
|
||||||
self.app.stop()
|
self.app.stop()
|
||||||
|
|
|
@ -142,9 +142,8 @@ class GunicornWorker(base.Worker):
|
||||||
)
|
)
|
||||||
if self.max_requests and req_count > self.max_requests:
|
if self.max_requests and req_count > self.max_requests:
|
||||||
self.alive = False
|
self.alive = False
|
||||||
self.log.info(
|
self.log.info("Max requests exceeded, shutting down: %s",
|
||||||
"Max requests exceeded, shutting down: %s", self
|
self)
|
||||||
)
|
|
||||||
elif pid == os.getpid() and self.ppid != os.getppid():
|
elif pid == os.getpid() and self.ppid != os.getppid():
|
||||||
self.alive = False
|
self.alive = False
|
||||||
self.log.info("Parent changed, shutting down: %s", self)
|
self.log.info("Parent changed, shutting down: %s", self)
|
||||||
|
|
1
tests/static/bp/decode me.txt
Normal file
1
tests/static/bp/decode me.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
I am just a regular static file that needs to have its uri decoded
|
BIN
tests/static/bp/python.png
Normal file
BIN
tests/static/bp/python.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
1
tests/static/bp/test.file
Normal file
1
tests/static/bp/test.file
Normal file
|
@ -0,0 +1 @@
|
||||||
|
I am just a regular static file
|
|
@ -25,6 +25,25 @@ def test_cookies():
|
||||||
assert response.text == 'Cookies are: working!'
|
assert response.text == 'Cookies are: working!'
|
||||||
assert response_cookies['right_back'].value == 'at you'
|
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", [
|
@pytest.mark.parametrize("httponly,expected", [
|
||||||
(False, False),
|
(False, False),
|
||||||
(True, True),
|
(True, True),
|
||||||
|
@ -34,7 +53,7 @@ def test_false_cookies(httponly, expected):
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def handler(request):
|
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'] = 'at you'
|
||||||
response.cookies['right_back']['httponly'] = httponly
|
response.cookies['right_back']['httponly'] = httponly
|
||||||
return response
|
return response
|
||||||
|
@ -43,7 +62,7 @@ def test_false_cookies(httponly, expected):
|
||||||
response_cookies = SimpleCookie()
|
response_cookies = SimpleCookie()
|
||||||
response_cookies.load(response.headers.get('Set-Cookie', {}))
|
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():
|
def test_http2_cookies():
|
||||||
app = Sanic('test_http2_cookies')
|
app = Sanic('test_http2_cookies')
|
||||||
|
|
269
tests/test_keep_alive_timeout.py
Normal file
269
tests/test_keep_alive_timeout.py
Normal file
|
@ -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]
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import uuid
|
import uuid
|
||||||
from importlib import reload
|
from importlib import reload
|
||||||
|
|
||||||
from sanic.config import LOGGING
|
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
|
from sanic.log import LOGGING_CONFIG_DEFAULTS
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
from io import StringIO
|
from io import StringIO
|
||||||
import logging
|
import logging
|
||||||
|
@ -40,18 +40,34 @@ def test_log():
|
||||||
assert rand_string in log_text
|
assert rand_string in log_text
|
||||||
|
|
||||||
|
|
||||||
def test_default_log_fmt():
|
def test_logging_defaults():
|
||||||
|
|
||||||
reset_logging()
|
reset_logging()
|
||||||
Sanic()
|
app = Sanic("test_logging")
|
||||||
for fmt in [h.formatter for h in logging.getLogger('sanic').handlers]:
|
|
||||||
assert fmt._fmt == LOGGING['formatters']['simple']['format']
|
|
||||||
|
|
||||||
|
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()
|
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__":
|
app = Sanic("test_logging", log_config=modified_config)
|
||||||
test_log()
|
|
||||||
|
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']
|
||||||
|
|
|
@ -1,38 +1,163 @@
|
||||||
|
from json import JSONDecodeError
|
||||||
|
|
||||||
from sanic import Sanic
|
from sanic import Sanic
|
||||||
import asyncio
|
import asyncio
|
||||||
from sanic.response import text
|
from sanic.response import text
|
||||||
from sanic.exceptions import RequestTimeout
|
|
||||||
from sanic.config import Config
|
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_default_app = Sanic('test_request_timeout_default')
|
||||||
|
request_no_timeout_app = Sanic('test_request_no_timeout')
|
||||||
|
|
||||||
@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_timeout_default_app.route('/1')
|
@request_timeout_default_app.route('/1')
|
||||||
async def handler_2(request):
|
async def handler1(request):
|
||||||
await asyncio.sleep(2)
|
return text('OK')
|
||||||
|
|
||||||
|
|
||||||
|
@request_no_timeout_app.route('/1')
|
||||||
|
async def handler2(request):
|
||||||
return text('OK')
|
return text('OK')
|
||||||
|
|
||||||
|
|
||||||
def test_default_server_error_request_timeout():
|
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.status == 408
|
||||||
assert response.text == 'Error: Request Timeout'
|
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'
|
||||||
|
|
38
tests/test_response_timeout.py
Normal file
38
tests/test_response_timeout.py
Normal file
|
@ -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'
|
|
@ -161,3 +161,20 @@ def test_static_content_range_error(file_name, static_file_directory):
|
||||||
assert 'Content-Range' in response.headers
|
assert 'Content-Range' in response.headers
|
||||||
assert response.headers['Content-Range'] == "bytes */%s" % (
|
assert response.headers['Content-Range'] == "bytes */%s" % (
|
||||||
len(get_file_content(static_file_directory, file_name)),)
|
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
|
||||||
|
|
|
@ -17,6 +17,9 @@ URL_FOR_VALUE2 = '/myurl?arg1=v1&arg1=v2#anchor'
|
||||||
URL_FOR_ARGS3 = dict(arg1='v1', _anchor='anchor', _scheme='http',
|
URL_FOR_ARGS3 = dict(arg1='v1', _anchor='anchor', _scheme='http',
|
||||||
_server='localhost:{}'.format(test_port), _external=True)
|
_server='localhost:{}'.format(test_port), _external=True)
|
||||||
URL_FOR_VALUE3 = 'http://localhost:{}/myurl?arg1=v1#anchor'.format(test_port)
|
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):
|
def _generate_handlers_from_names(app, l):
|
||||||
|
@ -49,7 +52,8 @@ def test_simple_url_for_getting(simple_app):
|
||||||
@pytest.mark.parametrize('args,url',
|
@pytest.mark.parametrize('args,url',
|
||||||
[(URL_FOR_ARGS1, URL_FOR_VALUE1),
|
[(URL_FOR_ARGS1, URL_FOR_VALUE1),
|
||||||
(URL_FOR_ARGS2, URL_FOR_VALUE2),
|
(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):
|
def test_simple_url_for_getting_with_more_params(args, url):
|
||||||
app = Sanic('more_url_build')
|
app = Sanic('more_url_build')
|
||||||
|
|
||||||
|
|
446
tests/test_url_for_static.py
Normal file
446
tests/test_url_for_static.py
Normal file
|
@ -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)),)
|
Loading…
Reference in New Issue
Block a user