diff --git a/requirements-dev.txt b/requirements-dev.txt index 28014eb6..163df025 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,6 @@ aiofiles aiohttp==1.3.5 +chardet<=2.3.0 beautifulsoup4 coverage httptools diff --git a/sanic/__init__.py b/sanic/__init__.py index b67a3a6a..7bf0994a 100644 --- a/sanic/__init__.py +++ b/sanic/__init__.py @@ -1,6 +1,6 @@ from sanic.app import Sanic from sanic.blueprints import Blueprint -__version__ = '0.4.1' +__version__ = '0.5.0' __all__ = ['Sanic', 'Blueprint'] diff --git a/sanic/__main__.py b/sanic/__main__.py index 322d735d..b6a66b8e 100644 --- a/sanic/__main__.py +++ b/sanic/__main__.py @@ -28,10 +28,13 @@ if __name__ == "__main__": raise ValueError("Module is not a Sanic app, it is a {}. " "Perhaps you meant {}.app?" .format(type(app).__name__, args.module)) + if args.cert is not None or args.key is not None: + ssl = {'cert': args.cert, 'key': args.key} + else: + ssl = None app.run(host=args.host, port=args.port, - workers=args.workers, debug=args.debug, - cert=args.cert, key=args.key) + workers=args.workers, debug=args.debug, ssl=ssl) except ImportError: log.error("No module named {} found.\n" " Example File: project/sanic_server.py -> app\n" diff --git a/sanic/app.py b/sanic/app.py index ef9ac57c..d2894ff2 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -7,7 +7,7 @@ from functools import partial from inspect import isawaitable, stack, getmodulename from traceback import format_exc from urllib.parse import urlencode, urlunparse -from ssl import create_default_context +from ssl import create_default_context, Purpose from sanic.config import Config from sanic.constants import HTTP_METHODS @@ -26,7 +26,7 @@ from sanic.websocket import WebSocketProtocol, ConnectionClosed class Sanic: def __init__(self, name=None, router=None, error_handler=None, - load_env=True): + load_env=True, request_class=None): # 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: @@ -44,6 +44,7 @@ class Sanic: self.name = name self.router = router or Router() + self.request_class = request_class self.error_handler = error_handler or ErrorHandler() self.config = Config(load_env=load_env) self.request_middleware = deque() @@ -444,17 +445,7 @@ class Sanic: # -------------------------------------------- # request.app = self - - response = False - # The if improves speed. I don't know why - if self.request_middleware: - for middleware in self.request_middleware: - response = middleware(request) - if isawaitable(response): - response = await response - if response: - break - + response = await self._run_request_middleware(request) # No middleware results if not response: # -------------------------------------------- # @@ -472,20 +463,6 @@ class Sanic: response = handler(request, *args, **kwargs) if isawaitable(response): response = await response - - # -------------------------------------------- # - # Response Middleware - # -------------------------------------------- # - - if self.response_middleware: - for middleware in self.response_middleware: - _response = middleware(request, response) - if isawaitable(_response): - _response = await _response - if _response: - response = _response - break - except Exception as e: # -------------------------------------------- # # Response Generation Failed @@ -503,6 +480,17 @@ class Sanic: else: response = HTTPResponse( "An error occurred while handling an error") + finally: + # -------------------------------------------- # + # Response Middleware + # -------------------------------------------- # + try: + response = await self._run_response_middleware(request, + response) + except: + log.exception( + 'Exception occured in one of response middleware handlers' + ) # pass the response to the correct callback if isinstance(response, StreamingHTTPResponse): @@ -615,6 +603,28 @@ class Sanic: return await serve(**server_settings) + async def _run_request_middleware(self, request): + # The if improves speed. I don't know why + if self.request_middleware: + for middleware in self.request_middleware: + response = middleware(request) + if isawaitable(response): + response = await response + if response: + return response + return None + + async def _run_response_middleware(self, request, response): + if self.response_middleware: + for middleware in self.response_middleware: + _response = middleware(request, response) + if isawaitable(_response): + _response = await _response + if _response: + response = _response + break + return response + def _helper(self, host="127.0.0.1", port=8000, debug=False, before_start=None, after_start=None, before_stop=None, after_stop=None, ssl=None, sock=None, workers=1, loop=None, @@ -626,9 +636,9 @@ class Sanic: # try common aliaseses cert = ssl.get('cert') or ssl.get('certificate') key = ssl.get('key') or ssl.get('keyfile') - if not cert and key: + if cert is None or key is None: raise ValueError("SSLContext or certificate and key required.") - context = create_default_context(purpose=ssl.Purpose.CLIENT_AUTH) + context = create_default_context(purpose=Purpose.CLIENT_AUTH) context.load_cert_chain(cert, keyfile=key) ssl = context if stop_event is not None: @@ -659,6 +669,7 @@ class Sanic: server_settings = { 'protocol': protocol, + 'request_class': self.request_class, 'host': host, 'port': port, 'sock': sock, diff --git a/sanic/response.py b/sanic/response.py index 4eecaf79..bebb8071 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -129,7 +129,7 @@ class StreamingHTTPResponse(BaseHTTPResponse): data = self._encode_body(data) self.transport.write( - b"%b\r\n%b\r\n" % (str(len(data)).encode(), data)) + b"%x\r\n%b\r\n" % (len(data), data)) async def stream( self, version="1.1", keep_alive=False, keep_alive_timeout=None): diff --git a/sanic/server.py b/sanic/server.py index 976503a6..7868836c 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -64,12 +64,13 @@ class HttpProtocol(asyncio.Protocol): 'parser', 'request', 'url', 'headers', # request config 'request_handler', 'request_timeout', 'request_max_size', + 'request_class', # connection management '_total_request_size', '_timeout_handler', '_last_communication_time') def __init__(self, *, loop, request_handler, error_handler, signal=Signal(), connections=set(), request_timeout=60, - request_max_size=None): + request_max_size=None, request_class=None): self.loop = loop self.transport = None self.request = None @@ -82,6 +83,7 @@ class HttpProtocol(asyncio.Protocol): self.error_handler = error_handler self.request_timeout = request_timeout self.request_max_size = request_max_size + self.request_class = request_class or Request self._total_request_size = 0 self._timeout_handler = None self._last_request_time = None @@ -151,7 +153,7 @@ class HttpProtocol(asyncio.Protocol): self.headers.append((name.decode().casefold(), value.decode())) def on_headers_complete(self): - self.request = Request( + self.request = self.request_class( url_bytes=self.url, headers=CIDict(self.headers), version=self.parser.get_http_version(), @@ -320,7 +322,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, request_timeout=60, ssl=None, sock=None, request_max_size=None, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, register_sys_signals=True, run_async=False, connections=None, - signal=Signal()): + signal=Signal(), request_class=None): """Start asynchronous HTTP Server on an individual process. :param host: Address to host on @@ -345,6 +347,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, :param reuse_port: `True` for multiple workers :param loop: asyncio compatible event loop :param protocol: subclass of asyncio protocol class + :param request_class: Request class to use :return: Nothing """ if not run_async: @@ -366,6 +369,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, error_handler=error_handler, request_timeout=request_timeout, request_max_size=request_max_size, + request_class=request_class, ) server_coroutine = loop.create_server( diff --git a/sanic/testing.py b/sanic/testing.py index 1cad6a7b..787106a6 100644 --- a/sanic/testing.py +++ b/sanic/testing.py @@ -1,3 +1,5 @@ +import traceback + from sanic.log import log HOST = '127.0.0.1' @@ -50,6 +52,8 @@ class SanicTestClient: **request_kwargs) results[-1] = response except Exception as e: + log.error( + 'Exception:\n{}'.format(traceback.format_exc())) exceptions.append(e) self.app.stop() diff --git a/tests/test_middleware.py b/tests/test_middleware.py index f84d7151..bc1a7eb8 100644 --- a/tests/test_middleware.py +++ b/tests/test_middleware.py @@ -2,6 +2,7 @@ from json import loads as json_loads, dumps as json_dumps from sanic import Sanic from sanic.request import Request from sanic.response import json, text, HTTPResponse +from sanic.exceptions import NotFound # ------------------------------------------------------------ # @@ -53,6 +54,27 @@ def test_middleware_response(): assert isinstance(results[2], HTTPResponse) +def test_middleware_response_exception(): + app = Sanic('test_middleware_response_exception') + result = {'status_code': None} + + @app.middleware('response') + async def process_response(reqest, response): + result['status_code'] = response.status + return response + + @app.exception(NotFound) + async def error_handler(request, exception): + return text('OK', exception.status_code) + + @app.route('/') + async def handler(request): + return text('FAIL') + + request, response = app.test_client.get('/page_not_found') + assert response.text == 'OK' + assert result['status_code'] == 404 + def test_middleware_override_request(): app = Sanic('test_middleware_override_request')