Added new tests, new request logic, and handler file

Added new tests for alternate uses for alternate range request types.

Changed error handlnig for new request logic that simplifies the integration logic

Moved the error handler and the content range handler to their own handler file to prevent circular imports.
This commit is contained in:
Kyle Blöm 2017-01-30 09:13:43 -08:00
parent 60f36135b3
commit bcd4546d58
6 changed files with 188 additions and 124 deletions

View File

@ -1,8 +1,3 @@
from .response import text, html
from .log import log
from traceback import format_exc, extract_tb
import sys
TRACEBACK_STYLE = ''' TRACEBACK_STYLE = '''
<style> <style>
body { body {
@ -138,6 +133,10 @@ class PayloadTooLarge(SanicException):
status_code = 413 status_code = 413
class HeaderNotFound(SanicException):
status_code = 400
class ContentRangeError(SanicException): class ContentRangeError(SanicException):
status_code = 416 status_code = 416
@ -149,70 +148,5 @@ class ContentRangeError(SanicException):
} }
class Handler: class InvalidRangeType(ContentRangeError):
handlers = None pass
def __init__(self):
self.handlers = {}
self.debug = False
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)
try:
response = handler(request=request, exception=exception)
except:
log.error(format_exc())
if self.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 isinstance(exception, SanicException):
return text(
'Error: {}'.format(exception),
status=getattr(exception, 'status_code', 500),
headers=getattr(exception, 'headers', dict())
)
elif self.debug:
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 html(INTERNAL_SERVER_ERROR_HTML, status=500)

127
sanic/handlers.py Normal file
View File

@ -0,0 +1,127 @@
from .response import text, html
from .log import log
from traceback import format_exc, extract_tb
import sys
from .exceptions import SanicException, HeaderNotFound, InvalidRangeType
from .exceptions import INTERNAL_SERVER_ERROR_HTML, TRACEBACK_LINE_HTML
from .exceptions import TRACEBACK_STYLE, TRACEBACK_WRAPPER_HTML
from .exceptions import ContentRangeError
class ErrorHandler:
handlers = None
def __init__(self):
self.handlers = {}
self.debug = False
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)
try:
response = handler(request=request, exception=exception)
except Exception:
log.error(format_exc())
if self.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),
status=getattr(exception, 'status_code', 500),
headers=getattr(exception, 'headers', dict())
)
elif self.debug:
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 html(INTERNAL_SERVER_ERROR_HTML, status=500)
class ContentRangeHandler:
"""
This class is for parsing the request header
"""
__slots__ = ('start', 'end', 'size', 'total', 'headers')
def __init__(self, request, stats):
self.total = stats.st_size
_range = request.headers.get('Range')
if _range is None:
raise HeaderNotFound('Range Header Not Found')
unit, _, value = tuple(map(str.strip, _range.partition('=')))
if unit != 'bytes':
raise InvalidRangeType(
'%s is not a valid Range Type' % (unit,), self)
start_b, _, end_b = tuple(map(str.strip, value.partition('-')))
try:
self.start = int(start_b) if start_b else None
except ValueError:
raise ContentRangeError(
'\'%s\' is invalid for Content Range' % (start_b,), self)
try:
self.end = int(end_b) if end_b else None
except ValueError:
raise ContentRangeError(
'\'%s\' is invalid for Content Range' % (end_b,), self)
if self.end is None:
if self.start is None:
raise ContentRangeError(
'Invalid for Content Range parameters', self)
else:
# this case represents `Content-Range: bytes 5-`
self.end = self.total
else:
if self.start is None:
# this case represents `Content-Range: bytes -5`
self.start = self.total - self.end
self.end = self.total
if self.start >= self.end:
raise ContentRangeError(
'Invalid for Content Range parameters', self)
self.size = self.end - self.start
self.headers = {
'Content-Range': "bytes %s-%s/%s" % (
self.start, self.end, self.total)}
def __bool__(self):
return self.size > 0

View File

@ -140,38 +140,6 @@ class HTTPResponse:
return self._cookies return self._cookies
class ContentRangeHandler:
"""
This class is for parsing the request header
"""
__slots__ = ('start', 'end', 'size', 'total', 'headers')
def __init__(self, request, stats):
self.size = self.start = 0
self.end = None
self.headers = dict()
self.total = stats.st_size
_range = request.headers.get('Range')
if _range is None:
return
unit, _, value = tuple(map(str.strip, _range.partition('=')))
if unit != 'bytes':
return
start_b, _, end_b = tuple(map(str.strip, value.partition('-')))
try:
self.start = int(start_b) if start_b else 0
self.end = int(end_b) if end_b else 0
except ValueError:
self.start = self.end = 0
return
self.size = self.end - self.start
self.headers['Content-Range'] = "bytes %s-%s/%s" % (
self.start, self.end, self.total)
def __bool__(self):
return self.size != 0
def json(body, status=200, headers=None, **kwargs): def json(body, status=200, headers=None, **kwargs):
""" """
Returns response object with body in json format. Returns response object with body in json format.

View File

@ -8,8 +8,8 @@ import warnings
from .config import Config from .config import Config
from .constants import HTTP_METHODS from .constants import HTTP_METHODS
from .exceptions import Handler
from .exceptions import ServerError from .exceptions import ServerError
from .handlers import ErrorHandler
from .log import log from .log import log
from .response import HTTPResponse from .response import HTTPResponse
from .router import Router from .router import Router
@ -34,7 +34,7 @@ class Sanic:
name = getmodulename(frame_records[1]) name = getmodulename(frame_records[1])
self.name = name self.name = name
self.router = router or Router() self.router = router or Router()
self.error_handler = error_handler or Handler() self.error_handler = error_handler or ErrorHandler()
self.config = Config() self.config = Config()
self.request_middleware = deque() self.request_middleware = deque()
self.response_middleware = deque() self.response_middleware = deque()

View File

@ -1,12 +1,15 @@
from aiofiles.os import stat from mimetypes import guess_type
from os import path from os import path
from re import sub from re import sub
from time import strftime, gmtime from time import strftime, gmtime
from mimetypes import guess_type
from urllib.parse import unquote from urllib.parse import unquote
from aiofiles.os import stat
from .exceptions import FileNotFound, InvalidUsage, ContentRangeError from .exceptions import FileNotFound, InvalidUsage, ContentRangeError
from .response import file, HTTPResponse, ContentRangeHandler from .exceptions import HeaderNotFound
from .response import file, HTTPResponse
from .handlers import ContentRangeHandler
def register(app, uri, file_or_directory, pattern, def register(app, uri, file_or_directory, pattern,
@ -56,30 +59,27 @@ def register(app, uri, file_or_directory, pattern,
stats = None stats = None
if use_modified_since: if use_modified_since:
stats = await stat(file_path) stats = await stat(file_path)
modified_since = strftime('%a, %d %b %Y %H:%M:%S GMT', modified_since = strftime(
gmtime(stats.st_mtime)) '%a, %d %b %Y %H:%M:%S GMT', gmtime(stats.st_mtime))
if request.headers.get('If-Modified-Since') == modified_since: if request.headers.get('If-Modified-Since') == modified_since:
return HTTPResponse(status=304) return HTTPResponse(status=304)
headers['Last-Modified'] = modified_since headers['Last-Modified'] = modified_since
_range = None _range = None
if use_content_range: if use_content_range:
_range = None
if not stats: if not stats:
stats = await stat(file_path) stats = await stat(file_path)
headers['Accept-Ranges'] = 'bytes' headers['Accept-Ranges'] = 'bytes'
headers['Content-Length'] = str(stats.st_size) headers['Content-Length'] = str(stats.st_size)
if request.method != 'HEAD': if request.method != 'HEAD':
try:
_range = ContentRangeHandler(request, stats) _range = ContentRangeHandler(request, stats)
# If the start byte is greater than the size except HeaderNotFound:
# of the entire file or if the end is pass
if _range.start >= _range.total or _range.end == 0:
raise ContentRangeError('Content-Range malformed',
_range)
if _range.start == 0 and _range.size == 0:
_range = None
else: else:
headers['Content-Length'] = str(_range.size) del headers['Content-Length']
for k, v in _range.headers.items(): for key, value in _range.headers.items():
headers[k] = v headers[key] = value
if request.method == 'HEAD': if request.method == 'HEAD':
return HTTPResponse( return HTTPResponse(
headers=headers, headers=headers,

View File

@ -86,8 +86,43 @@ def test_static_content_range_correct(static_file_path, static_file_content):
assert response.status == 200 assert response.status == 200
assert 'Content-Length' in response.headers assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers assert 'Content-Range' in response.headers
assert int(response.headers['Content-Length']) == 19-12 static_content = bytes(static_file_content)[12:19]
assert response.body == bytes(static_file_content)[12:19] assert int(response.headers['Content-Length']) == len(static_content)
assert response.body == static_content
def test_static_content_range_front(static_file_path, static_file_content):
app = Sanic('test_static')
app.static('/testing.file', static_file_path, use_content_range=True)
headers = {
'Range': 'bytes=12-'
}
request, response = sanic_endpoint_test(
app, uri='/testing.file', headers=headers)
assert response.status == 200
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(static_file_content)[12:]
assert int(response.headers['Content-Length']) == len(static_content)
assert response.body == static_content
def test_static_content_range_back(static_file_path, static_file_content):
app = Sanic('test_static')
app.static('/testing.file', static_file_path, use_content_range=True)
headers = {
'Range': 'bytes=-12'
}
request, response = sanic_endpoint_test(
app, uri='/testing.file', headers=headers)
assert response.status == 200
assert 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
static_content = bytes(static_file_content)[-12:]
assert int(response.headers['Content-Length']) == len(static_content)
assert response.body == static_content
def test_static_content_range_empty(static_file_path, static_file_content): def test_static_content_range_empty(static_file_path, static_file_content):