Much cleanup, 12 failing...
This commit is contained in:
parent
57202bfa89
commit
a553e64bbd
71
sanic/app.py
71
sanic/app.py
|
@ -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:
|
||||||
|
|
|
@ -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))
|
||||||
appname = escape(request.app.name)
|
if request is not None:
|
||||||
|
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}"
|
||||||
|
|
217
sanic/http.py
217
sanic/http.py
|
@ -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,119 +35,121 @@ 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_receive_request(self):
|
||||||
|
buf = self.recv_buffer
|
||||||
|
pos = 0
|
||||||
|
while len(buf) < self.protocol.request_max_size:
|
||||||
|
if buf:
|
||||||
|
pos = buf.find(b"\r\n\r\n", pos)
|
||||||
|
if pos >= 0:
|
||||||
|
break
|
||||||
|
pos = max(0, len(buf) - 3)
|
||||||
|
await self._receive_more()
|
||||||
|
if self.stage is Stage.IDLE:
|
||||||
|
self.stage = Stage.REQUEST
|
||||||
|
else:
|
||||||
|
raise PayloadTooLarge("Payload Too Large")
|
||||||
|
|
||||||
|
self.protocol._total_request_size = pos + 4
|
||||||
|
|
||||||
|
try:
|
||||||
|
reqline, *headers = buf[:pos].decode().split("\r\n")
|
||||||
|
method, self.url, protocol = reqline.split(" ")
|
||||||
|
if protocol not in ("HTTP/1.0", "HTTP/1.1"):
|
||||||
|
raise Exception
|
||||||
|
self.head_only = method.upper() == "HEAD"
|
||||||
|
headers = Header(
|
||||||
|
(name.lower(), value.lstrip())
|
||||||
|
for name, value in (h.split(":", 1) for h in headers)
|
||||||
|
)
|
||||||
|
except:
|
||||||
|
raise InvalidUsage("Bad Request")
|
||||||
|
request = self.protocol.request_class(
|
||||||
|
url_bytes=self.url.encode(),
|
||||||
|
headers=headers,
|
||||||
|
version=protocol[-3:],
|
||||||
|
method=method,
|
||||||
|
transport=self.protocol.transport,
|
||||||
|
app=self.protocol.app,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Prepare a request object from the header received
|
||||||
|
request.stream = self
|
||||||
|
self.protocol.state["requests_count"] += 1
|
||||||
|
self.keep_alive = (
|
||||||
|
protocol == "HTTP/1.1"
|
||||||
|
or headers.get("connection", "").lower() == "keep-alive"
|
||||||
|
)
|
||||||
|
# Prepare for request body
|
||||||
|
body = headers.get("transfer-encoding") == "chunked" or int(
|
||||||
|
headers.get("content-length", 0)
|
||||||
|
)
|
||||||
|
self.request_chunked = False
|
||||||
|
self.request_bytes_left = 0
|
||||||
|
if body:
|
||||||
|
expect = headers.get("expect")
|
||||||
|
if expect:
|
||||||
|
if expect.lower() == "100-continue":
|
||||||
|
self.expecting_continue = True
|
||||||
|
else:
|
||||||
|
raise HeaderExpectationFailed(
|
||||||
|
f"Unknown Expect: {expect}")
|
||||||
|
request.stream = self
|
||||||
|
if body is True:
|
||||||
|
self.request_chunked = True
|
||||||
|
pos -= 2 # One CRLF stays in buffer
|
||||||
|
else:
|
||||||
|
self.request_bytes_left = body
|
||||||
|
# Remove header and its trailing CRLF
|
||||||
|
del buf[: pos + 4]
|
||||||
|
self.stage = Stage.HANDLER
|
||||||
|
self.request = request
|
||||||
|
|
||||||
async def http1(self):
|
async def http1(self):
|
||||||
"""HTTP 1.1 connection handler"""
|
"""HTTP 1.1 connection handler"""
|
||||||
buf = self.recv_buffer
|
while self.stage is Stage.IDLE and self.keep_alive:
|
||||||
self.url = None
|
try:
|
||||||
while self.keep_alive:
|
# Receive request header and call handler
|
||||||
# Read request header
|
await self.http1_receive_request()
|
||||||
pos = 0
|
await self.protocol.request_handler(self.request)
|
||||||
while len(buf) < self.protocol.request_max_size:
|
if self.stage is Stage.HANDLER:
|
||||||
if buf:
|
raise ServerError("Handler produced no response")
|
||||||
pos = buf.find(b"\r\n\r\n", pos)
|
# Finish sending a response (if no error)
|
||||||
if pos >= 0:
|
if self.stage is Stage.RESPONSE:
|
||||||
break
|
await self.send(end_stream=True)
|
||||||
pos = max(0, len(buf) - 3)
|
# Consume any remaining request body
|
||||||
await self._receive_more()
|
if self.request_bytes_left or self.request_chunked:
|
||||||
|
logger.error(f"{self.request} body not consumed.")
|
||||||
|
async for _ in self:
|
||||||
|
pass
|
||||||
|
except Exception as e:
|
||||||
|
self.exception = e
|
||||||
|
except BaseException:
|
||||||
|
# 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:
|
if self.stage is Stage.IDLE:
|
||||||
self.stage = Stage.REQUEST
|
return
|
||||||
else:
|
# Request failure? Try to respond but then disconnect
|
||||||
self.stage = Stage.HANDLER
|
if self.stage is Stage.REQUEST:
|
||||||
raise PayloadTooLarge("Payload Too Large")
|
self.keep_alive = False
|
||||||
|
self.stage = Stage.HANDLER
|
||||||
|
# Return an error page if possible
|
||||||
|
if self.stage is Stage.HANDLER:
|
||||||
|
app = self.protocol.app
|
||||||
|
response = await app.handle_exception(self.request, e)
|
||||||
|
await self.respond(response).send(end_stream=True)
|
||||||
|
|
||||||
self.protocol._total_request_size = pos + 4
|
|
||||||
|
|
||||||
try:
|
|
||||||
reqline, *headers = buf[:pos].decode().split("\r\n")
|
|
||||||
method, self.url, protocol = reqline.split(" ")
|
|
||||||
if protocol not in ("HTTP/1.0", "HTTP/1.1"):
|
|
||||||
raise Exception
|
|
||||||
self.head_only = method.upper() == "HEAD"
|
|
||||||
headers = Header(
|
|
||||||
(name.lower(), value.lstrip())
|
|
||||||
for name, value in (h.split(":", 1) for h in headers)
|
|
||||||
)
|
|
||||||
except:
|
|
||||||
self.stage = Stage.HANDLER
|
|
||||||
raise InvalidUsage("Bad Request")
|
|
||||||
|
|
||||||
# Prepare a request object from the header received
|
|
||||||
request = self.protocol.request_class(
|
|
||||||
url_bytes=self.url.encode(),
|
|
||||||
headers=headers,
|
|
||||||
version=protocol[-3:],
|
|
||||||
method=method,
|
|
||||||
transport=self.protocol.transport,
|
|
||||||
app=self.protocol.app,
|
|
||||||
)
|
|
||||||
request.stream = self
|
|
||||||
self.protocol.state["requests_count"] += 1
|
|
||||||
self.keep_alive = (
|
|
||||||
protocol == "HTTP/1.1"
|
|
||||||
or headers.get("connection", "").lower() == "keep-alive"
|
|
||||||
)
|
|
||||||
# Prepare for request body
|
|
||||||
body = headers.get("transfer-encoding") == "chunked" or int(
|
|
||||||
headers.get("content-length", 0)
|
|
||||||
)
|
|
||||||
self.request_chunked = False
|
|
||||||
self.request_bytes_left = 0
|
|
||||||
self.stage = Stage.HANDLER
|
|
||||||
if body:
|
|
||||||
expect = headers.get("expect")
|
|
||||||
if expect:
|
|
||||||
if expect.lower() == "100-continue":
|
|
||||||
self.expecting_continue = True
|
|
||||||
else:
|
|
||||||
raise HeaderExpectationFailed(f"Unknown Expect: {expect}")
|
|
||||||
request.stream = self
|
|
||||||
if body is True:
|
|
||||||
self.request_chunked = True
|
|
||||||
pos -= 2 # One CRLF stays in buffer
|
|
||||||
else:
|
|
||||||
self.request_bytes_left = body
|
|
||||||
# Remove header and its trailing CRLF
|
|
||||||
del buf[: pos + 4]
|
|
||||||
|
|
||||||
# Run handler
|
|
||||||
try:
|
|
||||||
await self.protocol.request_handler(request)
|
|
||||||
except Exception:
|
|
||||||
logger.exception("Uncaught from app/handler")
|
|
||||||
await self.write_error(ServerError("Internal Server Error"))
|
|
||||||
if self.stage is Stage.IDLE:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if self.stage is Stage.HANDLER:
|
|
||||||
await self.respond(HTTPResponse(status=204)).send(end_stream=True)
|
|
||||||
|
|
||||||
# Finish sending a response (if no error)
|
|
||||||
elif self.stage is Stage.RESPONSE:
|
|
||||||
await self.send(end_stream=True)
|
|
||||||
|
|
||||||
# Consume any remaining request body
|
|
||||||
if self.request_bytes_left or self.request_chunked:
|
|
||||||
logger.error(
|
|
||||||
f"Handler of {method} {url} did not consume request body."
|
|
||||||
)
|
|
||||||
while await self.read():
|
|
||||||
pass
|
|
||||||
|
|
||||||
self.stage = Stage.IDLE
|
|
||||||
|
|
||||||
async def write_error(self, e):
|
|
||||||
if self.stage is Stage.HANDLER:
|
|
||||||
try:
|
|
||||||
response = HTTPResponse(f"{e}", e.status_code, content_type="text/plain")
|
|
||||||
await self.respond(response).send(end_stream=True)
|
|
||||||
except:
|
|
||||||
logger.exception("Error sending error")
|
|
||||||
|
|
||||||
# Request methods
|
# Request methods
|
||||||
|
|
||||||
|
|
|
@ -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 CancelledError:
|
||||||
except asyncio.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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user