Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 11 additions & 7 deletions httpx/dispatch/connection_pool.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import typing
from itertools import chain

from ..concurrency.asyncio import AsyncioBackend
from ..concurrency.base import ConcurrencyBackend
Expand Down Expand Up @@ -62,10 +63,11 @@ def add(self, connection: HTTPConnection) -> None:
self.by_origin[connection.origin] = {connection: 0.0}

def remove(self, connection: HTTPConnection) -> None:
del self.all[connection]
del self.by_origin[connection.origin][connection]
if not self.by_origin[connection.origin]:
del self.by_origin[connection.origin]
if connection in self.all:
del self.all[connection]
del self.by_origin[connection.origin][connection]
if not self.by_origin[connection.origin]:
del self.by_origin[connection.origin]

def clear(self) -> None:
self.all.clear()
Expand Down Expand Up @@ -175,7 +177,9 @@ async def release_connection(self, connection: HTTPConnection) -> None:

async def close(self) -> None:
self.is_closed = True
connections = list(self.keepalive_connections)
self.keepalive_connections.clear()
for connection in connections:

for connection in chain(self.keepalive_connections, self.active_connections):
await connection.close()

self.keepalive_connections.clear()
self.active_connections.clear()
20 changes: 16 additions & 4 deletions tests/client/test_async_client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
from h11 import RemoteProtocolError

import httpx

Expand Down Expand Up @@ -43,13 +44,24 @@ async def test_post_json(server, backend):
assert response.status_code == 200


async def test_stream_manger_release_resources(server, backend):
async with httpx.AsyncClient(backend=backend) as http:
response = await http.post(
server.url.copy_with(path="/echo_body"), stream=True, data=b"*" * 409600
)

assert response.status_code == 200
with pytest.raises(RemoteProtocolError):
await response.read()


async def test_stream_response(server, backend):
async with httpx.AsyncClient(backend=backend) as client:
response = await client.request("GET", server.url, stream=True)
assert response.status_code == 200
body = await response.read()
assert body == b"Hello, world!"
assert response.content == b"Hello, world!"
assert response.status_code == 200
body = await response.read()
assert body == b"Hello, world!"
assert response.content == b"Hello, world!"


async def test_access_content_stream_response(server, backend):
Expand Down
40 changes: 26 additions & 14 deletions tests/client/test_client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pytest
from h11 import RemoteProtocolError

import httpx

Expand Down Expand Up @@ -33,33 +34,44 @@ def test_post_json(server):
assert response.reason_phrase == "OK"


def test_stream_manger_release_resources(server):
with httpx.Client() as http:
response = http.post(
server.url.copy_with(path="/echo_body"), stream=True, data=b"*" * 409600
)

assert response.status_code == 200
with pytest.raises(RemoteProtocolError):
response.read()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should not let an h11 exception bubble up to our users; we need to raise an HTTPX-specific exception here. I believe ResponseClosed is the most adequate.

This made me realize that we are not force-closing responses yet here on client exit. You can see this by turning on HTTPX_DEBUG and seeing receive_event NEED_DATA log lines because the response object is still trying to receive response data, and that is because we did not call .close() on it to send a ResponseClosed event to h11.

I guess implementing response closing on client exit involves some form of storing "pending responses" on the client. We'd add the response there in the .request() methods, remove it when the response's on_close() callback is normally called, and iterate over it in client's __exit__() or __aexit__() to close any pending responses.

Would you like to look into this?



def test_stream_response(server):
with httpx.Client() as http:
response = http.get(server.url, stream=True)
assert response.status_code == 200
content = response.read()
assert content == b"Hello, world!"
assert response.status_code == 200
content = response.read()
assert content == b"Hello, world!"


def test_stream_iterator(server):
with httpx.Client() as http:
response = http.get(server.url, stream=True)
assert response.status_code == 200
body = b""
for chunk in response.stream():
body += chunk
assert body == b"Hello, world!"
assert response.status_code == 200
body = b""
for chunk in response.stream():
body += chunk
assert body == b"Hello, world!"


def test_raw_iterator(server):
with httpx.Client() as http:
response = http.get(server.url, stream=True)
assert response.status_code == 200
body = b""
for chunk in response.raw():
body += chunk
assert body == b"Hello, world!"
response.close() # TODO: should Response be available as context managers?
assert response.status_code == 200
body = b""
for chunk in response.raw():
body += chunk
assert body == b"Hello, world!"
response.close() # TODO: should Response be available as context managers?


def test_raise_for_status(server):
Expand Down
11 changes: 11 additions & 0 deletions tests/dispatch/test_connection_pools.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,17 @@ async def test_standard_response_close(server, backend):
assert len(http.keepalive_connections) == 1


async def test_close_connection_on_context_manager_exit(server, backend):
"""
After block with all connections should be closed.
"""
async with httpx.ConnectionPool(backend=backend) as http:
await http.request("GET", server.url)

assert len(http.active_connections) == 0
assert len(http.keepalive_connections) == 0


async def test_premature_response_close(server, backend):
"""
A premature close should close the connection.
Expand Down