Merge pull request #15 from channelcat/master

Merge upstream master branch
This commit is contained in:
7 2017-11-25 20:49:09 -08:00 committed by GitHub
commit 472bbcf293
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 341 additions and 97 deletions

View File

@ -1,4 +1,7 @@
include README.rst include README.rst
include MANIFEST.in
include LICENSE
include setup.py
recursive-exclude * __pycache__ recursive-exclude * __pycache__
recursive-exclude * *.py[co] recursive-exclude * *.py[co]

View File

@ -85,8 +85,35 @@ DB_USER = 'appuser'
Out of the box there are just a few predefined values which can be overwritten when creating the application. Out of the box there are just a few predefined values which can be overwritten when creating the application.
| Variable | Default | Description | | Variable | Default | Description |
| ----------------- | --------- | --------------------------------- | | ------------------ | --------- | --------------------------------------------- |
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) | | REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
| REQUEST_TIMEOUT | 60 | How long a request can take (sec) | | REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
| KEEP_ALIVE | True | Disables keep-alive when False | | RESPONSE_TIMEOUT | 60 | How long a response can take to process (sec) |
| KEEP_ALIVE | True | Disables keep-alive when False |
| KEEP_ALIVE_TIMEOUT | 5 | How long to hold a TCP connection open (sec) |
### The different Timeout variables:
A request timeout measures the duration of time between the instant when a new open TCP connection is passed to the Sanic backend server, and the instant when the whole HTTP request is received. If the time taken exceeds the `REQUEST_TIMEOUT` value (in seconds), this is considered a Client Error so Sanic generates a HTTP 408 response and sends that to the client. Adjust this value higher if your clients routinely pass very large request payloads or upload requests very slowly.
A response timeout measures the duration of time between the instant the Sanic server passes the HTTP request to the Sanic App, and the instant a HTTP response is sent to the client. If the time taken exceeds the `RESPONSE_TIMEOUT` value (in seconds), this is considered a Server Error so Sanic generates a HTTP 503 response and sets that to the client. Adjust this value higher if your application is likely to have long-running process that delay the generation of a response.
### What is Keep Alive? And what does the Keep Alive Timeout value do?
Keep-Alive is a HTTP feature indroduced in HTTP 1.1. When sending a HTTP request, the client (usually a web browser application) can set a Keep-Alive header to indicate for the http server (Sanic) to not close the TCP connection after it has send the response. This allows the client to reuse the existing TCP connection to send subsequent HTTP requests, and ensures more efficient network traffic for both the client and the server.
The `KEEP_ALIVE` config variable is set to `True` in Sanic by default. If you don't need this feature in your application, set it to `False` to cause all client connections to close immediately after a response is sent, regardless of the Keep-Alive header on the request.
The amount of time the server holds the TCP connection open is decided by the server itself. In Sanic, that value is configured using the `KEEP_ALIVE_TIMEOUT` value. By default, it is set to 5 seconds, this is the same default setting as the Apache HTTP server and is a good balance between allowing enough time for the client to send a new request, and not holding open too many connections at once. Do not exceed 75 seconds unless you know your clients are using a browser which supports TCP connections held open for that long.
For reference:
```
Apache httpd server default keepalive timeout = 5 seconds
Nginx server default keepalive timeout = 75 seconds
Nginx performance tuning guidelines uses keepalive = 15 seconds
IE (5-9) client hard keepalive limit = 60 seconds
Firefox client hard keepalive limit = 115 seconds
Opera 11 client hard keepalive limit = 120 seconds
Chrome 13+ client keepalive limit > 300+ seconds
```

View File

@ -1,7 +1,7 @@
# Extensions # Extensions
A list of Sanic extensions created by the community. A list of Sanic extensions created by the community.
- [Sanic-Plugins-Framework](https://github.com/ashleysommer/sanicpluginsframework): Library for easily creating and using Sanic plugins.
- [Sessions](https://github.com/subyraman/sanic_session): Support for sessions. - [Sessions](https://github.com/subyraman/sanic_session): Support for sessions.
Allows using redis, memcache or an in memory store. Allows using redis, memcache or an in memory store.
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors. - [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.

View File

@ -75,6 +75,10 @@ The following variables are accessible as properties on `Request` objects:
- `ip` (str) - IP address of the requester. - `ip` (str) - IP address of the requester.
- `port` (str) - Port address of the requester.
- `socket` (tuple) - (IP, port) of the requester.
- `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object. - `app` - a reference to the Sanic application object that is handling this request. This is useful when inside blueprints or other handlers in modules that do not have access to the global `app` object.
```python ```python

49
examples/pytest_xdist.py Normal file
View File

@ -0,0 +1,49 @@
"""pytest-xdist example for sanic server
Install testing tools:
$ pip install pytest pytest-xdist
Run with xdist params:
$ pytest examples/pytest_xdist.py -n 8 # 8 workers
"""
import re
from sanic import Sanic
from sanic.response import text
from sanic.testing import PORT as PORT_BASE, SanicTestClient
import pytest
@pytest.fixture(scope="session")
def test_port(worker_id):
m = re.search(r'[0-9]+', worker_id)
if m:
num_id = m.group(0)
else:
num_id = 0
port = PORT_BASE + int(num_id)
return port
@pytest.fixture(scope="session")
def app():
app = Sanic()
@app.route('/')
async def index(request):
return text('OK')
return app
@pytest.fixture(scope="session")
def client(app, test_port):
return SanicTestClient(app, test_port)
@pytest.mark.parametrize('run_id', range(100))
def test_index(client, run_id):
request, response = client._sanic_endpoint_test('get', '/')
assert response.status == 200
assert response.text == 'OK'

View File

@ -28,7 +28,8 @@ 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,
strict_slashes=False, log_config=None): strict_slashes=False, log_config=None,
configure_logging=True):
# Get name from previous stack frame # Get name from previous stack frame
if name is None: if name is None:
@ -36,7 +37,8 @@ class Sanic:
name = getmodulename(frame_records[1]) name = getmodulename(frame_records[1])
# logging # logging
logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) if configure_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()
@ -47,6 +49,7 @@ class Sanic:
self.response_middleware = deque() self.response_middleware = deque()
self.blueprints = {} self.blueprints = {}
self._blueprint_order = [] self._blueprint_order = []
self.configure_logging = configure_logging
self.debug = None self.debug = None
self.sock = None self.sock = None
self.strict_slashes = strict_slashes self.strict_slashes = strict_slashes
@ -54,7 +57,7 @@ class Sanic:
self.is_running = False self.is_running = False
self.is_request_stream = False self.is_request_stream = False
self.websocket_enabled = False self.websocket_enabled = False
self.websocket_tasks = [] self.websocket_tasks = set()
# Register alternative method names # Register alternative method names
self.go_fast = self.run self.go_fast = self.run
@ -259,7 +262,7 @@ class Sanic:
# its future is kept in self.websocket_tasks in case it # its future is kept in self.websocket_tasks in case it
# needs to be cancelled due to the server being stopped # needs to be cancelled due to the server being stopped
fut = ensure_future(handler(request, ws, *args, **kwargs)) fut = ensure_future(handler(request, ws, *args, **kwargs))
self.websocket_tasks.append(fut) self.websocket_tasks.add(fut)
try: try:
await fut await fut
except (CancelledError, ConnectionClosed): except (CancelledError, ConnectionClosed):
@ -345,13 +348,14 @@ 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, name='static', host=None): stream_large_files=False, name='static', host=None,
strict_slashes=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, name, host) stream_large_files, name, host, strict_slashes)
def blueprint(self, blueprint, **options): def blueprint(self, blueprint, **options):
"""Register a blueprint on the application. """Register a blueprint on the application.
@ -574,9 +578,9 @@ class Sanic:
try: try:
response = await self._run_response_middleware(request, response = await self._run_response_middleware(request,
response) response)
except: except BaseException:
error_logger.exception( error_logger.exception(
'Exception occured in one of response middleware handlers' 'Exception occurred in one of response middleware handlers'
) )
# pass the response to the correct callback # pass the response to the correct callback
@ -642,7 +646,7 @@ class Sanic:
serve(**server_settings) serve(**server_settings)
else: else:
serve_multiple(server_settings, workers) serve_multiple(server_settings, workers)
except: except BaseException:
error_logger.exception( error_logger.exception(
'Experienced exception while trying to serve') 'Experienced exception while trying to serve')
raise raise
@ -793,7 +797,7 @@ class Sanic:
listeners = [partial(listener, self) for listener in listeners] listeners = [partial(listener, self) for listener in listeners]
server_settings[settings_name] = listeners server_settings[settings_name] = listeners
if debug: if self.configure_logging and debug:
logger.setLevel(logging.DEBUG) logger.setLevel(logging.DEBUG)
if self.config.LOGO is not None: if self.config.LOGO is not None:
logger.debug(self.config.LOGO) logger.debug(self.config.LOGO)

View File

@ -221,8 +221,12 @@ class Blueprint:
name = kwargs.pop('name', 'static') name = kwargs.pop('name', 'static')
if not name.startswith(self.name + '.'): if not name.startswith(self.name + '.'):
name = '{}.{}'.format(self.name, name) name = '{}.{}'.format(self.name, name)
kwargs.update(name=name) kwargs.update(name=name)
strict_slashes = kwargs.get('strict_slashes')
if strict_slashes is None and self.strict_slashes is not None:
kwargs.update(strict_slashes=self.strict_slashes)
static = FutureStatic(uri, file_or_directory, args, kwargs) static = FutureStatic(uri, file_or_directory, args, kwargs)
self.statics.append(static) self.statics.append(static)

View File

@ -33,12 +33,6 @@ class Config(dict):
self.REQUEST_TIMEOUT = 60 # 60 seconds self.REQUEST_TIMEOUT = 60 # 60 seconds
self.RESPONSE_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.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

View File

@ -1,4 +1,4 @@
from sanic.response import ALL_STATUS_CODES, COMMON_STATUS_CODES from sanic.response import STATUS_CODES
TRACEBACK_STYLE = ''' TRACEBACK_STYLE = '''
<style> <style>
@ -275,8 +275,7 @@ def abort(status_code, message=None):
in response.py for the given status code. in response.py for the given status code.
""" """
if message is None: if message is None:
message = COMMON_STATUS_CODES.get(status_code, message = STATUS_CODES.get(status_code)
ALL_STATUS_CODES.get(status_code))
# These are stored as bytes in the STATUS_CODES dict # These are stored as bytes in the STATUS_CODES dict
message = message.decode('utf8') message = message.decode('utf8')
sanic_exception = _sanic_exceptions.get(status_code, SanicException) sanic_exception = _sanic_exceptions.get(status_code, SanicException)

View File

@ -46,7 +46,8 @@ class Request(dict):
__slots__ = ( __slots__ = (
'app', 'headers', 'version', 'method', '_cookies', 'transport', 'app', 'headers', 'version', 'method', '_cookies', 'transport',
'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', 'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files',
'_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr' '_ip', '_parsed_url', 'uri_template', 'stream', '_remote_addr',
'_socket', '_port'
) )
def __init__(self, url_bytes, headers, version, method, transport): def __init__(self, url_bytes, headers, version, method, transport):
@ -167,11 +168,27 @@ class Request(dict):
@property @property
def ip(self): def ip(self):
if not hasattr(self, '_ip'): if not hasattr(self, '_socket'):
self._ip = (self.transport.get_extra_info('peername') or self._get_address()
(None, None))
return self._ip return self._ip
@property
def port(self):
if not hasattr(self, '_socket'):
self._get_address()
return self._port
@property
def socket(self):
if not hasattr(self, '_socket'):
self._get_socket()
return self._socket
def _get_address(self):
self._socket = (self.transport.get_extra_info('peername') or
(None, None))
self._ip, self._port = self._socket
@property @property
def remote_addr(self): def remote_addr(self):
"""Attempt to return the original client ip based on X-Forwarded-For. """Attempt to return the original client ip based on X-Forwarded-For.

View File

@ -3,20 +3,14 @@ from os import path
try: try:
from ujson import dumps as json_dumps from ujson import dumps as json_dumps
except: except BaseException:
from json import dumps as json_dumps from json import dumps as json_dumps
from aiofiles import open as open_async from aiofiles import open as open_async
from sanic.cookies import CookieJar from sanic.cookies import CookieJar
COMMON_STATUS_CODES = { STATUS_CODES = {
200: b'OK',
400: b'Bad Request',
404: b'Not Found',
500: b'Internal Server Error',
}
ALL_STATUS_CODES = {
100: b'Continue', 100: b'Continue',
101: b'Switching Protocols', 101: b'Switching Protocols',
102: b'Processing', 102: b'Processing',
@ -162,11 +156,10 @@ class StreamingHTTPResponse(BaseHTTPResponse):
headers = self._parse_headers() headers = self._parse_headers()
# Try to pull from the common codes first if self.status is 200:
# Speeds up response rate 6% over pulling from all status = b'OK'
status = COMMON_STATUS_CODES.get(self.status) else:
if not status: status = STATUS_CODES.get(self.status)
status = ALL_STATUS_CODES.get(self.status)
return (b'HTTP/%b %d %b\r\n' return (b'HTTP/%b %d %b\r\n'
b'%b' b'%b'
@ -209,11 +202,10 @@ class HTTPResponse(BaseHTTPResponse):
headers = self._parse_headers() headers = self._parse_headers()
# Try to pull from the common codes first if self.status is 200:
# Speeds up response rate 6% over pulling from all status = b'OK'
status = COMMON_STATUS_CODES.get(self.status) else:
if not status: status = STATUS_CODES.get(self.status, b'UNKNOWN RESPONSE')
status = ALL_STATUS_CODES.get(self.status, b'UNKNOWN RESPONSE')
return (b'HTTP/%b %d %b\r\n' return (b'HTTP/%b %d %b\r\n'
b'Connection: %b\r\n' b'Connection: %b\r\n'
@ -292,15 +284,22 @@ def html(body, status=200, headers=None):
content_type="text/html; charset=utf-8") content_type="text/html; charset=utf-8")
async def file(location, mime_type=None, headers=None, _range=None): async def file(
location, mime_type=None, headers=None, filename=None, _range=None):
"""Return a response object with file data. """Return a response object with file data.
:param location: Location of file on system. :param location: Location of file on system.
:param mime_type: Specific mime_type. :param mime_type: Specific mime_type.
:param headers: Custom Headers. :param headers: Custom Headers.
:param filename: Override filename.
:param _range: :param _range:
""" """
filename = path.split(location)[-1] headers = headers or {}
if filename:
headers.setdefault(
'Content-Disposition',
'attachment; filename="{}"'.format(filename))
filename = filename or path.split(location)[-1]
async with open_async(location, mode='rb') as _file: async with open_async(location, mode='rb') as _file:
if _range: if _range:
@ -312,24 +311,30 @@ async def file(location, mime_type=None, headers=None, _range=None):
out_stream = await _file.read() out_stream = await _file.read()
mime_type = mime_type or guess_type(filename)[0] or 'text/plain' mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
return HTTPResponse(status=200, return HTTPResponse(status=200,
headers=headers, headers=headers,
content_type=mime_type, content_type=mime_type,
body_bytes=out_stream) body_bytes=out_stream)
async def file_stream(location, chunk_size=4096, mime_type=None, headers=None, async def file_stream(
_range=None): location, chunk_size=4096, mime_type=None, headers=None,
filename=None, _range=None):
"""Return a streaming response object with file data. """Return a streaming response object with file data.
:param location: Location of file on system. :param location: Location of file on system.
:param chunk_size: The size of each chunk in the stream (in bytes) :param chunk_size: The size of each chunk in the stream (in bytes)
:param mime_type: Specific mime_type. :param mime_type: Specific mime_type.
:param headers: Custom Headers. :param headers: Custom Headers.
:param filename: Override filename.
:param _range: :param _range:
""" """
filename = path.split(location)[-1] headers = headers or {}
if filename:
headers.setdefault(
'Content-Disposition',
'attachment; filename="{}"'.format(filename))
filename = filename or path.split(location)[-1]
_file = await open_async(location, mode='rb') _file = await open_async(location, mode='rb')

View File

@ -93,6 +93,10 @@ class Router:
pattern = 'string' pattern = 'string'
if ':' in parameter_string: if ':' in parameter_string:
name, pattern = parameter_string.split(':', 1) name, pattern = parameter_string.split(':', 1)
if not name:
raise ValueError(
"Invalid parameter syntax: {}".format(parameter_string)
)
default = (str, pattern) default = (str, pattern)
# Pull from pre-configured types # Pull from pre-configured types
@ -115,10 +119,8 @@ class Router:
:return: Nothing :return: Nothing
""" """
if version is not None: if version is not None:
if uri.startswith('/'): version = re.escape(str(version).strip('/').lstrip('v'))
uri = "/".join(["/v{}".format(str(version)), uri[1:]]) uri = "/".join(["/v{}".format(version), uri.lstrip('/')])
else:
uri = "/".join(["/v{}".format(str(version)), uri])
# add regular version # add regular version
self._add(uri, methods, handler, host, name) self._add(uri, methods, handler, host, name)
@ -126,8 +128,15 @@ class Router:
return return
# Add versions with and without trailing / # Add versions with and without trailing /
slashed_methods = self.routes_all.get(uri + '/', frozenset({}))
if isinstance(methods, Iterable):
_slash_is_missing = all(method in slashed_methods for
method in methods)
else:
_slash_is_missing = methods in slashed_methods
slash_is_missing = ( slash_is_missing = (
not uri[-1] == '/' and not self.routes_all.get(uri + '/', False) not uri[-1] == '/' and not _slash_is_missing
) )
without_slash_is_missing = ( without_slash_is_missing = (
uri[-1] == '/' and not uri[-1] == '/' and not

View File

@ -76,7 +76,7 @@ class HttpProtocol(asyncio.Protocol):
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,
response_timeout=60, keep_alive_timeout=15, response_timeout=60, keep_alive_timeout=5,
request_max_size=None, request_class=None, access_log=True, 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):
@ -192,6 +192,7 @@ class HttpProtocol(asyncio.Protocol):
else: else:
logger.info('KeepAlive Timeout. Closing connection.') logger.info('KeepAlive Timeout. Closing connection.')
self.transport.close() self.transport.close()
self.transport = None
# -------------------------------------------- # # -------------------------------------------- #
# Parsing # Parsing
@ -311,7 +312,7 @@ class HttpProtocol(asyncio.Protocol):
else: else:
extra['byte'] = -1 extra['byte'] = -1
if self.request: if self.request is not None:
extra['host'] = '{0}:{1}'.format(self.request.ip[0], extra['host'] = '{0}:{1}'.format(self.request.ip[0],
self.request.ip[1]) self.request.ip[1])
extra['request'] = '{0} {1}'.format(self.request.method, extra['request'] = '{0} {1}'.format(self.request.method,
@ -342,8 +343,10 @@ class HttpProtocol(asyncio.Protocol):
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:
logger.error('Connection lost before response written @ %s', if self._debug:
self.request.ip) logger.error('Connection lost before response written @ %s',
self.request.ip)
keep_alive = False
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(
@ -351,6 +354,7 @@ class HttpProtocol(asyncio.Protocol):
finally: finally:
if not keep_alive: if not keep_alive:
self.transport.close() self.transport.close()
self.transport = None
else: else:
self._keep_alive_timeout_handler = self.loop.call_later( self._keep_alive_timeout_handler = self.loop.call_later(
self.keep_alive_timeout, self.keep_alive_timeout,
@ -379,8 +383,10 @@ class HttpProtocol(asyncio.Protocol):
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:
logger.error('Connection lost before response written @ %s', if self._debug:
self.request.ip) logger.error('Connection lost before response written @ %s',
self.request.ip)
keep_alive = False
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(
@ -388,6 +394,7 @@ class HttpProtocol(asyncio.Protocol):
finally: finally:
if not keep_alive: if not keep_alive:
self.transport.close() self.transport.close()
self.transport = None
else: else:
self._keep_alive_timeout_handler = self.loop.call_later( self._keep_alive_timeout_handler = self.loop.call_later(
self.keep_alive_timeout, self.keep_alive_timeout,
@ -407,8 +414,9 @@ class HttpProtocol(asyncio.Protocol):
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:
logger.error('Connection lost before error written @ %s', if self._debug:
self.request.ip if self.request else 'Unknown') logger.error('Connection lost before error written @ %s',
self.request.ip if self.request else 'Unknown')
except Exception as e: except Exception as e:
self.bail_out( self.bail_out(
"Writing error failed, connection closed {}".format( "Writing error failed, connection closed {}".format(
@ -489,7 +497,7 @@ 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, response_timeout=60, keep_alive_timeout=60, request_timeout=60, response_timeout=60, keep_alive_timeout=5,
ssl=None, sock=None, request_max_size=None, reuse_port=False, ssl=None, sock=None, request_max_size=None, reuse_port=False,
loop=None, protocol=HttpProtocol, backlog=100, loop=None, protocol=HttpProtocol, backlog=100,
register_sys_signals=True, run_async=False, connections=None, register_sys_signals=True, run_async=False, connections=None,
@ -515,6 +523,8 @@ def serve(host, port, request_handler, error_handler, before_start=None,
`app` instance and `loop` `app` instance and `loop`
:param debug: enables debug output (slows server) :param debug: enables debug output (slows server)
:param request_timeout: time in seconds :param request_timeout: time in seconds
:param response_timeout: time in seconds
:param keep_alive_timeout: time in seconds
:param ssl: SSLContext :param ssl: SSLContext
:param sock: Socket for the server to accept connections from :param sock: Socket for the server to accept connections from
:param request_max_size: size in bytes, `None` for no limit :param request_max_size: size in bytes, `None` for no limit
@ -578,7 +588,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 BaseException:
logger.exception("Unable to start server") logger.exception("Unable to start server")
return return

View File

@ -18,7 +18,8 @@ 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, name='static', host=None): stream_large_files, name='static', host=None,
strict_slashes=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
@ -103,7 +104,7 @@ def register(app, uri, file_or_directory, pattern,
if isinstance(stream_large_files, int): if isinstance(stream_large_files, int):
threshold = stream_large_files threshold = stream_large_files
else: else:
threshold = 1024*1000 threshold = 1024 * 1024
if not stats: if not stats:
stats = await stat(file_path) stats = await stat(file_path)
@ -122,4 +123,5 @@ def register(app, uri, file_or_directory, pattern,
if not name.startswith('_static_'): if not name.startswith('_static_'):
name = '_static_{}'.format(name) name = '_static_{}'.format(name)
app.route(uri, methods=['GET', 'HEAD'], name=name, host=host)(_handler) app.route(uri, methods=['GET', 'HEAD'], name=name, host=host,
strict_slashes=strict_slashes)(_handler)

View File

@ -8,8 +8,9 @@ PORT = 42101
class SanicTestClient: class SanicTestClient:
def __init__(self, app): def __init__(self, app, port=PORT):
self.app = app self.app = app
self.port = port
async def _local_request(self, method, uri, cookies=None, *args, **kwargs): async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
import aiohttp import aiohttp
@ -17,7 +18,7 @@ class SanicTestClient:
url = uri url = uri
else: else:
url = 'http://{host}:{port}{uri}'.format( url = 'http://{host}:{port}{uri}'.format(
host=HOST, port=PORT, uri=uri) host=HOST, port=self.port, uri=uri)
logger.info(url) logger.info(url)
conn = aiohttp.TCPConnector(verify_ssl=False) conn = aiohttp.TCPConnector(verify_ssl=False)
@ -66,7 +67,7 @@ class SanicTestClient:
exceptions.append(e) exceptions.append(e)
self.app.stop() self.app.stop()
self.app.run(host=HOST, debug=debug, port=PORT, **server_kwargs) self.app.run(host=HOST, debug=debug, port=self.port, **server_kwargs)
self.app.listeners['after_server_start'].pop() self.app.listeners['after_server_start'].pop()
if exceptions: if exceptions:
@ -76,14 +77,14 @@ class SanicTestClient:
try: try:
request, response = results request, response = results
return request, response return request, response
except: except BaseException:
raise ValueError( raise ValueError(
"Request and response object expected, got ({})".format( "Request and response object expected, got ({})".format(
results)) results))
else: else:
try: try:
return results[-1] return results[-1]
except: except BaseException:
raise ValueError( raise ValueError(
"Request object expected, got ({})".format(results)) "Request object expected, got ({})".format(results))

View File

@ -13,10 +13,18 @@ class WebSocketProtocol(HttpProtocol):
self.websocket_max_size = websocket_max_size self.websocket_max_size = websocket_max_size
self.websocket_max_queue = websocket_max_queue self.websocket_max_queue = websocket_max_queue
def connection_timeout(self): # timeouts make no sense for websocket routes
# timeouts make no sense for websocket routes def request_timeout_callback(self):
if self.websocket is None: if self.websocket is None:
super().connection_timeout() super().request_timeout_callback()
def response_timeout_callback(self):
if self.websocket is None:
super().response_timeout_callback()
def keep_alive_timeout_callback(self):
if self.websocket is None:
super().keep_alive_timeout_callback()
def connection_lost(self, exc): def connection_lost(self, exc):
if self.websocket is not None: if self.websocket is not None:
@ -82,4 +90,5 @@ class WebSocketProtocol(HttpProtocol):
) )
self.websocket.subprotocol = subprotocol self.websocket.subprotocol = subprotocol
self.websocket.connection_made(request.transport) self.websocket.connection_made(request.transport)
self.websocket.connection_open()
return self.websocket return self.websocket

View File

@ -23,6 +23,9 @@ from sanic.websocket import WebSocketProtocol
class GunicornWorker(base.Worker): class GunicornWorker(base.Worker):
http_protocol = HttpProtocol
websocket_protocol = WebSocketProtocol
def __init__(self, *args, **kw): # pragma: no cover def __init__(self, *args, **kw): # pragma: no cover
super().__init__(*args, **kw) super().__init__(*args, **kw)
cfg = self.cfg cfg = self.cfg
@ -46,8 +49,9 @@ class GunicornWorker(base.Worker):
def run(self): def run(self):
is_debug = self.log.loglevel == logging.DEBUG is_debug = self.log.loglevel == logging.DEBUG
protocol = (WebSocketProtocol if self.app.callable.websocket_enabled protocol = (
else HttpProtocol) self.websocket_protocol if self.app.callable.websocket_enabled
else self.http_protocol)
self._server_settings = self.app.callable._helper( self._server_settings = self.app.callable._helper(
loop=self.loop, loop=self.loop,
debug=is_debug, debug=is_debug,
@ -70,13 +74,13 @@ class GunicornWorker(base.Worker):
trigger_events(self._server_settings.get('before_stop', []), trigger_events(self._server_settings.get('before_stop', []),
self.loop) self.loop)
self.loop.run_until_complete(self.close()) self.loop.run_until_complete(self.close())
except: except BaseException:
traceback.print_exc() traceback.print_exc()
finally: finally:
try: try:
trigger_events(self._server_settings.get('after_stop', []), trigger_events(self._server_settings.get('after_stop', []),
self.loop) self.loop)
except: except BaseException:
traceback.print_exc() traceback.print_exc()
finally: finally:
self.loop.close() self.loop.close()

View File

@ -58,11 +58,11 @@ def exception_app():
raise InvalidUsage("OK") raise InvalidUsage("OK")
@app.route('/abort/401') @app.route('/abort/401')
def handler_invalid(request): def handler_401_error(request):
abort(401) abort(401)
@app.route('/abort') @app.route('/abort')
def handler_invalid(request): def handler_500_error(request):
abort(500) abort(500)
return text("OK") return text("OK")
@ -186,7 +186,7 @@ def test_exception_in_exception_handler_debug_off(exception_app):
assert response.body == b'An error occurred while handling an error' assert response.body == b'An error occurred while handling an error'
def test_exception_in_exception_handler_debug_off(exception_app): def test_exception_in_exception_handler_debug_on(exception_app):
"""Test that an exception thrown in an error handler is handled""" """Test that an exception thrown in an error handler is handled"""
request, response = exception_app.test_client.get( request, response = exception_app.test_client.get(
'/error_in_error_handler_handler', '/error_in_error_handler_handler',

View File

@ -1,11 +1,17 @@
import uuid import uuid
import logging
from io import StringIO
from importlib import reload from importlib import reload
import pytest
from unittest.mock import Mock
import sanic
from sanic.response import text from sanic.response import text
from sanic.log import LOGGING_CONFIG_DEFAULTS from sanic.log import LOGGING_CONFIG_DEFAULTS
from sanic import Sanic from sanic import Sanic
from io import StringIO
import logging
logging_format = '''module: %(module)s; \ logging_format = '''module: %(module)s; \
function: %(funcName)s(); \ function: %(funcName)s(); \
@ -71,3 +77,31 @@ def test_logging_pass_customer_logconfig():
for fmt in [h.formatter for h in logging.getLogger('sanic.access').handlers]: for fmt in [h.formatter for h in logging.getLogger('sanic.access').handlers]:
assert fmt._fmt == modified_config['formatters']['access']['format'] assert fmt._fmt == modified_config['formatters']['access']['format']
@pytest.mark.parametrize('debug', (True, False, ))
def test_log_connection_lost(debug, monkeypatch):
""" Should not log Connection lost exception on non debug """
app = Sanic('connection_lost')
stream = StringIO()
root = logging.getLogger('root')
root.addHandler(logging.StreamHandler(stream))
monkeypatch.setattr(sanic.server, 'logger', root)
@app.route('/conn_lost')
async def conn_lost(request):
response = text('Ok')
response.output = Mock(side_effect=RuntimeError)
return response
with pytest.raises(ValueError):
# catch ValueError: Exception during request
app.test_client.get('/conn_lost', debug=debug)
log = stream.getvalue()
if debug:
assert log.startswith(
'Connection lost before response written @')
else:
assert 'Connection lost before response written @' not in log

View File

@ -59,7 +59,7 @@ def test_middleware_response_exception():
result = {'status_code': None} result = {'status_code': None}
@app.middleware('response') @app.middleware('response')
async def process_response(reqest, response): async def process_response(request, response):
result['status_code'] = response.status result['status_code'] = response.status
return response return response

View File

@ -27,6 +27,16 @@ def test_sync():
assert response.text == 'Hello' assert response.text == 'Hello'
def test_remote_address():
app = Sanic('test_text')
@app.route('/')
def handler(request):
return text("{}".format(request.ip))
request, response = app.test_client.get('/')
assert response.text == '127.0.0.1'
def test_text(): def test_text():
app = Sanic('test_text') app = Sanic('test_text')

View File

@ -149,7 +149,22 @@ def test_file_response(file_name, static_file_directory):
request, response = app.test_client.get('/files/{}'.format(file_name)) request, response = app.test_client.get('/files/{}'.format(file_name))
assert response.status == 200 assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name) assert response.body == get_file_content(static_file_directory, file_name)
assert 'Content-Disposition' not in response.headers
@pytest.mark.parametrize('source,dest', [
('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'), ('python.png', 'logo.png')])
def test_file_response_custom_filename(source, dest, static_file_directory):
app = Sanic('test_file_helper')
@app.route('/files/<filename>', methods=['GET'])
def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
file_path = os.path.abspath(unquote(file_path))
return file(file_path, filename=dest)
request, response = app.test_client.get('/files/{}'.format(source))
assert response.status == 200
assert response.body == get_file_content(static_file_directory, source)
assert response.headers['Content-Disposition'] == 'attachment; filename="{}"'.format(dest)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_file_head_response(file_name, static_file_directory): def test_file_head_response(file_name, static_file_directory):
@ -191,7 +206,22 @@ def test_file_stream_response(file_name, static_file_directory):
request, response = app.test_client.get('/files/{}'.format(file_name)) request, response = app.test_client.get('/files/{}'.format(file_name))
assert response.status == 200 assert response.status == 200
assert response.body == get_file_content(static_file_directory, file_name) assert response.body == get_file_content(static_file_directory, file_name)
assert 'Content-Disposition' not in response.headers
@pytest.mark.parametrize('source,dest', [
('test.file', 'my_file.txt'), ('decode me.txt', 'readme.md'), ('python.png', 'logo.png')])
def test_file_stream_response_custom_filename(source, dest, static_file_directory):
app = Sanic('test_file_helper')
@app.route('/files/<filename>', methods=['GET'])
def file_route(request, filename):
file_path = os.path.join(static_file_directory, filename)
file_path = os.path.abspath(unquote(file_path))
return file_stream(file_path, chunk_size=32, filename=dest)
request, response = app.test_client.get('/files/{}'.format(source))
assert response.status == 200
assert response.body == get_file_content(static_file_directory, source)
assert response.headers['Content-Disposition'] == 'attachment; filename="{}"'.format(dest)
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt']) @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt'])
def test_file_stream_head_response(file_name, static_file_directory): def test_file_stream_head_response(file_name, static_file_directory):

View File

@ -44,6 +44,24 @@ def test_shorthand_routes_get():
request, response = app.test_client.post('/get') request, response = app.test_client.post('/get')
assert response.status == 405 assert response.status == 405
def test_shorthand_routes_multiple():
app = Sanic('test_shorthand_routes_multiple')
@app.get('/get')
def get_handler(request):
return text('OK')
@app.options('/get')
def options_handler(request):
return text('')
request, response = app.test_client.get('/get/')
assert response.status == 200
assert response.text == 'OK'
request, response = app.test_client.options('/get/')
assert response.status == 200
def test_route_strict_slash(): def test_route_strict_slash():
app = Sanic('test_route_strict_slash') app = Sanic('test_route_strict_slash')
@ -71,6 +89,16 @@ def test_route_strict_slash():
request, response = app.test_client.post('/post') request, response = app.test_client.post('/post')
assert response.status == 404 assert response.status == 404
def test_route_invalid_parameter_syntax():
with pytest.raises(ValueError):
app = Sanic('test_route_invalid_param_syntax')
@app.get('/get/<:string>', strict_slashes=True)
def handler(request):
return text('OK')
request, response = app.test_client.get('/get')
def test_route_strict_slash_default_value(): def test_route_strict_slash_default_value():
app = Sanic('test_route_strict_slash', strict_slashes=True) app = Sanic('test_route_strict_slash', strict_slashes=True)
@ -744,6 +772,7 @@ def test_remove_route_without_clean_cache():
assert response.status == 200 assert response.status == 200
app.remove_route('/test', clean_cache=True) app.remove_route('/test', clean_cache=True)
app.remove_route('/test/', clean_cache=True)
request, response = app.test_client.get('/test') request, response = app.test_client.get('/test')
assert response.status == 404 assert response.status == 404

View File

@ -164,7 +164,7 @@ def test_static_content_range_error(file_name, static_file_directory):
@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png']) @pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png'])
def test_static_file(static_file_directory, file_name): def test_static_file_specified_host(static_file_directory, file_name):
app = Sanic('test_static') app = Sanic('test_static')
app.static( app.static(
'/testing.file', '/testing.file',

View File

@ -5,7 +5,7 @@ from sanic import Sanic
from sanic.response import text from sanic.response import text
from sanic.views import HTTPMethodView from sanic.views import HTTPMethodView
from sanic.blueprints import Blueprint from sanic.blueprints import Blueprint
from sanic.testing import PORT as test_port from sanic.testing import PORT as test_port, HOST as test_host
from sanic.exceptions import URLBuildError from sanic.exceptions import URLBuildError
import string import string
@ -15,11 +15,11 @@ URL_FOR_VALUE1 = '/myurl?arg1=v1&arg1=v2'
URL_FOR_ARGS2 = dict(arg1=['v1', 'v2'], _anchor='anchor') URL_FOR_ARGS2 = dict(arg1=['v1', 'v2'], _anchor='anchor')
URL_FOR_VALUE2 = '/myurl?arg1=v1&arg1=v2#anchor' 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='{}:{}'.format(test_host, test_port), _external=True)
URL_FOR_VALUE3 = 'http://localhost:{}/myurl?arg1=v1#anchor'.format(test_port) URL_FOR_VALUE3 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host, test_port)
URL_FOR_ARGS4 = dict(arg1='v1', _anchor='anchor', _external=True, URL_FOR_ARGS4 = dict(arg1='v1', _anchor='anchor', _external=True,
_server='http://localhost:{}'.format(test_port),) _server='http://{}:{}'.format(test_host, test_port),)
URL_FOR_VALUE4 = 'http://localhost:{}/myurl?arg1=v1#anchor'.format(test_port) URL_FOR_VALUE4 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host, test_port)
def _generate_handlers_from_names(app, l): def _generate_handlers_from_names(app, l):