Even more cleanup and error checking, 8 failing tests.
This commit is contained in:
parent
a553e64bbd
commit
7f41c5fa6b
67
sanic/app.py
67
sanic/app.py
|
@ -930,7 +930,7 @@ class Sanic:
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def handle_exception(self, request, exception):
|
async def handle_exception(self, request, exception, name=""):
|
||||||
try:
|
try:
|
||||||
response = self.error_handler.response(request, exception)
|
response = self.error_handler.response(request, exception)
|
||||||
if isawaitable(response):
|
if isawaitable(response):
|
||||||
|
@ -947,6 +947,17 @@ class Sanic:
|
||||||
response = HTTPResponse(
|
response = HTTPResponse(
|
||||||
"An error occurred while handling an error", status=500
|
"An error occurred while handling an error", status=500
|
||||||
)
|
)
|
||||||
|
# Run response middleware
|
||||||
|
if response is not None:
|
||||||
|
try:
|
||||||
|
response = await self._run_response_middleware(
|
||||||
|
request, response, request_name=name
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
error_logger.exception(
|
||||||
|
"Exception occurred in one of response "
|
||||||
|
"middleware handlers"
|
||||||
|
)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
@ -1011,62 +1022,34 @@ class Sanic:
|
||||||
response = handler(request, *args, **kwargs)
|
response = handler(request, *args, **kwargs)
|
||||||
if isawaitable(response):
|
if isawaitable(response):
|
||||||
response = await response
|
response = await response
|
||||||
except CancelledError:
|
# Run response middleware
|
||||||
# If response handler times out, the server handles the error
|
|
||||||
# and cancels the handle_request job.
|
|
||||||
# In this case, the transport is already closed and we cannot
|
|
||||||
# issue a response.
|
|
||||||
response = None
|
|
||||||
cancelled = True
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
# -------------------------------------------- #
|
|
||||||
# Response Generation Failed
|
|
||||||
# -------------------------------------------- #
|
|
||||||
response = await self.handle_exception(request, e)
|
|
||||||
finally:
|
|
||||||
# -------------------------------------------- #
|
|
||||||
# Response Middleware
|
|
||||||
# -------------------------------------------- #
|
|
||||||
# Don't run response middleware if response is None
|
|
||||||
if response is not None:
|
if response is not None:
|
||||||
try:
|
try:
|
||||||
response = await self._run_response_middleware(
|
response = await self._run_response_middleware(
|
||||||
request, response, request_name=name
|
request, response, request_name=name
|
||||||
)
|
)
|
||||||
except CancelledError:
|
except Exception:
|
||||||
# Response middleware can timeout too, as above.
|
|
||||||
response = None
|
|
||||||
cancelled = True
|
|
||||||
except BaseException:
|
|
||||||
error_logger.exception(
|
error_logger.exception(
|
||||||
"Exception occurred in one of response "
|
"Exception occurred in one of response "
|
||||||
"middleware handlers"
|
"middleware handlers"
|
||||||
)
|
)
|
||||||
if cancelled:
|
# Stream response
|
||||||
raise CancelledError()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# pass the response to the correct callback
|
|
||||||
if isinstance(response, StreamingHTTPResponse):
|
if 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:
|
|
||||||
raise ServerError(f"Invalid response type {response} (need HTTPResponse)")
|
|
||||||
except Exception as e:
|
|
||||||
response = await self.handle_exception(request, e)
|
|
||||||
|
|
||||||
# pass the response to the correct callback
|
|
||||||
if response is None:
|
|
||||||
pass
|
|
||||||
elif isinstance(response, StreamingHTTPResponse):
|
|
||||||
await response.stream(request)
|
|
||||||
elif isinstance(response, HTTPResponse):
|
|
||||||
await request.respond(response).send(end_stream=True)
|
|
||||||
else:
|
else:
|
||||||
raise ServerError(
|
raise ServerError(
|
||||||
f"Invalid response type {response} (need HTTPResponse)")
|
f"Invalid response type {response!r} (need HTTPResponse)"
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# -------------------------------------------- #
|
||||||
|
# Response Generation Failed
|
||||||
|
# -------------------------------------------- #
|
||||||
|
response = await self.handle_exception(request, e, name)
|
||||||
|
await request.respond(response).send(end_stream=True)
|
||||||
|
|
||||||
|
|
||||||
# -------------------------------------------------------------------- #
|
# -------------------------------------------------------------------- #
|
||||||
# Testing
|
# Testing
|
||||||
|
|
|
@ -128,13 +128,13 @@ class Http:
|
||||||
logger.error(f"{self.request} body not consumed.")
|
logger.error(f"{self.request} body not consumed.")
|
||||||
async for _ in self:
|
async for _ in self:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except CancelledError:
|
||||||
self.exception = e
|
|
||||||
except BaseException:
|
|
||||||
# Exit after trying to finish a response
|
# Exit after trying to finish a response
|
||||||
self.keep_alive = False
|
self.keep_alive = False
|
||||||
if self.exception is None:
|
if self.exception is None:
|
||||||
self.exception = ServiceUnavailable(f"Cancelled")
|
self.exception = ServiceUnavailable(f"Cancelled")
|
||||||
|
except Exception as e:
|
||||||
|
self.exception = e
|
||||||
if self.exception:
|
if self.exception:
|
||||||
e, self.exception = self.exception, None
|
e, self.exception = self.exception, None
|
||||||
# Exception while idle? Probably best to close connection
|
# Exception while idle? Probably best to close connection
|
||||||
|
|
|
@ -76,6 +76,7 @@ class StreamingHTTPResponse(BaseHTTPResponse):
|
||||||
self.content_type,
|
self.content_type,
|
||||||
).send
|
).send
|
||||||
await self.streaming_fn(self)
|
await self.streaming_fn(self)
|
||||||
|
await self.send(end_stream=True)
|
||||||
|
|
||||||
class HTTPResponse(BaseHTTPResponse):
|
class HTTPResponse(BaseHTTPResponse):
|
||||||
__slots__ = ("body", "status", "content_type", "headers", "_cookies")
|
__slots__ = ("body", "status", "content_type", "headers", "_cookies")
|
||||||
|
|
|
@ -167,7 +167,8 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
await self._data_received.wait()
|
await self._data_received.wait()
|
||||||
|
|
||||||
def check_timeouts(self):
|
def check_timeouts(self):
|
||||||
"""Runs itself once a second to enforce any expired timeouts."""
|
"""Runs itself periodically to enforce any expired timeouts."""
|
||||||
|
try:
|
||||||
if not self._task:
|
if not self._task:
|
||||||
return
|
return
|
||||||
duration = current_time() - self._time
|
duration = current_time() - self._time
|
||||||
|
@ -177,14 +178,17 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
elif stage is Stage.REQUEST and duration > self.request_timeout:
|
elif stage is Stage.REQUEST and duration > self.request_timeout:
|
||||||
self._http.exception = RequestTimeout("Request Timeout")
|
self._http.exception = RequestTimeout("Request Timeout")
|
||||||
elif (
|
elif (
|
||||||
stage in (Stage.REQUEST, Stage.FAILED)
|
stage in (Stage.HANDLER, Stage.RESPONSE, Stage.FAILED)
|
||||||
and duration > self.response_timeout
|
and duration > self.response_timeout
|
||||||
):
|
):
|
||||||
self._http.exception = ServiceUnavailable("Response Timeout")
|
self._http.exception = RequestTimeout("Response Timeout")
|
||||||
else:
|
else:
|
||||||
self.loop.call_later(1.0, self.check_timeouts)
|
interval = min(self.keep_alive_timeout, self.request_timeout, self.response_timeout) / 2
|
||||||
|
self.loop.call_later(max(0.1, interval), self.check_timeouts)
|
||||||
return
|
return
|
||||||
self._task.cancel()
|
self._task.cancel()
|
||||||
|
except:
|
||||||
|
logger.exception("protocol.check_timeouts")
|
||||||
|
|
||||||
async def send(self, data):
|
async def send(self, data):
|
||||||
"""Writes data with backpressure control."""
|
"""Writes data with backpressure control."""
|
||||||
|
@ -232,6 +236,10 @@ class HttpProtocol(asyncio.Protocol):
|
||||||
self.resume_writing()
|
self.resume_writing()
|
||||||
if self._task:
|
if self._task:
|
||||||
self._task.cancel()
|
self._task.cancel()
|
||||||
|
if self._debug and self._http and self._http.request:
|
||||||
|
logger.error(
|
||||||
|
f"Connection lost before response written @ {self._http.request.ip}",
|
||||||
|
)
|
||||||
except:
|
except:
|
||||||
logger.exception("protocol.connection_lost")
|
logger.exception("protocol.connection_lost")
|
||||||
|
|
||||||
|
|
|
@ -109,12 +109,11 @@ def test_log_connection_lost(app, debug, monkeypatch):
|
||||||
@app.route("/conn_lost")
|
@app.route("/conn_lost")
|
||||||
async def conn_lost(request):
|
async def conn_lost(request):
|
||||||
response = text("Ok")
|
response = text("Ok")
|
||||||
response.output = Mock(side_effect=RuntimeError)
|
request.transport.close()
|
||||||
return response
|
return response
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
req, res = app.test_client.get("/conn_lost", debug=debug)
|
||||||
# catch ValueError: Exception during request
|
assert res is None
|
||||||
app.test_client.get("/conn_lost", debug=debug)
|
|
||||||
|
|
||||||
log = stream.getvalue()
|
log = stream.getvalue()
|
||||||
|
|
||||||
|
|
|
@ -54,7 +54,7 @@ def test_middleware_response(app):
|
||||||
|
|
||||||
|
|
||||||
def test_middleware_response_exception(app):
|
def test_middleware_response_exception(app):
|
||||||
result = {"status_code": None}
|
result = {"status_code": "middleware not run"}
|
||||||
|
|
||||||
@app.middleware("response")
|
@app.middleware("response")
|
||||||
async def process_response(request, response):
|
async def process_response(request, response):
|
||||||
|
|
|
@ -79,8 +79,12 @@ class DelayableSanicTestClient(SanicTestClient):
|
||||||
|
|
||||||
request_timeout_default_app = Sanic("test_request_timeout_default")
|
request_timeout_default_app = Sanic("test_request_timeout_default")
|
||||||
request_no_timeout_app = Sanic("test_request_no_timeout")
|
request_no_timeout_app = Sanic("test_request_no_timeout")
|
||||||
request_timeout_default_app.config.REQUEST_TIMEOUT = 0.6
|
|
||||||
request_no_timeout_app.config.REQUEST_TIMEOUT = 0.6
|
# Note: The delayed client pauses before making a request, so technically
|
||||||
|
# it is in keep alive duration. Earlier Sanic versions entered a new connection
|
||||||
|
# in request mode even if no bytes of request were received.
|
||||||
|
request_timeout_default_app.config.KEEP_ALIVE_TIMEOUT = 0.6
|
||||||
|
request_no_timeout_app.config.KEEP_ALIVE_TIMEOUT = 0.6
|
||||||
|
|
||||||
|
|
||||||
@request_timeout_default_app.route("/1")
|
@request_timeout_default_app.route("/1")
|
||||||
|
|
|
@ -469,6 +469,7 @@ def test_file_stream_response_range(
|
||||||
)
|
)
|
||||||
|
|
||||||
request, response = app.test_client.get("/files/{}".format(file_name))
|
request, response = app.test_client.get("/files/{}".format(file_name))
|
||||||
|
print(response.body)
|
||||||
assert response.status == 206
|
assert response.status == 206
|
||||||
assert "Content-Range" in response.headers
|
assert "Content-Range" in response.headers
|
||||||
assert response.headers["Content-Range"] == "bytes {}-{}/{}".format(
|
assert response.headers["Content-Range"] == "bytes {}-{}/{}".format(
|
||||||
|
|
Loading…
Reference in New Issue
Block a user