Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Close responses when on cancellations occur during reading. #2156

Merged
merged 4 commits into from Mar 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
12 changes: 6 additions & 6 deletions httpx/_client.py
Expand Up @@ -900,7 +900,7 @@ def send(

return response

except Exception as exc:
except BaseException as exc:
response.close()
raise exc

Expand Down Expand Up @@ -932,7 +932,7 @@ def _send_handling_auth(
request = next_request
history.append(response)

except Exception as exc:
except BaseException as exc:
response.close()
raise exc
finally:
Expand Down Expand Up @@ -971,7 +971,7 @@ def _send_handling_redirects(
response.next_request = request
return response

except Exception as exc:
except BaseException as exc:
response.close()
raise exc

Expand Down Expand Up @@ -1604,7 +1604,7 @@ async def send(

return response

except Exception as exc: # pragma: no cover
except BaseException as exc: # pragma: no cover
await response.aclose()
raise exc

Expand Down Expand Up @@ -1636,7 +1636,7 @@ async def _send_handling_auth(
request = next_request
history.append(response)

except Exception as exc:
except BaseException as exc:
await response.aclose()
raise exc
finally:
Expand Down Expand Up @@ -1676,7 +1676,7 @@ async def _send_handling_redirects(
response.next_request = request
return response

except Exception as exc:
except BaseException as exc:
await response.aclose()
raise exc

Expand Down
40 changes: 40 additions & 0 deletions tests/client/test_async_client.py
Expand Up @@ -324,6 +324,46 @@ async def hello_world(request):
assert response.text == "Hello, world!"


@pytest.mark.usefixtures("async_environment")
async def test_cancellation_during_stream():
"""
If any BaseException is raised during streaming the response, then the
stream should be closed.

This includes:

* `asyncio.CancelledError` (A subclass of BaseException from Python 3.8 onwards.)
* `trio.Cancelled`
* `KeyboardInterrupt`
* `SystemExit`

See https://github.com/encode/httpx/issues/2139
"""
stream_was_closed = False

def response_with_cancel_during_stream(request):
class CancelledStream(httpx.AsyncByteStream):
async def __aiter__(self) -> typing.AsyncIterator[bytes]:
yield b"Hello"
raise KeyboardInterrupt()
yield b", world" # pragma: nocover

async def aclose(self) -> None:
nonlocal stream_was_closed
stream_was_closed = True

return httpx.Response(
200, headers={"Content-Length": "12"}, stream=CancelledStream()
)

transport = httpx.MockTransport(response_with_cancel_during_stream)

async with httpx.AsyncClient(transport=transport) as client:
with pytest.raises(KeyboardInterrupt):
await client.get("https://www.example.com")
assert stream_was_closed


@pytest.mark.usefixtures("async_environment")
async def test_server_extensions(server):
url = server.url
Expand Down