Much cleanup, 12 failing...

This commit is contained in:
L. Kärkkäinen 2020-02-29 16:50:59 +02:00
parent 57202bfa89
commit a553e64bbd
4 changed files with 146 additions and 174 deletions

View File

@ -930,6 +930,26 @@ class Sanic:
""" """
pass pass
async def handle_exception(self, request, exception):
try:
response = self.error_handler.response(request, exception)
if isawaitable(response):
response = await response
except Exception as e:
if isinstance(e, SanicException):
response = self.error_handler.default(request, e)
elif self.debug:
response = HTTPResponse(
f"Error while handling error: {e}\nStack: {format_exc()}",
status=500,
)
else:
response = HTTPResponse(
"An error occurred while handling an error", status=500
)
return response
async def handle_request(self, request): async def handle_request(self, request):
"""Take a request from the HTTP Server and return a response object """Take a request from the HTTP Server and return a response object
to be sent back The HTTP Server only expects a response object, so to be sent back The HTTP Server only expects a response object, so
@ -1003,27 +1023,7 @@ class Sanic:
# -------------------------------------------- # # -------------------------------------------- #
# Response Generation Failed # Response Generation Failed
# -------------------------------------------- # # -------------------------------------------- #
response = await self.handle_exception(request, e)
try:
response = self.error_handler.response(request, e)
if isawaitable(response):
response = await response
except Exception as e:
if isinstance(e, SanicException):
response = self.error_handler.default(
request=request, exception=e
)
elif self.debug:
response = HTTPResponse(
"Error while handling error: {}\nStack: {}".format(
e, format_exc()
),
status=500,
)
else:
response = HTTPResponse(
"An error occurred while handling an error", status=500
)
finally: finally:
# -------------------------------------------- # # -------------------------------------------- #
# Response Middleware # Response Middleware
@ -1048,39 +1048,14 @@ class Sanic:
try: try:
# pass the response to the correct callback # pass the response to the correct callback
if response is None: if isinstance(response, StreamingHTTPResponse):
pass
elif isinstance(response, StreamingHTTPResponse):
await response.stream(request) await response.stream(request)
elif isinstance(response, HTTPResponse): elif isinstance(response, HTTPResponse):
await request.respond(response).send(end_stream=True) await request.respond(response).send(end_stream=True)
else: else:
raise ServerError(f"Invalid response type {response} (need HTTPResponse)") raise ServerError(f"Invalid response type {response} (need HTTPResponse)")
except Exception as e: except Exception as e:
# -------------------------------------------- # response = await self.handle_exception(request, e)
# Response Generation Failed
# -------------------------------------------- #
try:
response = self.error_handler.response(request, e)
if isawaitable(response):
response = await response
except Exception as e:
if isinstance(e, SanicException):
response = self.error_handler.default(
request=request, exception=e
)
elif self.debug:
response = HTTPResponse(
"Error while handling error: {}\nStack: {}".format(
e, format_exc()
),
status=500,
)
else:
response = HTTPResponse(
"An error occurred while handling an error", status=500
)
# pass the response to the correct callback # pass the response to the correct callback
if response is None: if response is None:

View File

@ -71,10 +71,14 @@ def _render_traceback_html(request, exception):
exc_value = exc_value.__cause__ exc_value = exc_value.__cause__
traceback_html = TRACEBACK_BORDER.join(reversed(exceptions)) traceback_html = TRACEBACK_BORDER.join(reversed(exceptions))
if request is not None:
appname = escape(request.app.name) appname = escape(request.app.name)
path = escape(request.path)
else:
appname = "<no request received>"
path = "unknown"
name = escape(exception.__class__.__name__) name = escape(exception.__class__.__name__)
value = escape(exception) value = escape(exception)
path = escape(request.path)
return ( return (
f"<h2>Traceback of {appname} (most recent call last):</h2>" f"<h2>Traceback of {appname} (most recent call last):</h2>"
f"{traceback_html}" f"{traceback_html}"

View File

@ -1,5 +1,7 @@
from asyncio import CancelledError
from enum import Enum from enum import Enum
from sanic.compat import Header
from sanic.exceptions import ( from sanic.exceptions import (
HeaderExpectationFailed, HeaderExpectationFailed,
InvalidUsage, InvalidUsage,
@ -14,7 +16,6 @@ from sanic.helpers import has_message_body, remove_entity_headers
from sanic.log import access_logger, logger from sanic.log import access_logger, logger
from sanic.request import Request from sanic.request import Request
from sanic.response import HTTPResponse from sanic.response import HTTPResponse
from sanic.compat import Header
class Stage(Enum): class Stage(Enum):
@ -34,18 +35,14 @@ class Http:
self.recv_buffer = protocol.recv_buffer self.recv_buffer = protocol.recv_buffer
self.protocol = protocol self.protocol = protocol
self.expecting_continue = False self.expecting_continue = False
# Note: connections are initially in request mode and do not obey self.stage = Stage.IDLE
# keep-alive timeout like with some other servers.
self.stage = Stage.REQUEST
self.keep_alive = True self.keep_alive = True
self.head_only = None self.head_only = None
self.request = None
self.exception = None
async def http1(self): async def http1_receive_request(self):
"""HTTP 1.1 connection handler"""
buf = self.recv_buffer buf = self.recv_buffer
self.url = None
while self.keep_alive:
# Read request header
pos = 0 pos = 0
while len(buf) < self.protocol.request_max_size: while len(buf) < self.protocol.request_max_size:
if buf: if buf:
@ -57,7 +54,6 @@ class Http:
if self.stage is Stage.IDLE: if self.stage is Stage.IDLE:
self.stage = Stage.REQUEST self.stage = Stage.REQUEST
else: else:
self.stage = Stage.HANDLER
raise PayloadTooLarge("Payload Too Large") raise PayloadTooLarge("Payload Too Large")
self.protocol._total_request_size = pos + 4 self.protocol._total_request_size = pos + 4
@ -73,10 +69,7 @@ class Http:
for name, value in (h.split(":", 1) for h in headers) for name, value in (h.split(":", 1) for h in headers)
) )
except: except:
self.stage = Stage.HANDLER
raise InvalidUsage("Bad Request") raise InvalidUsage("Bad Request")
# Prepare a request object from the header received
request = self.protocol.request_class( request = self.protocol.request_class(
url_bytes=self.url.encode(), url_bytes=self.url.encode(),
headers=headers, headers=headers,
@ -85,6 +78,8 @@ class Http:
transport=self.protocol.transport, transport=self.protocol.transport,
app=self.protocol.app, app=self.protocol.app,
) )
# Prepare a request object from the header received
request.stream = self request.stream = self
self.protocol.state["requests_count"] += 1 self.protocol.state["requests_count"] += 1
self.keep_alive = ( self.keep_alive = (
@ -97,14 +92,14 @@ class Http:
) )
self.request_chunked = False self.request_chunked = False
self.request_bytes_left = 0 self.request_bytes_left = 0
self.stage = Stage.HANDLER
if body: if body:
expect = headers.get("expect") expect = headers.get("expect")
if expect: if expect:
if expect.lower() == "100-continue": if expect.lower() == "100-continue":
self.expecting_continue = True self.expecting_continue = True
else: else:
raise HeaderExpectationFailed(f"Unknown Expect: {expect}") raise HeaderExpectationFailed(
f"Unknown Expect: {expect}")
request.stream = self request.stream = self
if body is True: if body is True:
self.request_chunked = True self.request_chunked = True
@ -113,40 +108,48 @@ class Http:
self.request_bytes_left = body self.request_bytes_left = body
# Remove header and its trailing CRLF # Remove header and its trailing CRLF
del buf[: pos + 4] del buf[: pos + 4]
self.stage = Stage.HANDLER
self.request = request
# Run handler async def http1(self):
"""HTTP 1.1 connection handler"""
while self.stage is Stage.IDLE and self.keep_alive:
try: try:
await self.protocol.request_handler(request) # Receive request header and call handler
except Exception: await self.http1_receive_request()
logger.exception("Uncaught from app/handler") await self.protocol.request_handler(self.request)
await self.write_error(ServerError("Internal Server Error"))
if self.stage is Stage.IDLE:
continue
if self.stage is Stage.HANDLER: if self.stage is Stage.HANDLER:
await self.respond(HTTPResponse(status=204)).send(end_stream=True) raise ServerError("Handler produced no response")
# Finish sending a response (if no error) # Finish sending a response (if no error)
elif self.stage is Stage.RESPONSE: if self.stage is Stage.RESPONSE:
await self.send(end_stream=True) await self.send(end_stream=True)
# Consume any remaining request body # Consume any remaining request body
if self.request_bytes_left or self.request_chunked: if self.request_bytes_left or self.request_chunked:
logger.error( logger.error(f"{self.request} body not consumed.")
f"Handler of {method} {url} did not consume request body." async for _ in self:
)
while await self.read():
pass pass
except Exception as e:
self.stage = Stage.IDLE self.exception = e
except BaseException:
async def write_error(self, e): # Exit after trying to finish a response
self.keep_alive = False
if self.exception is None:
self.exception = ServiceUnavailable(f"Cancelled")
if self.exception:
e, self.exception = self.exception, None
# Exception while idle? Probably best to close connection
if self.stage is Stage.IDLE:
return
# Request failure? Try to respond but then disconnect
if self.stage is Stage.REQUEST:
self.keep_alive = False
self.stage = Stage.HANDLER
# Return an error page if possible
if self.stage is Stage.HANDLER: if self.stage is Stage.HANDLER:
try: app = self.protocol.app
response = HTTPResponse(f"{e}", e.status_code, content_type="text/plain") response = await app.handle_exception(self.request, e)
await self.respond(response).send(end_stream=True) await self.respond(response).send(end_stream=True)
except:
logger.exception("Error sending error")
# Request methods # Request methods

View File

@ -4,6 +4,7 @@ import os
import sys import sys
import traceback import traceback
from asyncio import CancelledError
from collections import deque from collections import deque
from functools import partial from functools import partial
from inspect import isawaitable from inspect import isawaitable
@ -11,7 +12,8 @@ from multiprocessing import Process
from signal import SIG_IGN, SIGINT, SIGTERM, Signals from signal import SIG_IGN, SIGINT, SIGTERM, Signals
from signal import signal as signal_func from signal import signal as signal_func
from socket import SO_REUSEADDR, SOL_SOCKET, socket from socket import SO_REUSEADDR, SOL_SOCKET, socket
from time import time, monotonic as current_time from time import monotonic as current_time
from time import time
from sanic.compat import Header from sanic.compat import Header
from sanic.exceptions import ( from sanic.exceptions import (
@ -145,20 +147,8 @@ class HttpProtocol(asyncio.Protocol):
self._http = Http(self) self._http = Http(self)
self._time = current_time() self._time = current_time()
self.check_timeouts() self.check_timeouts()
try:
await self._http.http1() await self._http.http1()
except asyncio.CancelledError: except CancelledError:
await self._http.write_error(
self._exception
or ServiceUnavailable("Request handler cancelled")
)
except SanicException as e:
await self._http.write_error(e)
except BaseException as e:
logger.exception(
f"Uncaught exception while handling URL {self._http.url}"
)
except asyncio.CancelledError:
pass pass
except: except:
logger.exception("protocol.connection_task uncaught") logger.exception("protocol.connection_task uncaught")
@ -185,12 +175,12 @@ class HttpProtocol(asyncio.Protocol):
if stage is Stage.IDLE and duration > self.keep_alive_timeout: if stage is Stage.IDLE and duration > self.keep_alive_timeout:
logger.debug("KeepAlive Timeout. Closing connection.") logger.debug("KeepAlive Timeout. Closing connection.")
elif stage is Stage.REQUEST and duration > self.request_timeout: elif stage is Stage.REQUEST and duration > self.request_timeout:
self._exception = RequestTimeout("Request Timeout") self._http.exception = RequestTimeout("Request Timeout")
elif ( elif (
stage in (Stage.REQUEST, Stage.FAILED) stage in (Stage.REQUEST, Stage.FAILED)
and duration > self.response_timeout and duration > self.response_timeout
): ):
self._exception = ServiceUnavailable("Response Timeout") self._http.exception = ServiceUnavailable("Response Timeout")
else: else:
self.loop.call_later(1.0, self.check_timeouts) self.loop.call_later(1.0, self.check_timeouts)
return return