Checklist
Describe the bug
When connecting to HTTP2 streams, if the other side closes the connection (causing the socket to go into a CLOSE_WAIT state), the Client/Stream doesn't handle that appropriately, instead it keeps reading b'' from a closed socket forever.
To reproduce
Install from master:
pip install git+https://github.com/encode/httpx.git@bacc2d18350d9f6645828461435fe6e19b9dc518
Generate self-signed keys:
openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' -keyout key.pem -out cert.pem
I'm using nodejs version 14.5 to create a small HTTP2 server that streams continuously, called server.js.
consthttp2=require('http2');constfs=require('fs');constserver=http2.createSecureServer({key: fs.readFileSync('key.pem'),cert: fs.readFileSync('cert.pem')});server.on('error',(err)=>console.error(err));server.on('stream',(stream,headers,flags)=>{stream.respond({':status': 200,'content-type': 'text/event-stream'});setInterval(()=>stream.write(`${Math.random()}\n`),500);});server.listen(8443);To install nodejs:
$ curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash -
..
$ sudo apt-get install -y nodejs
...
$ nodejs -v
v14.5.0
$
A small python client that talks to the server (client.py):
importhttpxdefget_client():
timeout=httpx.Timeout(read_timeout=5.0)
client=httpx.Client(
base_url="https://localhost:8443",
http2=True,
timeout=timeout,
verify="cert.pem",
)
returnclientdefmain():
withget_client() asc, c.stream("GET", "") ass:
foriins.iter_lines():
print(i)
if__name__=='__main__':
main()I start the server:
In another terminal I then connect with the client and I start getting things like:
$ python3.8 client.py
0.5366774977298967
0.282442060319521
0.1150984598113638
0.3190482876162004
...
I then kill the server:
Expected behavior
The client exits somewhat gracefully, perhaps logging a warning that the other side closed the connection.
Actual behavior
The client just blocks. On inspecting with strace it seems to be continuously reading from an empty buffer:
$ sudo strace -p <PID OF client.py>
read(5, "", 5) = 0
ioctl(5, FIONBIO, [1]) = 0
read(5, "", 5) = 0
ioctl(5, FIONBIO, [1]) = 0
read(5, "", 5) = 0
ioctl(5, FIONBIO, [1]) = 0
read(5, "", 5) = 0
...
Debugging material
I was able to track the bug to SyncSocketStream.read:
defread(self, n: int, timeout: TimeoutDict) ->bytes:
read_timeout=timeout.get("read")
exc_map= {socket.timeout: ReadTimeout, socket.error: ReadError}
withself.read_lock:
withmap_exceptions(exc_map):
self.sock.settimeout(read_timeout)
returnself.sock.recv(n) # returns b''Here self.sock.recv(n) returns simply b'' but that's never handled further up the call stack:
>/home/<username>/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_backends/sync.py(63)read()
->returnself.sock.recv(n)
(Pdb) pself.sock.recv(n)
b''
If I keep pressing n it comes back around:
(Pdb) n>/home/<username>/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_backends/sync.py(63)read()
->returnself.sock.recv(n)
(Pdb) n--Return-->/home/<username>/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_backends/sync.py(63)read()->b''->returnself.sock.recv(n)
(Pdb) >/home/<username>/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_sync/http2.py(205)receive_events()
->events=self.h2_state.receive_data(data)
(Pdb) >/home/<username>/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_sync/http2.py(206)receive_events()
->foreventinevents:
(Pdb) >/home/<username>/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_sync/http2.py(216)receive_events()
->data_to_send=self.h2_state.data_to_send()
(Pdb) >/home/<username>/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_sync/http2.py(217)receive_events()
->self.socket.write(data_to_send, timeout)
(Pdb) --Return-->/home/<username>/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_sync/http2.py(217)receive_events()->None->self.socket.write(data_to_send, timeout)
(Pdb) >/home/<username>/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_sync/http2.py(196)wait_for_event()
->whilenotself.events[stream_id]:
(Pdb) >/home/<username>/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_sync/http2.py(197)wait_for_event()
->self.receive_events(timeout)
(Pdb) >/home/<username>/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_backends/sync.py(63)read()
->returnself.sock.recv(n)
Environment
- OS: Ubuntu 18.04.1
- Python version: 3.8.0
- HTTPX version: Pinned at bacc2d18350d9f6645828461435fe6e19b9dc518
- Async environment: asyncio
- HTTP proxy: no
- Custom certificates: yes (but only because I couldn't get nodejs http2 to work without them)
I'm happy to provide more information if that's not sufficient.
Checklist
master.Describe the bug
When connecting to HTTP2 streams, if the other side closes the connection (causing the socket to go into a
CLOSE_WAITstate), the Client/Stream doesn't handle that appropriately, instead it keeps readingb''from a closed socket forever.To reproduce
Install from master:
Generate self-signed keys:
I'm using
nodejsversion 14.5 to create a small HTTP2 server that streams continuously, calledserver.js.To install
nodejs:A small python client that talks to the server (
client.py):I start the server:
In another terminal I then connect with the client and I start getting things like:
I then kill the server:
Expected behavior
The client exits somewhat gracefully, perhaps logging a warning that the other side closed the connection.
Actual behavior
The client just blocks. On inspecting with strace it seems to be continuously reading from an empty buffer:
Debugging material
I was able to track the bug to
SyncSocketStream.read:Here
self.sock.recv(n)returns simplyb''but that's never handled further up the call stack:If I keep pressing
nit comes back around:Environment
I'm happy to provide more information if that's not sufficient.