Merge pull request #15 from channelcat/master
Merge upstream master branch
This commit is contained in:
commit
472bbcf293
|
@ -1,4 +1,7 @@
|
|||
include README.rst
|
||||
include MANIFEST.in
|
||||
include LICENSE
|
||||
include setup.py
|
||||
|
||||
recursive-exclude * __pycache__
|
||||
recursive-exclude * *.py[co]
|
||||
|
|
|
@ -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.
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ----------------- | --------- | --------------------------------- |
|
||||
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
||||
| REQUEST_TIMEOUT | 60 | How long a request can take (sec) |
|
||||
| KEEP_ALIVE | True | Disables keep-alive when False |
|
||||
| Variable | Default | Description |
|
||||
| ------------------ | --------- | --------------------------------------------- |
|
||||
| REQUEST_MAX_SIZE | 100000000 | How big a request may be (bytes) |
|
||||
| REQUEST_TIMEOUT | 60 | How long a request can take to arrive (sec) |
|
||||
| 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
|
||||
```
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Extensions
|
||||
|
||||
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.
|
||||
Allows using redis, memcache or an in memory store.
|
||||
- [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors.
|
||||
|
|
|
@ -75,6 +75,10 @@ The following variables are accessible as properties on `Request` objects:
|
|||
|
||||
- `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.
|
||||
|
||||
```python
|
||||
|
|
49
examples/pytest_xdist.py
Normal file
49
examples/pytest_xdist.py
Normal 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'
|
24
sanic/app.py
24
sanic/app.py
|
@ -28,7 +28,8 @@ class Sanic:
|
|||
|
||||
def __init__(self, name=None, router=None, error_handler=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
|
||||
if name is None:
|
||||
|
@ -36,7 +37,8 @@ class Sanic:
|
|||
name = getmodulename(frame_records[1])
|
||||
|
||||
# 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.router = router or Router()
|
||||
|
@ -47,6 +49,7 @@ class Sanic:
|
|||
self.response_middleware = deque()
|
||||
self.blueprints = {}
|
||||
self._blueprint_order = []
|
||||
self.configure_logging = configure_logging
|
||||
self.debug = None
|
||||
self.sock = None
|
||||
self.strict_slashes = strict_slashes
|
||||
|
@ -54,7 +57,7 @@ class Sanic:
|
|||
self.is_running = False
|
||||
self.is_request_stream = False
|
||||
self.websocket_enabled = False
|
||||
self.websocket_tasks = []
|
||||
self.websocket_tasks = set()
|
||||
|
||||
# Register alternative method names
|
||||
self.go_fast = self.run
|
||||
|
@ -259,7 +262,7 @@ class Sanic:
|
|||
# its future is kept in self.websocket_tasks in case it
|
||||
# needs to be cancelled due to the server being stopped
|
||||
fut = ensure_future(handler(request, ws, *args, **kwargs))
|
||||
self.websocket_tasks.append(fut)
|
||||
self.websocket_tasks.add(fut)
|
||||
try:
|
||||
await fut
|
||||
except (CancelledError, ConnectionClosed):
|
||||
|
@ -345,13 +348,14 @@ class Sanic:
|
|||
# Static Files
|
||||
def static(self, uri, file_or_directory, pattern=r'/?.+',
|
||||
use_modified_since=True, use_content_range=False,
|
||||
stream_large_files=False, 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
|
||||
file or a directory. See
|
||||
"""
|
||||
static_register(self, uri, file_or_directory, pattern,
|
||||
use_modified_since, use_content_range,
|
||||
stream_large_files, name, host)
|
||||
stream_large_files, name, host, strict_slashes)
|
||||
|
||||
def blueprint(self, blueprint, **options):
|
||||
"""Register a blueprint on the application.
|
||||
|
@ -574,9 +578,9 @@ class Sanic:
|
|||
try:
|
||||
response = await self._run_response_middleware(request,
|
||||
response)
|
||||
except:
|
||||
except BaseException:
|
||||
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
|
||||
|
@ -642,7 +646,7 @@ class Sanic:
|
|||
serve(**server_settings)
|
||||
else:
|
||||
serve_multiple(server_settings, workers)
|
||||
except:
|
||||
except BaseException:
|
||||
error_logger.exception(
|
||||
'Experienced exception while trying to serve')
|
||||
raise
|
||||
|
@ -793,7 +797,7 @@ class Sanic:
|
|||
listeners = [partial(listener, self) for listener in listeners]
|
||||
server_settings[settings_name] = listeners
|
||||
|
||||
if debug:
|
||||
if self.configure_logging and debug:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
if self.config.LOGO is not None:
|
||||
logger.debug(self.config.LOGO)
|
||||
|
|
|
@ -221,8 +221,12 @@ class Blueprint:
|
|||
name = kwargs.pop('name', 'static')
|
||||
if not name.startswith(self.name + '.'):
|
||||
name = '{}.{}'.format(self.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)
|
||||
self.statics.append(static)
|
||||
|
||||
|
|
|
@ -33,12 +33,6 @@ class Config(dict):
|
|||
self.REQUEST_TIMEOUT = 60 # 60 seconds
|
||||
self.RESPONSE_TIMEOUT = 60 # 60 seconds
|
||||
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_QUEUE = 32
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from sanic.response import ALL_STATUS_CODES, COMMON_STATUS_CODES
|
||||
from sanic.response import STATUS_CODES
|
||||
|
||||
TRACEBACK_STYLE = '''
|
||||
<style>
|
||||
|
@ -275,8 +275,7 @@ def abort(status_code, message=None):
|
|||
in response.py for the given status code.
|
||||
"""
|
||||
if message is None:
|
||||
message = COMMON_STATUS_CODES.get(status_code,
|
||||
ALL_STATUS_CODES.get(status_code))
|
||||
message = STATUS_CODES.get(status_code)
|
||||
# These are stored as bytes in the STATUS_CODES dict
|
||||
message = message.decode('utf8')
|
||||
sanic_exception = _sanic_exceptions.get(status_code, SanicException)
|
||||
|
|
|
@ -46,7 +46,8 @@ class Request(dict):
|
|||
__slots__ = (
|
||||
'app', 'headers', 'version', 'method', '_cookies', 'transport',
|
||||
'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):
|
||||
|
@ -167,11 +168,27 @@ class Request(dict):
|
|||
|
||||
@property
|
||||
def ip(self):
|
||||
if not hasattr(self, '_ip'):
|
||||
self._ip = (self.transport.get_extra_info('peername') or
|
||||
(None, None))
|
||||
if not hasattr(self, '_socket'):
|
||||
self._get_address()
|
||||
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
|
||||
def remote_addr(self):
|
||||
"""Attempt to return the original client ip based on X-Forwarded-For.
|
||||
|
|
|
@ -3,20 +3,14 @@ from os import path
|
|||
|
||||
try:
|
||||
from ujson import dumps as json_dumps
|
||||
except:
|
||||
except BaseException:
|
||||
from json import dumps as json_dumps
|
||||
|
||||
from aiofiles import open as open_async
|
||||
|
||||
from sanic.cookies import CookieJar
|
||||
|
||||
COMMON_STATUS_CODES = {
|
||||
200: b'OK',
|
||||
400: b'Bad Request',
|
||||
404: b'Not Found',
|
||||
500: b'Internal Server Error',
|
||||
}
|
||||
ALL_STATUS_CODES = {
|
||||
STATUS_CODES = {
|
||||
100: b'Continue',
|
||||
101: b'Switching Protocols',
|
||||
102: b'Processing',
|
||||
|
@ -162,11 +156,10 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
|||
|
||||
headers = self._parse_headers()
|
||||
|
||||
# Try to pull from the common codes first
|
||||
# Speeds up response rate 6% over pulling from all
|
||||
status = COMMON_STATUS_CODES.get(self.status)
|
||||
if not status:
|
||||
status = ALL_STATUS_CODES.get(self.status)
|
||||
if self.status is 200:
|
||||
status = b'OK'
|
||||
else:
|
||||
status = STATUS_CODES.get(self.status)
|
||||
|
||||
return (b'HTTP/%b %d %b\r\n'
|
||||
b'%b'
|
||||
|
@ -209,11 +202,10 @@ class HTTPResponse(BaseHTTPResponse):
|
|||
|
||||
headers = self._parse_headers()
|
||||
|
||||
# Try to pull from the common codes first
|
||||
# Speeds up response rate 6% over pulling from all
|
||||
status = COMMON_STATUS_CODES.get(self.status)
|
||||
if not status:
|
||||
status = ALL_STATUS_CODES.get(self.status, b'UNKNOWN RESPONSE')
|
||||
if self.status is 200:
|
||||
status = b'OK'
|
||||
else:
|
||||
status = STATUS_CODES.get(self.status, b'UNKNOWN RESPONSE')
|
||||
|
||||
return (b'HTTP/%b %d %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")
|
||||
|
||||
|
||||
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.
|
||||
|
||||
:param location: Location of file on system.
|
||||
:param mime_type: Specific mime_type.
|
||||
:param headers: Custom Headers.
|
||||
:param filename: Override filename.
|
||||
: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:
|
||||
if _range:
|
||||
|
@ -312,24 +311,30 @@ async def file(location, mime_type=None, headers=None, _range=None):
|
|||
out_stream = await _file.read()
|
||||
|
||||
mime_type = mime_type or guess_type(filename)[0] or 'text/plain'
|
||||
|
||||
return HTTPResponse(status=200,
|
||||
headers=headers,
|
||||
content_type=mime_type,
|
||||
body_bytes=out_stream)
|
||||
|
||||
|
||||
async def file_stream(location, chunk_size=4096, mime_type=None, headers=None,
|
||||
_range=None):
|
||||
async def file_stream(
|
||||
location, chunk_size=4096, mime_type=None, headers=None,
|
||||
filename=None, _range=None):
|
||||
"""Return a streaming response object with file data.
|
||||
|
||||
:param location: Location of file on system.
|
||||
:param chunk_size: The size of each chunk in the stream (in bytes)
|
||||
:param mime_type: Specific mime_type.
|
||||
:param headers: Custom Headers.
|
||||
:param filename: Override filename.
|
||||
: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')
|
||||
|
||||
|
|
|
@ -93,6 +93,10 @@ class Router:
|
|||
pattern = 'string'
|
||||
if ':' in parameter_string:
|
||||
name, pattern = parameter_string.split(':', 1)
|
||||
if not name:
|
||||
raise ValueError(
|
||||
"Invalid parameter syntax: {}".format(parameter_string)
|
||||
)
|
||||
|
||||
default = (str, pattern)
|
||||
# Pull from pre-configured types
|
||||
|
@ -115,10 +119,8 @@ class Router:
|
|||
:return: Nothing
|
||||
"""
|
||||
if version is not None:
|
||||
if uri.startswith('/'):
|
||||
uri = "/".join(["/v{}".format(str(version)), uri[1:]])
|
||||
else:
|
||||
uri = "/".join(["/v{}".format(str(version)), uri])
|
||||
version = re.escape(str(version).strip('/').lstrip('v'))
|
||||
uri = "/".join(["/v{}".format(version), uri.lstrip('/')])
|
||||
# add regular version
|
||||
self._add(uri, methods, handler, host, name)
|
||||
|
||||
|
@ -126,8 +128,15 @@ class Router:
|
|||
return
|
||||
|
||||
# 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 = (
|
||||
not uri[-1] == '/' and not self.routes_all.get(uri + '/', False)
|
||||
not uri[-1] == '/' and not _slash_is_missing
|
||||
)
|
||||
without_slash_is_missing = (
|
||||
uri[-1] == '/' and not
|
||||
|
|
|
@ -76,7 +76,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||
|
||||
def __init__(self, *, loop, request_handler, error_handler,
|
||||
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,
|
||||
keep_alive=True, is_request_stream=False, router=None,
|
||||
state=None, debug=False, **kwargs):
|
||||
|
@ -192,6 +192,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||
else:
|
||||
logger.info('KeepAlive Timeout. Closing connection.')
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
|
||||
# -------------------------------------------- #
|
||||
# Parsing
|
||||
|
@ -311,7 +312,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||
else:
|
||||
extra['byte'] = -1
|
||||
|
||||
if self.request:
|
||||
if self.request is not None:
|
||||
extra['host'] = '{0}:{1}'.format(self.request.ip[0],
|
||||
self.request.ip[1])
|
||||
extra['request'] = '{0} {1}'.format(self.request.method,
|
||||
|
@ -342,8 +343,10 @@ class HttpProtocol(asyncio.Protocol):
|
|||
self.url, type(response))
|
||||
self.write_error(ServerError('Invalid response type'))
|
||||
except RuntimeError:
|
||||
logger.error('Connection lost before response written @ %s',
|
||||
self.request.ip)
|
||||
if self._debug:
|
||||
logger.error('Connection lost before response written @ %s',
|
||||
self.request.ip)
|
||||
keep_alive = False
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing response failed, connection closed {}".format(
|
||||
|
@ -351,6 +354,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||
finally:
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
else:
|
||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||
self.keep_alive_timeout,
|
||||
|
@ -379,8 +383,10 @@ class HttpProtocol(asyncio.Protocol):
|
|||
self.url, type(response))
|
||||
self.write_error(ServerError('Invalid response type'))
|
||||
except RuntimeError:
|
||||
logger.error('Connection lost before response written @ %s',
|
||||
self.request.ip)
|
||||
if self._debug:
|
||||
logger.error('Connection lost before response written @ %s',
|
||||
self.request.ip)
|
||||
keep_alive = False
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing response failed, connection closed {}".format(
|
||||
|
@ -388,6 +394,7 @@ class HttpProtocol(asyncio.Protocol):
|
|||
finally:
|
||||
if not keep_alive:
|
||||
self.transport.close()
|
||||
self.transport = None
|
||||
else:
|
||||
self._keep_alive_timeout_handler = self.loop.call_later(
|
||||
self.keep_alive_timeout,
|
||||
|
@ -407,8 +414,9 @@ class HttpProtocol(asyncio.Protocol):
|
|||
version = self.request.version if self.request else '1.1'
|
||||
self.transport.write(response.output(version))
|
||||
except RuntimeError:
|
||||
logger.error('Connection lost before error written @ %s',
|
||||
self.request.ip if self.request else 'Unknown')
|
||||
if self._debug:
|
||||
logger.error('Connection lost before error written @ %s',
|
||||
self.request.ip if self.request else 'Unknown')
|
||||
except Exception as e:
|
||||
self.bail_out(
|
||||
"Writing error failed, connection closed {}".format(
|
||||
|
@ -489,7 +497,7 @@ def trigger_events(events, loop):
|
|||
|
||||
def serve(host, port, request_handler, error_handler, before_start=None,
|
||||
after_start=None, before_stop=None, after_stop=None, debug=False,
|
||||
request_timeout=60, 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,
|
||||
loop=None, protocol=HttpProtocol, backlog=100,
|
||||
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`
|
||||
:param debug: enables debug output (slows server)
|
||||
:param request_timeout: time in seconds
|
||||
:param response_timeout: time in seconds
|
||||
:param keep_alive_timeout: time in seconds
|
||||
:param ssl: SSLContext
|
||||
:param sock: Socket for the server to accept connections from
|
||||
: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:
|
||||
http_server = loop.run_until_complete(server_coroutine)
|
||||
except:
|
||||
except BaseException:
|
||||
logger.exception("Unable to start server")
|
||||
return
|
||||
|
||||
|
|
|
@ -18,7 +18,8 @@ from sanic.response import file, file_stream, HTTPResponse
|
|||
|
||||
def register(app, uri, file_or_directory, pattern,
|
||||
use_modified_since, use_content_range,
|
||||
stream_large_files, 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
|
||||
# make a good effort here. Modified-since is nice, but we could
|
||||
# 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):
|
||||
threshold = stream_large_files
|
||||
else:
|
||||
threshold = 1024*1000
|
||||
threshold = 1024 * 1024
|
||||
|
||||
if not stats:
|
||||
stats = await stat(file_path)
|
||||
|
@ -122,4 +123,5 @@ def register(app, uri, file_or_directory, pattern,
|
|||
if not name.startswith('_static_'):
|
||||
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)
|
||||
|
|
|
@ -8,8 +8,9 @@ PORT = 42101
|
|||
|
||||
|
||||
class SanicTestClient:
|
||||
def __init__(self, app):
|
||||
def __init__(self, app, port=PORT):
|
||||
self.app = app
|
||||
self.port = port
|
||||
|
||||
async def _local_request(self, method, uri, cookies=None, *args, **kwargs):
|
||||
import aiohttp
|
||||
|
@ -17,7 +18,7 @@ class SanicTestClient:
|
|||
url = uri
|
||||
else:
|
||||
url = 'http://{host}:{port}{uri}'.format(
|
||||
host=HOST, port=PORT, uri=uri)
|
||||
host=HOST, port=self.port, uri=uri)
|
||||
|
||||
logger.info(url)
|
||||
conn = aiohttp.TCPConnector(verify_ssl=False)
|
||||
|
@ -66,7 +67,7 @@ class SanicTestClient:
|
|||
exceptions.append(e)
|
||||
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()
|
||||
|
||||
if exceptions:
|
||||
|
@ -76,14 +77,14 @@ class SanicTestClient:
|
|||
try:
|
||||
request, response = results
|
||||
return request, response
|
||||
except:
|
||||
except BaseException:
|
||||
raise ValueError(
|
||||
"Request and response object expected, got ({})".format(
|
||||
results))
|
||||
else:
|
||||
try:
|
||||
return results[-1]
|
||||
except:
|
||||
except BaseException:
|
||||
raise ValueError(
|
||||
"Request object expected, got ({})".format(results))
|
||||
|
||||
|
|
|
@ -13,10 +13,18 @@ class WebSocketProtocol(HttpProtocol):
|
|||
self.websocket_max_size = websocket_max_size
|
||||
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:
|
||||
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):
|
||||
if self.websocket is not None:
|
||||
|
@ -82,4 +90,5 @@ class WebSocketProtocol(HttpProtocol):
|
|||
)
|
||||
self.websocket.subprotocol = subprotocol
|
||||
self.websocket.connection_made(request.transport)
|
||||
self.websocket.connection_open()
|
||||
return self.websocket
|
||||
|
|
|
@ -23,6 +23,9 @@ from sanic.websocket import WebSocketProtocol
|
|||
|
||||
class GunicornWorker(base.Worker):
|
||||
|
||||
http_protocol = HttpProtocol
|
||||
websocket_protocol = WebSocketProtocol
|
||||
|
||||
def __init__(self, *args, **kw): # pragma: no cover
|
||||
super().__init__(*args, **kw)
|
||||
cfg = self.cfg
|
||||
|
@ -46,8 +49,9 @@ class GunicornWorker(base.Worker):
|
|||
|
||||
def run(self):
|
||||
is_debug = self.log.loglevel == logging.DEBUG
|
||||
protocol = (WebSocketProtocol if self.app.callable.websocket_enabled
|
||||
else HttpProtocol)
|
||||
protocol = (
|
||||
self.websocket_protocol if self.app.callable.websocket_enabled
|
||||
else self.http_protocol)
|
||||
self._server_settings = self.app.callable._helper(
|
||||
loop=self.loop,
|
||||
debug=is_debug,
|
||||
|
@ -70,13 +74,13 @@ class GunicornWorker(base.Worker):
|
|||
trigger_events(self._server_settings.get('before_stop', []),
|
||||
self.loop)
|
||||
self.loop.run_until_complete(self.close())
|
||||
except:
|
||||
except BaseException:
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
try:
|
||||
trigger_events(self._server_settings.get('after_stop', []),
|
||||
self.loop)
|
||||
except:
|
||||
except BaseException:
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
self.loop.close()
|
||||
|
|
|
@ -58,11 +58,11 @@ def exception_app():
|
|||
raise InvalidUsage("OK")
|
||||
|
||||
@app.route('/abort/401')
|
||||
def handler_invalid(request):
|
||||
def handler_401_error(request):
|
||||
abort(401)
|
||||
|
||||
@app.route('/abort')
|
||||
def handler_invalid(request):
|
||||
def handler_500_error(request):
|
||||
abort(500)
|
||||
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'
|
||||
|
||||
|
||||
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"""
|
||||
request, response = exception_app.test_client.get(
|
||||
'/error_in_error_handler_handler',
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
import uuid
|
||||
import logging
|
||||
|
||||
from io import StringIO
|
||||
from importlib import reload
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock
|
||||
|
||||
import sanic
|
||||
from sanic.response import text
|
||||
from sanic.log import LOGGING_CONFIG_DEFAULTS
|
||||
from sanic import Sanic
|
||||
from io import StringIO
|
||||
import logging
|
||||
|
||||
|
||||
logging_format = '''module: %(module)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]:
|
||||
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
|
||||
|
|
|
@ -59,7 +59,7 @@ def test_middleware_response_exception():
|
|||
result = {'status_code': None}
|
||||
|
||||
@app.middleware('response')
|
||||
async def process_response(reqest, response):
|
||||
async def process_response(request, response):
|
||||
result['status_code'] = response.status
|
||||
return response
|
||||
|
||||
|
|
|
@ -27,6 +27,16 @@ def test_sync():
|
|||
|
||||
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():
|
||||
app = Sanic('test_text')
|
||||
|
|
|
@ -149,7 +149,22 @@ def test_file_response(file_name, static_file_directory):
|
|||
request, response = app.test_client.get('/files/{}'.format(file_name))
|
||||
assert response.status == 200
|
||||
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'])
|
||||
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))
|
||||
assert response.status == 200
|
||||
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'])
|
||||
def test_file_stream_head_response(file_name, static_file_directory):
|
||||
|
|
|
@ -44,6 +44,24 @@ def test_shorthand_routes_get():
|
|||
request, response = app.test_client.post('/get')
|
||||
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():
|
||||
app = Sanic('test_route_strict_slash')
|
||||
|
||||
|
@ -71,6 +89,16 @@ def test_route_strict_slash():
|
|||
request, response = app.test_client.post('/post')
|
||||
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():
|
||||
app = Sanic('test_route_strict_slash', strict_slashes=True)
|
||||
|
||||
|
@ -421,7 +449,7 @@ def test_websocket_route_with_subprotocols():
|
|||
'Sec-WebSocket-Key': 'dGhlIHNhbXBsZSBub25jZQ==',
|
||||
'Sec-WebSocket-Version': '13'})
|
||||
assert response.status == 101
|
||||
|
||||
|
||||
assert results == ['bar', 'bar', None, None]
|
||||
|
||||
|
||||
|
@ -744,6 +772,7 @@ def test_remove_route_without_clean_cache():
|
|||
assert response.status == 200
|
||||
|
||||
app.remove_route('/test', clean_cache=True)
|
||||
app.remove_route('/test/', clean_cache=True)
|
||||
|
||||
request, response = app.test_client.get('/test')
|
||||
assert response.status == 404
|
||||
|
|
|
@ -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'])
|
||||
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.static(
|
||||
'/testing.file',
|
||||
|
|
|
@ -5,7 +5,7 @@ from sanic import Sanic
|
|||
from sanic.response import text
|
||||
from sanic.views import HTTPMethodView
|
||||
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
|
||||
|
||||
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_VALUE2 = '/myurl?arg1=v1&arg1=v2#anchor'
|
||||
URL_FOR_ARGS3 = dict(arg1='v1', _anchor='anchor', _scheme='http',
|
||||
_server='localhost:{}'.format(test_port), _external=True)
|
||||
URL_FOR_VALUE3 = 'http://localhost:{}/myurl?arg1=v1#anchor'.format(test_port)
|
||||
_server='{}:{}'.format(test_host, test_port), _external=True)
|
||||
URL_FOR_VALUE3 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host, 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)
|
||||
_server='http://{}:{}'.format(test_host, test_port),)
|
||||
URL_FOR_VALUE4 = 'http://{}:{}/myurl?arg1=v1#anchor'.format(test_host, test_port)
|
||||
|
||||
|
||||
def _generate_handlers_from_names(app, l):
|
||||
|
|
Loading…
Reference in New Issue
Block a user