{exc_value}
Traceback (most recent call last):
+ {frame_html} +
+ {exc_name}: {exc_value}
+ while handling uri {uri}
+
+ File {0.filename}, line {0.lineno},
+ in {0.name}
+
{0.line}
+ The server encountered an internal error and cannot complete + your request. +
+''' class SanicException(Exception): @@ -45,29 +144,61 @@ class Handler: self.handlers = {} self.sanic = sanic + def _render_traceback_html(self, exception, request): + exc_type, exc_value, tb = sys.exc_info() + frames = extract_tb(tb) + + frame_html = [] + for frame in frames: + frame_html.append(TRACEBACK_LINE_HTML.format(frame)) + + return TRACEBACK_WRAPPER_HTML.format( + style=TRACEBACK_STYLE, + exc_name=exc_type.__name__, + exc_value=exc_value, + frame_html=''.join(frame_html), + uri=request.url) + def add(self, exception, handler): self.handlers[exception] = handler def response(self, request, exception): """ Fetches and executes an exception handler and returns a response object + :param request: Request :param exception: Exception to handle :return: Response object """ handler = self.handlers.get(type(exception), self.default) - response = handler(request=request, exception=exception) + try: + response = handler(request=request, exception=exception) + except: + log.error(format_exc()) + if self.sanic.debug: + response_message = ( + 'Exception raised in exception handler "{}" ' + 'for uri: "{}"\n{}').format( + handler.__name__, request.url, format_exc()) + log.error(response_message) + return text(response_message, 500) + else: + return text('An error occurred while handling an error', 500) return response def default(self, request, exception): + log.error(format_exc()) if issubclass(type(exception), SanicException): return text( - "Error: {}".format(exception), + 'Error: {}'.format(exception), status=getattr(exception, 'status_code', 500)) elif self.sanic.debug: - return text( - "Error: {}\nException: {}".format( - exception, format_exc()), status=500) + html_output = self._render_traceback_html(exception, request) + + response_message = ( + 'Exception occurred while handling uri: "{}"\n{}'.format( + request.url, format_exc())) + log.error(response_message) + return html(html_output, status=500) else: - return text( - "An error occurred while generating the request", status=500) + return html(INTERNAL_SERVER_ERROR_HTML, status=500) diff --git a/sanic/log.py b/sanic/log.py index 3988bf12..1b4d7334 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -1,3 +1,3 @@ import logging -log = logging.getLogger(__name__) +log = logging.getLogger('sanic') diff --git a/sanic/request.py b/sanic/request.py index 62d89781..6f02d09b 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -21,16 +21,13 @@ class RequestParameters(dict): value of the list and getlist returns the whole shebang """ - def __init__(self, *args, **kwargs): - self.super = super() - self.super.__init__(*args, **kwargs) - def get(self, name, default=None): - values = self.super.get(name) - return values[0] if values else default + """Return the first value, either the default or actual""" + return super().get(name, [default])[0] def getlist(self, name, default=None): - return self.super.get(name, default) + """Return the entire list""" + return super().get(name, default) class Request(dict): @@ -38,18 +35,20 @@ class Request(dict): Properties of an HTTP request such as URL, headers, etc. """ __slots__ = ( - 'url', 'headers', 'version', 'method', '_cookies', + 'url', 'headers', 'version', 'method', '_cookies', 'transport', 'query_string', 'body', 'parsed_json', 'parsed_args', 'parsed_form', 'parsed_files', + '_ip', ) - def __init__(self, url_bytes, headers, version, method): + def __init__(self, url_bytes, headers, version, method, transport): # TODO: Content-Encoding detection url_parsed = parse_url(url_bytes) self.url = url_parsed.path.decode('utf-8') self.headers = headers self.version = version self.method = method + self.transport = transport self.query_string = None if url_parsed.query: self.query_string = url_parsed.query.decode('utf-8') @@ -64,7 +63,7 @@ class Request(dict): @property def json(self): - if not self.parsed_json: + if self.parsed_json is None: try: self.parsed_json = json_loads(self.body) except Exception: @@ -72,6 +71,17 @@ class Request(dict): return self.parsed_json + @property + def token(self): + """ + Attempts to return the auth header token. + :return: token related to request + """ + auth_header = self.headers.get('Authorization') + if auth_header is not None: + return auth_header.split()[1] + return auth_header + @property def form(self): if self.parsed_form is None: @@ -125,6 +135,12 @@ class Request(dict): self._cookies = {} return self._cookies + @property + def ip(self): + if not hasattr(self, '_ip'): + self._ip = self.transport.get_extra_info('peername') + return self._ip + File = namedtuple('File', ['type', 'body', 'name']) @@ -132,6 +148,7 @@ File = namedtuple('File', ['type', 'body', 'name']) def parse_multipart_form(body, boundary): """ Parses a request body and returns fields and files + :param body: Bytes request body :param boundary: Bytes multipart boundary :return: fields (RequestParameters), files (RequestParameters) diff --git a/sanic/response.py b/sanic/response.py index 2c4c7f27..244bc1b3 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -83,10 +83,10 @@ class HTTPResponse: if body is not None: try: # Try to encode it regularly - self.body = body.encode('utf-8') + self.body = body.encode() except AttributeError: # Convert it to a str if you can't - self.body = str(body).encode('utf-8') + self.body = str(body).encode() else: self.body = body_bytes @@ -103,10 +103,14 @@ class HTTPResponse: headers = b'' if self.headers: - headers = b''.join( - b'%b: %b\r\n' % (name.encode(), value.encode('utf-8')) - for name, value in self.headers.items() - ) + for name, value in self.headers.items(): + try: + headers += ( + b'%b: %b\r\n' % (name.encode(), value.encode('utf-8'))) + except AttributeError: + headers += ( + b'%b: %b\r\n' % ( + str(name).encode(), str(value).encode('utf-8'))) # Try to pull from the common codes first # Speeds up response rate 6% over pulling from all @@ -139,21 +143,45 @@ class HTTPResponse: def json(body, status=200, headers=None): + """ + Returns response object with body in json format. + :param body: Response data to be serialized. + :param status: Response code. + :param headers: Custom Headers. + """ return HTTPResponse(json_dumps(body), headers=headers, status=status, content_type="application/json") def text(body, status=200, headers=None): + """ + Returns response object with body in text format. + :param body: Response data to be encoded. + :param status: Response code. + :param headers: Custom Headers. + """ return HTTPResponse(body, status=status, headers=headers, content_type="text/plain; charset=utf-8") def html(body, status=200, headers=None): + """ + Returns response object with body in html format. + :param body: Response data to be encoded. + :param status: Response code. + :param headers: Custom Headers. + """ return HTTPResponse(body, status=status, headers=headers, content_type="text/html; charset=utf-8") async def file(location, mime_type=None, headers=None): + """ + Returns response object with file data. + :param location: Location of file on system. + :param mime_type: Specific mime_type. + :param headers: Custom Headers. + """ filename = path.split(location)[-1] async with open_async(location, mode='rb') as _file: @@ -165,3 +193,26 @@ async def file(location, mime_type=None, headers=None): headers=headers, content_type=mime_type, body_bytes=out_stream) + + +def redirect(to, headers=None, status=302, + content_type="text/html; charset=utf-8"): + """ + Aborts execution and causes a 302 redirect (by default). + + :param to: path or fully qualified URL to redirect to + :param headers: optional dict of headers to include in the new request + :param status: status code (int) of the new request, defaults to 302 + :param content_type: + the content type (string) of the response + :returns: the redirecting Response + """ + headers = headers or {} + + # According to RFC 7231, a relative URI is now permitted. + headers['Location'] = to + + return HTTPResponse( + status=status, + headers=headers, + content_type=content_type) diff --git a/sanic/router.py b/sanic/router.py index 4cc1f073..ec67f690 100644 --- a/sanic/router.py +++ b/sanic/router.py @@ -3,6 +3,7 @@ from collections import defaultdict, namedtuple from functools import lru_cache from .config import Config from .exceptions import NotFound, InvalidUsage +from .views import CompositionView Route = namedtuple('Route', ['handler', 'methods', 'pattern', 'parameters']) Parameter = namedtuple('Parameter', ['name', 'cast']) @@ -23,16 +24,28 @@ class RouteExists(Exception): pass +class RouteDoesNotExist(Exception): + pass + + class Router: """ Router supports basic routing with parameters and method checks + Usage: - @sanic.route('/my/url/