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:
parent
afe12f2d9e
commit
293954a5d8
|
@ -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
127
sanic/handlers.py
Normal 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
|
|
@ -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.
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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':
|
||||||
_range = ContentRangeHandler(request, stats)
|
try:
|
||||||
# If the start byte is greater than the size
|
_range = ContentRangeHandler(request, stats)
|
||||||
# of the entire file or if the end is
|
except HeaderNotFound:
|
||||||
if _range.start >= _range.total or _range.end == 0:
|
pass
|
||||||
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,
|
||||||
|
|
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue
Block a user