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
ca02ed3b9f
commit
bb05413a92
|
@ -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
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
|
||||
|
||||
|
||||
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.
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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):
|
||||
|
|
Loading…
Reference in New Issue
Block a user