Skip to content

Commit

Permalink
Better request cancel handling (sanic-org#2513)
Browse files Browse the repository at this point in the history
  • Loading branch information
ahopkins authored Sep 19, 2022
1 parent 7f894c4 commit 389363a
Show file tree
Hide file tree
Showing 7 changed files with 46 additions and 8 deletions.
5 changes: 5 additions & 0 deletions sanic/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
from asyncio import CancelledError
from typing import Any, Dict, Optional, Union

from sanic.helpers import STATUS_CODES


class RequestCancelled(CancelledError):
quiet = True


class SanicException(Exception):
message: str = ""

Expand Down
10 changes: 7 additions & 3 deletions sanic/http/http1.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@
BadRequest,
ExpectationFailed,
PayloadTooLarge,
RequestCancelled,
ServerError,
ServiceUnavailable,
)
from sanic.headers import format_http1_response
from sanic.helpers import has_message_body
Expand Down Expand Up @@ -132,15 +132,19 @@ async def http1(self):

if self.stage is Stage.RESPONSE:
await self.response.send(end_stream=True)
except CancelledError:
except CancelledError as exc:
# Write an appropriate response before exiting
if not self.protocol.transport:
logger.info(
f"Request: {self.request.method} {self.request.url} "
"stopped. Transport is closed."
)
return
e = self.exception or ServiceUnavailable("Cancelled")
e = (
RequestCancelled()
if self.protocol.conn_info.lost
else (self.exception or exc)
)
self.exception = None
self.keep_alive = False
await self.error_response(e)
Expand Down
2 changes: 2 additions & 0 deletions sanic/models/server_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class ConnInfo:
"client",
"client_ip",
"ctx",
"lost",
"peername",
"server_port",
"server",
Expand All @@ -33,6 +34,7 @@ class ConnInfo:

def __init__(self, transport: TransportProtocol, unix=None):
self.ctx = SimpleNamespace()
self.lost = False
self.peername = None
self.server = self.client = ""
self.server_port = self.client_port = 0
Expand Down
6 changes: 4 additions & 2 deletions sanic/server/protocols/base_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

from typing import TYPE_CHECKING, Optional

from sanic.exceptions import RequestCancelled


if TYPE_CHECKING:
from sanic.app import Sanic

import asyncio

from asyncio import CancelledError
from asyncio.transports import Transport
from time import monotonic as current_time

Expand Down Expand Up @@ -69,7 +70,7 @@ async def send(self, data):
"""
await self._can_write.wait()
if self.transport.is_closing():
raise CancelledError
raise RequestCancelled
self.transport.write(data)
self._time = current_time()

Expand Down Expand Up @@ -120,6 +121,7 @@ def connection_lost(self, exc):
try:
self.connections.discard(self)
self.resume_writing()
self.conn_info.lost = True
if self._task:
self._task.cancel()
except BaseException:
Expand Down
8 changes: 6 additions & 2 deletions sanic/server/protocols/http_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,11 @@
from asyncio import CancelledError
from time import monotonic as current_time

from sanic.exceptions import RequestTimeout, ServiceUnavailable
from sanic.exceptions import (
RequestCancelled,
RequestTimeout,
ServiceUnavailable,
)
from sanic.http import Http, Stage
from sanic.log import Colors, error_logger, logger
from sanic.models.server_types import ConnInfo
Expand Down Expand Up @@ -225,7 +229,7 @@ async def send(self, data): # no cov
"""
await self._can_write.wait()
if self.transport.is_closing():
raise CancelledError
raise RequestCancelled
await self.app.dispatch(
"http.lifecycle.send",
inline=True,
Expand Down
21 changes: 21 additions & 0 deletions tests/test_cancellederror.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import asyncio

from asyncio import CancelledError

import pytest

from sanic import Request, Sanic, json


def test_can_raise_in_handler(app: Sanic):
@app.get("/")
async def handler(request: Request):
raise CancelledError("STOP!!")

@app.exception(CancelledError)
async def handle_cancel(request: Request, exc: CancelledError):
return json({"message": exc.args[0]}, status=418)

_, response = app.test_client.get("/")
assert response.status == 418
assert response.json["message"] == "STOP!!"
2 changes: 1 addition & 1 deletion tests/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ def handler(request):
with caplog.at_level(logging.ERROR):
reqrequest, response = app.test_client.get("/")

assert response.status == 503
assert response.status == 500
assert (
"sanic.error",
logging.ERROR,
Expand Down

0 comments on commit 389363a

Please sign in to comment.