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 ca02ed3b9f
commit bb05413a92
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 = '''
<style>
body {
@ -138,6 +133,10 @@ class PayloadTooLarge(SanicException):
status_code = 413
class HeaderNotFound(SanicException):
status_code = 400
class ContentRangeError(SanicException):
status_code = 416
@ -149,70 +148,5 @@ class ContentRangeError(SanicException):
}
class Handler:
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:
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)
class InvalidRangeType(ContentRangeError):
pass

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
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):
"""
Returns response object with body in json format.

View File

@ -7,8 +7,8 @@ from traceback import format_exc
import warnings
from .config import Config
from .handlers import ErrorHandler
from .constants import HTTP_METHODS
from .exceptions import Handler
from .exceptions import ServerError
from .log import log
from .response import HTTPResponse
@ -34,7 +34,7 @@ class Sanic:
name = getmodulename(frame_records[1])
self.name = name
self.router = router or Router()
self.error_handler = error_handler or Handler()
self.error_handler = error_handler or ErrorHandler()
self.config = Config()
self.request_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 re import sub
from time import strftime, gmtime
from mimetypes import guess_type
from urllib.parse import unquote
from aiofiles.os import stat
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,
@ -56,30 +59,27 @@ def register(app, uri, file_or_directory, pattern,
stats = None
if use_modified_since:
stats = await stat(file_path)
modified_since = strftime('%a, %d %b %Y %H:%M:%S GMT',
gmtime(stats.st_mtime))
modified_since = strftime(
'%a, %d %b %Y %H:%M:%S GMT', gmtime(stats.st_mtime))
if request.headers.get('If-Modified-Since') == modified_since:
return HTTPResponse(status=304)
headers['Last-Modified'] = modified_since
_range = None
if use_content_range:
_range = None
if not stats:
stats = await stat(file_path)
headers['Accept-Ranges'] = 'bytes'
headers['Content-Length'] = str(stats.st_size)
if request.method != 'HEAD':
_range = ContentRangeHandler(request, stats)
# If the start byte is greater than the size
# of the entire file or if the end is
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
try:
_range = ContentRangeHandler(request, stats)
except HeaderNotFound:
pass
else:
headers['Content-Length'] = str(_range.size)
for k, v in _range.headers.items():
headers[k] = v
del headers['Content-Length']
for key, value in _range.headers.items():
headers[key] = value
if request.method == 'HEAD':
return HTTPResponse(
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 'Content-Length' in response.headers
assert 'Content-Range' in response.headers
assert int(response.headers['Content-Length']) == 19-12
assert response.body == bytes(static_file_content)[12:19]
static_content = 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):