Skip to content

Commit

Permalink
Fix response not closed when read stream canceled
Browse files Browse the repository at this point in the history
  • Loading branch information
guyskk committed Mar 28, 2022
1 parent c82885a commit 098c8d6
Show file tree
Hide file tree
Showing 3 changed files with 46 additions and 6 deletions.
12 changes: 6 additions & 6 deletions httpx/_client.py
Expand Up @@ -901,7 +901,7 @@ def send(

return response

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

Expand Down Expand Up @@ -933,7 +933,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 @@ -972,7 +972,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 @@ -1605,7 +1605,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 @@ -1637,7 +1637,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 @@ -1677,7 +1677,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
14 changes: 14 additions & 0 deletions tests/client/test_async_client.py
@@ -1,3 +1,4 @@
import asyncio
import typing
from datetime import timedelta

Expand Down Expand Up @@ -331,3 +332,16 @@ async def test_server_extensions(server):
response = await client.get(url)
assert response.status_code == 200
assert response.extensions["http_version"] == b"HTTP/1.1"


@pytest.mark.asyncio
async def test_cancelled_response(server):
async with httpx.AsyncClient() as client:
url = server.url.join("/drip?delay=0&duration=0.1")
response = await asyncio.wait_for(client.get(url), 0.2)
assert response.status_code == 200
assert response.content == b"*"
async with httpx.AsyncClient() as client:
url = server.url.join("/drip?delay=0&duration=0.5")
with pytest.raises(asyncio.TimeoutError):
await asyncio.wait_for(client.get(url), 0.2)
26 changes: 26 additions & 0 deletions tests/conftest.py
Expand Up @@ -4,6 +4,7 @@
import threading
import time
import typing
import urllib.parse

import pytest
import trustme
Expand Down Expand Up @@ -76,6 +77,8 @@ async def app(scope, receive, send):
assert scope["type"] == "http"
if scope["path"].startswith("/slow_response"):
await slow_response(scope, receive, send)
if scope["path"].startswith("/drip"):
await drip_response(scope, receive, send)
elif scope["path"].startswith("/status"):
await status_code(scope, receive, send)
elif scope["path"].startswith("/echo_body"):
Expand Down Expand Up @@ -126,6 +129,29 @@ async def slow_response(scope, receive, send):
await send({"type": "http.response.body", "body": b"Hello, world!"})


async def drip_response(scope, receive, send):
"""
Drips data over a duration after an optional initial delay.
eg: https://httpbin.org/drip?delay=0&duration=1
"""
qs = urllib.parse.parse_qs(scope["query_string"].decode())
delay = float(qs.get("delay", ["0"])[0])
duration = float(qs.get("duration", ["1"])[0])
await sleep(delay)
await send(
{
"type": "http.response.start",
"status": 200,
"headers": [[b"content-type", b"text/plain"]],
}
)
drip = {"type": "http.response.body", "body": b"*", "more_body": True}
for _ in range(int(duration * 10)):
await send(drip)
await sleep(0.1)
await send({"type": "http.response.body", "body": b""})


async def status_code(scope, receive, send):
status_code = int(scope["path"].replace("/status/", ""))
await send(
Expand Down

0 comments on commit 098c8d6

Please sign in to comment.