Skip to content

Client/Stream doesn't handle CLOSE_WAIT sockets well (meaning other side closed the connection), loops around forever #110

Closed

Description

@nawarnoori

Checklist

  • The bug is reproducible against the latest release and/or master.
  • There are no similar issues or pull requests to fix it yet.

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.

const http2 = require('http2');
const fs = require('fs');

const server = 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):

import httpx


def get_client():
    timeout = httpx.Timeout(read_timeout=5.0)
    client = httpx.Client(
        base_url="https://localhost:8443",
        http2=True,
        timeout=timeout,
        verify="cert.pem",
    )
    return client


def main():
    with get_client() as c, c.stream("GET", "") as s:
        for i in s.iter_lines():
            print(i)


if __name__ == '__main__':
    main()

I start the server:

nodejs server.js

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:

$ nodejs server.js
^C
$

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:

def read(self, n: int, timeout: TimeoutDict) -> bytes:
    read_timeout = timeout.get("read")
    exc_map = {socket.timeout: ReadTimeout, socket.error: ReadError}
    
    with self.read_lock:
        with map_exceptions(exc_map):
    	self.sock.settimeout(read_timeout)
    	return self.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()
-> return self.sock.recv(n)
(Pdb) p self.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()
-> return self.sock.recv(n)
(Pdb) n
--Return--
> /home/<username>/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_backends/sync.py(63)read()->b''
-> return self.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()
-> for event in events:
(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()
-> while not self.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()
-> return self.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.

Activity

  1. florimondmanca commented on Jul 3, 2020

    @florimondmanca
    Contributor

    Hi there, thanks for this, some "off the top of my head" thoughts…

    My understanding here is that in general sock.recv() returning b"" should be treated as "the peer has disconnected", so we should check for that and raise some kind of disconnect exception when that happens, correct?

    We do have an internal logic for checking "is this connection dropped?", but I'm not sure a/ if it's done in the sync case, and b/ if it's given a chance to run between reads on the socket. This would need double-checking.

    Could you verify what's the behavior with an async client? My assumption is that we wouldn't see this behavior in that case, and instead something else (eg trio would raise a BrokenResourceError that we'd probably re-raise as a NetworkError, not sure about asyncio).

    So, what does this do…?

    import asyncio
    import trio
    import httpx
    
    def get_client():
        timeout = httpx.Timeout(read_timeout=5.0)
        client = httpx.AsyncClient(
            base_url="https://localhost:8443",
            http2=True,
            timeout=timeout,
            verify="cert.pem",
        )
        return client
    
    async def main():
        async with get_client() as c, c.stream("GET", "") as s:
            async for i in s.iter_lines():
                print(i)
    
    
    if __name__ == '__main__':
        # Comment out whichever shouldn't run.
        asyncio.run(main())
        trio.run(main)
  2. florimondmanca commented on Jul 3, 2020

    @florimondmanca
    Contributor

    Also note-worthy that this is most likely a bug in httpcore, rather than HTTPX. Going to transfer the issue over there…

  3. transferred this issue fromencode/httpxon Jul 3, 2020
  4. nawarnoori commented on Jul 5, 2020

    @nawarnoori
    Author

    Hi, @florimondmanca

    Yes, I think disconnecting and raising some kind of specific exception that indicates the other side has closed or something's happened to the socket is all fair. I'd ideally like to be able to work with the sync version of httpx since I work with an internal framework that doesn't support async/await and instead relies on gevent.

    So for the sake of testing the async client, I put together this short client where you can try out both asyncio and trio:

    import sys
    import asyncio
    
    import trio
    import httpx
    
    
    def get_client():
        timeout = httpx.Timeout(read_timeout=5.0)
        client = httpx.AsyncClient(
            base_url="https://localhost:8443",
            http2=True,
            timeout=timeout,
            verify="cert.pem",
        )
        return client
    
    
    async def main():
        async with get_client() as c, c.stream("GET", "") as s:
            async for i in s.aiter_lines():
                print(i)
    
    
    if __name__ == '__main__':
        if (
            len(sys.argv) != 2
            or (lib := sys.argv[1]) not in ("asyncio", "trio")
        ):
            raise SystemExit("Pass trio/asyncio as sole arg.")
        if lib == "asyncio":
            asyncio.run(main())
        else:
            trio.run(main)

    With asyncio, the strace output is a little different on account of using epoll internally but otherwise same effect, the client locks up and strace spews a stream of:

    getpid()                                = 5922
    epoll_wait(3, [], 1, 0)                 = 0
    epoll_wait(3, [], 1, 0)                 = 0
    epoll_wait(3, [], 1, 0)                 = 0
    getpid()                                = 5922
    ...
    

    ad infinitum. The curious bit here is that the socket is gone.

    This is before closing the server (you can clearly see the socket being in an ESTABLISHED state):

    $ sudo lsof -np <async client PID> | grep IPv4
    python3.8 6742 myusername    8u     IPv4          552902352      0t0       TCP 127.0.0.1:52602->127.0.0.1:8443 (ESTABLISHED)
    

    After closing the server:

    $ sudo lsof -np <async client PID> | grep IPv4
    $
    

    but the client is still hanging. On Ctrl-C-ing it, I get a traceback that's along the lines of:

    ^CTraceback (most recent call last):
      File "async_client.py", line 31, in <module>
        asyncio.run(main())
      File "/usr/lib/python3.8/asyncio/runners.py", line 43, in run
        return loop.run_until_complete(main)
      File "/usr/lib/python3.8/asyncio/base_events.py", line 595, in run_until_complete
        self.run_forever()
      File "/usr/lib/python3.8/asyncio/base_events.py", line 563, in run_forever
        self._run_once()
      File "/usr/lib/python3.8/asyncio/base_events.py", line 1844, in _run_once
        handle._run()
      File "/usr/lib/python3.8/asyncio/events.py", line 81, in _run
        self._context.run(self._callback, *self._args)
      File "/usr/lib/python3.8/asyncio/streams.py", line 704, in read
        data = bytes(self._buffer[:n])
    KeyboardInterrupt
    

    I got curious so I then ran the client with extra diags, passing debug=True to asyncio.run and setting PYTHONASYNCIODEBUG to 1:

    PYTHONASYNCIODEBUG=1 python3.8 async_client.py asyncio
    

    After killing the server and attaching strace, I got:

    getpid()                                = 6990
    stat("/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", {st_mode=S_IFREG|0664, st_size=14247, ...}) = 0
    stat("async_client.py", {st_mode=S_IFREG|0664, st_size=717, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/events.py", {st_mode=S_IFREG|0644, st_size=26170, ...}) = 0
    stat("/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/connection_pool.py", {st_mode=S_IFREG|0664, st_size=11305, ...}) = 0
    stat("/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", {st_mode=S_IFREG|0664, st_size=39294, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/base_events.py", {st_mode=S_IFREG|0644, st_size=72001, ...}) = 0
    stat("/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/base.py", {st_mode=S_IFREG|0664, st_size=3849, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/tasks.py", {st_mode=S_IFREG|0644, st_size=33076, ...}) = 0
    stat("/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_backends/asyncio.py", {st_mode=S_IFREG|0664, st_size=8639, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/runners.py", {st_mode=S_IFREG|0644, st_size=2021, ...}) = 0
    stat("/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", {st_mode=S_IFREG|0664, st_size=14247, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/events.py", {st_mode=S_IFREG|0644, st_size=26170, ...}) = 0
    stat("/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/connection_pool.py", {st_mode=S_IFREG|0664, st_size=11305, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/base_events.py", {st_mode=S_IFREG|0644, st_size=72001, ...}) = 0
    stat("/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/base.py", {st_mode=S_IFREG|0664, st_size=3849, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/tasks.py", {st_mode=S_IFREG|0644, st_size=33076, ...}) = 0
    stat("/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_backends/asyncio.py", {st_mode=S_IFREG|0664, st_size=8639, ...}) = 0
    stat("/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", {st_mode=S_IFREG|0664, st_size=14247, ...}) = 0
    stat("async_client.py", {st_mode=S_IFREG|0664, st_size=717, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/events.py", {st_mode=S_IFREG|0644, st_size=26170, ...}) = 0
    stat("/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/connection_pool.py", {st_mode=S_IFREG|0664, st_size=11305, ...}) = 0
    stat("/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", {st_mode=S_IFREG|0664, st_size=39294, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/base_events.py", {st_mode=S_IFREG|0644, st_size=72001, ...}) = 0
    stat("/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/base.py", {st_mode=S_IFREG|0664, st_size=3849, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/tasks.py", {st_mode=S_IFREG|0644, st_size=33076, ...}) = 0
    stat("/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_backends/asyncio.py", {st_mode=S_IFREG|0664, st_size=8639, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/runners.py", {st_mode=S_IFREG|0644, st_size=2021, ...}) = 0
    stat("/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", {st_mode=S_IFREG|0664, st_size=14247, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/base_events.py", {st_mode=S_IFREG|0644, st_size=72001, ...}) = 0
    stat("/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/base.py", {st_mode=S_IFREG|0664, st_size=3849, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/tasks.py", {st_mode=S_IFREG|0644, st_size=33076, ...}) = 0
    stat("/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_backends/asyncio.py", {st_mode=S_IFREG|0664, st_size=8639, ...}) = 0
    epoll_wait(3, [], 1, 0)                 = 0
    stat("/usr/lib/python3.8/asyncio/runners.py", {st_mode=S_IFREG|0644, st_size=2021, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/base_events.py", {st_mode=S_IFREG|0644, st_size=72001, ...}) = 0
    stat("async_client.py", {st_mode=S_IFREG|0664, st_size=717, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/events.py", {st_mode=S_IFREG|0644, st_size=26170, ...}) = 0
    epoll_wait(3, [], 1, 0)                 = 0
    stat("async_client.py", {st_mode=S_IFREG|0664, st_size=717, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/events.py", {st_mode=S_IFREG|0644, st_size=26170, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/base_events.py", {st_mode=S_IFREG|0644, st_size=72001, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/tasks.py", {st_mode=S_IFREG|0644, st_size=33076, ...}) = 0
    stat("/usr/lib/python3.8/asyncio/runners.py", {st_mode=S_IFREG|0644, st_size=2021, ...}) = 0
    epoll_wait(3, [], 1, 0)                 = 0
    getpid()                                = 6990
    ...
    

    which was much more revealing, and on killing the client, the following traceback:

    ^CTraceback (most recent call last):
      File "async_client.py", line 31, in <module>
        asyncio.run(main(), debug=True)
      File "/usr/lib/python3.8/asyncio/runners.py", line 43, in run
        return loop.run_until_complete(main)
      File "/usr/lib/python3.8/asyncio/base_events.py", line 595, in run_until_complete
        self.run_forever()
      File "/usr/lib/python3.8/asyncio/base_events.py", line 563, in run_forever
        self._run_once()
      File "/usr/lib/python3.8/asyncio/base_events.py", line 1836, in _run_once
        handle._run()
      File "/usr/lib/python3.8/asyncio/events.py", line 81, in _run
        self._context.run(self._callback, *self._args)
      File "async_client.py", line 20, in main
        async for i in s.aiter_lines():
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 993, in aiter_lines
        async for text in self.aiter_text():
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 987, in aiter_text
        async for chunk in self.aiter_bytes():
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 976, in aiter_bytes
        async for chunk in self.aiter_raw():
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 1009, in aiter_raw
        async for part in self._raw_stream:
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/connection_pool.py", line 49, in __aiter__
        async for chunk in self.stream:
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/base.py", line 57, in __aiter__
        async for chunk in self.aiterator:
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", line 360, in body_iter
        event = await self.connection.wait_for_event(self.stream_id, timeout)
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", line 197, in wait_for_event
        await self.receive_events(timeout)
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", line 204, in receive_events
        data = await self.socket.read(self.READ_NUM_BYTES, timeout)
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_backends/asyncio.py", line 134, in read
        return await asyncio.wait_for(
      File "/usr/lib/python3.8/asyncio/tasks.py", line 470, in wait_for
        fut = ensure_future(fut, loop=loop)
      File "/usr/lib/python3.8/asyncio/tasks.py", line 661, in ensure_future
        task = loop.create_task(coro_or_future)
      File "/usr/lib/python3.8/asyncio/base_events.py", line 427, in create_task
        task = tasks.Task(coro, loop=self, name=name)
      File "/usr/lib/python3.8/traceback.py", line 211, in extract_stack
        stack = StackSummary.extract(walk_stack(f), limit=limit)
      File "/usr/lib/python3.8/traceback.py", line 362, in extract
        linecache.checkcache(filename)
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/linecache.py", line 74, in checkcache
        stat = os.stat(fullname)
    KeyboardInterrupt
    sys:1: RuntimeWarning: coroutine 'StreamReader.read' was never awaited
    

    I also attached gdb to the client in that state with Python support enabled, and got the following traceback when I typed py-bt:

    (sandbox3.8) [sandbox3.8]$ sudo gdb python3.8 -p 7310
    [sudo] password for myusername:
    GNU gdb (Ubuntu 8.1-0ubuntu3.2) 8.1.0.20180409-git
    Copyright (C) 2018 Free Software Foundation, Inc.
    License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
    This is free software: you are free to change and redistribute it.
    There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
    and "show warranty" for details.
    This GDB was configured as "x86_64-linux-gnu".
    Type "show configuration" for configuration details.
    For bug reporting instructions, please see:
    <http://www.gnu.org/software/gdb/bugs/>.
    Find the GDB manual and other documentation resources online at:
    <http://www.gnu.org/software/gdb/documentation/>.
    For help, type "help".
    Type "apropos word" to search for commands related to "word"...
    Reading symbols from python3.8...Reading symbols from /usr/lib/debug/.build-id/90/a961e0dce2c265da6de7226d5103723038022b.debug...done.
    done.
    Attaching to program: /usr/bin/python3.8, process 7310
    [New LWP 7312]
    [Thread debugging using libthread_db enabled]
    Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
    _PyEval_EvalFrameDefault () at ../Python/ceval.c:2499
    2499    ../Python/ceval.c: No such file or directory.
    (gdb) py-bt
    Traceback (most recent call first):
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/linecache.py", line 16, in getline
        lines = getlines(filename, module_globals)
      File "/usr/lib/python3.8/traceback.py", line 288, in line
        self._line = linecache.getline(self.filename, self.lineno).strip()
      File "/usr/lib/python3.8/traceback.py", line 622, in extract
      File "/usr/lib/python3.8/traceback.py", line 211, in extract_stack
        stack = StackSummary.extract(walk_stack(f), limit=limit)
      File "/usr/lib/python3.8/asyncio/base_events.py", line 427, in create_task
        task = tasks.Task(coro, loop=self, name=name)
      File "/usr/lib/python3.8/asyncio/tasks.py", line 661, in ensure_future
        task = loop.create_task(coro_or_future)
      File "/usr/lib/python3.8/asyncio/tasks.py", line 726, in wait_for
        results (in the order of the original sequence, not necessarily
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_backends/asyncio.py", line 390, in read
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", line 204, in receive_events
        data = await self.socket.read(self.READ_NUM_BYTES, timeout)
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", line 197, in wait_for_event
        await self.receive_events(timeout)
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", line 360, in body_iter
        event = await self.connection.wait_for_event(self.stream_id, timeout)
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/base.py", line 57, in __aiter__
        async for chunk in self.aiterator:
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/connection_pool.py", line 49, in __aiter__
        async for chunk in self.stream:
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 1009, in aiter_raw
        async for part in self._raw_stream:
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 976, in aiter_bytes
        async for chunk in self.aiter_raw():
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 987, in aiter_text
        async for chunk in self.aiter_bytes():
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 993, in aiter_lines
        async for text in self.aiter_text():
      File "async_client.py", line 20, in main
      <built-in method run of Context object at remote 0x7f4aabaf6980>
      File "/usr/lib/python3.8/asyncio/events.py", line 81, in _run
        self._context.run(self._callback, *self._args)
      File "/usr/lib/python3.8/asyncio/base_events.py", line 2348, in _run_once
      File "/usr/lib/python3.8/asyncio/base_events.py", line 1075, in run_forever
        raise
      File "/usr/lib/python3.8/asyncio/base_events.py", line 595, in run_until_complete
        self.run_forever()
      File "/usr/lib/python3.8/asyncio/runners.py", line 299, in run
      File "async_client.py", line 543, in <module>
    (gdb)
    

    which overlaps mostly with the above traceback on killing the client.

    Interestingly, trio here does a more sensible thing in that it just raises an exception which in turn crashes the client:

    Traceback (most recent call last):
      File "async_client.py", line 33, in <module>
        trio.run(main)
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/trio/_core/_run.py", line 1896, in run
        raise runner.main_task_outcome.error
      File "async_client.py", line 20, in main
        async for i in s.aiter_lines():
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 993, in aiter_lines
        async for text in self.aiter_text():
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 987, in aiter_text
        async for chunk in self.aiter_bytes():
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 976, in aiter_bytes
        async for chunk in self.aiter_raw():
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 1009, in aiter_raw
        async for part in self._raw_stream:
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/connection_pool.py", line 49, in __aiter__
        async for chunk in self.stream:
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/base.py", line 57, in __aiter__
        async for chunk in self.aiterator:
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", line 360, in body_iter
        event = await self.connection.wait_for_event(self.stream_id, timeout)
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", line 197, in wait_for_event
        await self.receive_events(timeout)
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", line 204, in receive_events
        data = await self.socket.read(self.READ_NUM_BYTES, timeout)
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_backends/trio.py", line 63, in read
        return await self.stream.receive_some(max_bytes=n)
      File "/usr/lib/python3.8/contextlib.py", line 131, in __exit__
        self.gen.throw(type, value, traceback)
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_exceptions.py", line 12, in map_exceptions
        raise to_exc(exc) from None
    httpcore._exceptions.ReadError
    

    Hope all this helps, I'm happy to do a bit more poking around if there's something specific you want me to look into.

  5. florimondmanca commented on Jul 5, 2020

    @florimondmanca
    Contributor

    That’s a lot of debugging info, thanks.

    I reckon the trio case looks okay to me - the host disconnected while we were reading the response and that’s raising a ReadError, so all good.

    For asyncio and the sync case, I’m not quite sure. My understanding is that on the surface the calls block waiting for data to be received, but under the hood there are system calls that receive empty butes forever, is that accurate? Or are we looping over empty bytes not realizing that means that the host has disconnected?

    The first case seems like a trickier thing to fix at first glance, but if the second case is what’s actually happening, we could just « break and raise ReadError » to match the behavior in the trio case.

    Could you run the script again with HTTPCORE_LOG_LEVEL=trace python ...? If we’re in the second case, you should see an endless stream of Data<0 bytes> displayed in those logs.

  6. florimondmanca commented on Jul 5, 2020

    @florimondmanca
    Contributor

    Another thing to check IMO would be to see what happens in a regular HTTP/1.1 situation, ie do we see this « loop forever » behavior too or do we get an exception right away? Could help narrow down if there’s a problem in our HTTP/2 code specifically or if it’s elsewhere/more general.

  7. nawarnoori commented on Jul 5, 2020

    @nawarnoori
    Author

    Or are we looping over empty bytes not realizing that means that the host has disconnected?

    Yes, that would appear to be the case in the sync client, the SyncSocketStream.read method gets 0 bytes from the socket, then there's a bit of looping around in the stack above until eventually it's back to that read method where the socket returns b'' again (check out my pdb traceback in my original post). To know for sure the socket is closed/the connection is dead, I think reading empty is enough, at least according to this:

    When a recv returns 0 bytes, it means the other side has closed (or is in the process of closing) the connection. You will not receive any more data on this connection. Ever. You may be able to send data successfully; I’ll talk more about this later.

    The above applies for the sync case only (and for blocking sockets I think).

    For the async case, I'm not sure what's going on and why the client is hanging, the socket is definitely gone at this point (i.e. no socket hanging around in CLOSE_WAIT state). I'll investigate further those two cases that you suggest tomorrow.

  8. nawarnoori commented on Jul 6, 2020

    @nawarnoori
    Author

    @florimondmanca Hello again,

    So I went ahead and tried out both the sync and asyncio cases with httpcore diags enabled.

    For the sync case

    $ HTTPCORE_LOG_LEVEL=trace python client.py
    TRACE [2020-07-06 13:58:04] httpcore._sync.connection_pool - get_connection_from_pool=(b'https', b'localhost', 8443)
    TRACE [2020-07-06 13:58:04] httpcore._sync.connection_pool - created connection=<httpcore._sync.connection.SyncHTTPConnection object at 0x7f247dfab910>
    TRACE [2020-07-06 13:58:04] httpcore._sync.connection - open_socket origin=(b'https', b'localhost', 8443) timeout={'connect': None, 'read': 5.0, 'write': None, 'pool': None}
    TRACE [2020-07-06 13:58:04] httpcore._sync.connection - create_connection socket=<httpcore._backends.sync.SyncSocketStream object at 0x7f247dfabb90> http_version='HTTP/2'
    TRACE [2020-07-06 13:58:04] httpcore._sync.connection - connection.request method=b'GET' url=(b'https', b'localhost', 8443, b'/') headers=[(b'host', b'localhost:8443'), (b'user-agent', b'python-httpx/0.13.3'), (b'accept', b'*/*'), (b'accept-encoding', b'gzip, deflate'), (b'connection', b'keep-alive')]
    TRACE [2020-07-06 13:58:04] httpcore._sync.http2 - initiate_connection=<httpcore._sync.http2.SyncHTTP2Connection object at 0x7f247dfabb40>
    TRACE [2020-07-06 13:58:04] httpcore._sync.http2 - send_headers stream_id=1 headers=[(b':method', b'GET'), (b':authority', b'localhost:8443'), (b':scheme', b'https'), (b':path', b'/'), (b'user-agent', b'python-httpx/0.13.3'), (b'accept', b'*/*'), (b'accept-encoding', b'gzip, deflate'), (b'connection', b'keep-alive')]
    TRACE [2020-07-06 13:58:04] httpcore._sync.http2 - receive_event stream_id=0 event=<RemoteSettingsChanged changed_settings:{}>
    TRACE [2020-07-06 13:58:04] httpcore._sync.http2 - receive_event stream_id=0 event=<SettingsAcknowledged changed_settings:{}>
    TRACE [2020-07-06 13:58:04] httpcore._sync.http2 - receive_event stream_id=1 event=<ResponseReceived stream_id:1, headers:[(b':status', b'200'), (b'content-type', b'text/event-stream'), (b'date', b'Mon, 06 Jul 2020 12:58:04 GMT')]>
    TRACE [2020-07-06 13:58:04] httpcore._sync.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:19, data:302e373733383235353932343537393339360a>
    0.7738255924579396
    
    TRACE [2020-07-06 13:58:05] httpcore._sync.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:19, data:302e383930303136323532343835333536310a>
    0.8900162524853561
    
    TRACE [2020-07-06 13:58:05] httpcore._sync.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:19, data:302e353232313631303831363332303039350a>
    0.5221610816320095
    
    TRACE [2020-07-06 13:58:06] httpcore._sync.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:19, data:302e393733353139303431383038363534310a>
    0.9735190418086541
    
    TRACE [2020-07-06 13:58:06] httpcore._sync.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:20, data:302e34363030373838333938323939303531360a>
    0.46007883982990516
    
    TRACE [2020-07-06 13:58:07] httpcore._sync.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:19, data:302e363138313930363534393030353132310a>
    0.6181906549005121
    
    TRACE [2020-07-06 13:58:07] httpcore._sync.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:20, data:302e32313736313539343533353930333136320a>
    0.21761594535903162
    
    TRACE [2020-07-06 13:58:08] httpcore._sync.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:20, data:302e31313633343432393734313536363532310a>
    0.11634429741566521
    
    TRACE [2020-07-06 13:58:08] httpcore._sync.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:21, data:302e303033393630353130343830333636333836>
    0.003960510480366386
    
    TRACE [2020-07-06 13:58:09] httpcore._sync.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:19, data:302e383830393131353837343033343232310a>
    0.8809115874034221
    
    TRACE [2020-07-06 13:58:09] httpcore._sync.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:19, data:302e363937363239303133313431313036350a>
    0.6976290131411065
    
    TRACE [2020-07-06 13:58:10] httpcore._sync.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:19, data:302e373831313737383238353234363734370a>
    0.7811778285246747
    
    TRACE [2020-07-06 13:58:10] httpcore._sync.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:19, data:302e373233313635303131313735323434310a>
    0.7231650111752441
    
    TRACE [2020-07-06 13:58:11] httpcore._sync.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:19, data:302e313131393335383939383232303830330a>
    0.1119358998220803
    
    
    < THIS IS WHERE I KILLED THE SERVER - no diagnostics at all>
    
    
    
    
    
    
    <THIS IS WHERE I KILLED THE CLIENT (NOTE THE ^C AT THE START OF THE NEXT LINE>
    ^CTRACE [2020-07-06 14:03:46] httpcore._sync.http2 - close_stream stream_id=1
    TRACE [2020-07-06 14:03:46] httpcore._sync.http2 - close_connection=<httpcore._sync.http2.SyncHTTP2Connection object at 0x7f247dfabb40>
    Traceback (most recent call last):
      File "client.py", line 22, in <module>
        main()
      File "client.py", line 17, in main
        for i in s.iter_lines():
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 926, in iter_lines
        for text in self.iter_text():
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 920, in iter_text
        for chunk in self.iter_bytes():
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 909, in iter_bytes
        for chunk in self.iter_raw():
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 942, in iter_raw
        for part in self._raw_stream:
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_sync/connection_pool.py", line 49, in __iter__
        for chunk in self.stream:
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_sync/base.py", line 57, in __iter__
        for chunk in self.iterator:
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_sync/http2.py", line 360, in body_iter
        event = self.connection.wait_for_event(self.stream_id, timeout)
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_sync/http2.py", line 197, in wait_for_event
        self.receive_events(timeout)
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_sync/http2.py", line 217, in receive_events
        self.socket.write(data_to_send, timeout)
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_backends/sync.py", line 68, in write
        with self.write_lock:
    KeyboardInterrupt
    

    and likewise for the async client:

    $ HTTPCORE_LOG_LEVEL=trace python async_client.py asyncio
    TRACE [2020-07-06 14:06:52] httpcore._async.connection_pool - get_connection_from_pool=(b'https', b'localhost', 8443)
    TRACE [2020-07-06 14:06:52] httpcore._async.connection_pool - created connection=<httpcore._async.connection.AsyncHTTPConnection object at 0x7fbe818571e0>
    TRACE [2020-07-06 14:06:52] httpcore._async.connection - open_socket origin=(b'https', b'localhost', 8443) timeout={'connect': None, 'read': 5.0, 'write': None, 'pool': None}
    TRACE [2020-07-06 14:06:52] httpcore._async.connection - create_connection socket=<httpcore._backends.asyncio.SocketStream object at 0x7fbe8186b730> http_version='HTTP/2'
    TRACE [2020-07-06 14:06:52] httpcore._async.connection - connection.request method=b'GET' url=(b'https', b'localhost', 8443, b'/') headers=[(b'host', b'localhost:8443'), (b'user-agent', b'python-httpx/0.13.3'), (b'accept', b'*/*'), (b'accept-encoding', b'gzip, deflate'), (b'connection', b'keep-alive')]
    TRACE [2020-07-06 14:06:52] httpcore._async.http2 - initiate_connection=<httpcore._async.http2.AsyncHTTP2Connection object at 0x7fbe81857550>
    TRACE [2020-07-06 14:06:52] httpcore._async.http2 - send_headers stream_id=1 headers=[(b':method', b'GET'), (b':authority', b'localhost:8443'), (b':scheme', b'https'), (b':path', b'/'), (b'user-agent', b'python-httpx/0.13.3'), (b'accept', b'*/*'), (b'accept-encoding', b'gzip, deflate'), (b'connection', b'keep-alive')]
    TRACE [2020-07-06 14:06:52] httpcore._async.http2 - receive_event stream_id=0 event=<RemoteSettingsChanged changed_settings:{}>
    TRACE [2020-07-06 14:06:52] httpcore._async.http2 - receive_event stream_id=0 event=<SettingsAcknowledged changed_settings:{}>
    TRACE [2020-07-06 14:06:52] httpcore._async.http2 - receive_event stream_id=1 event=<ResponseReceived stream_id:1, headers:[(b':status', b'200'), (b'content-type', b'text/event-stream'), (b'date', b'Mon, 06 Jul 2020 13:06:52 GMT')]>
    TRACE [2020-07-06 14:06:52] httpcore._async.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:19, data:302e353933343530333535303639343230310a>
    0.5934503550694201
    
    TRACE [2020-07-06 14:06:53] httpcore._async.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:20, data:302e32383936323834343334313335333936360a>
    0.28962844341353966
    
    TRACE [2020-07-06 14:06:53] httpcore._async.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:20, data:302e33313030343038393938323838303533340a>
    0.31004089982880534
    
    TRACE [2020-07-06 14:06:54] httpcore._async.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:19, data:302e373235303139303636303732363735390a>
    0.7250190660726759
    
    TRACE [2020-07-06 14:06:54] httpcore._async.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:20, data:302e30343135363133363334353335373338360a>
    0.04156136345357386
    
    TRACE [2020-07-06 14:06:55] httpcore._async.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:19, data:302e353831343231383730363832343731320a>
    0.5814218706824712
    
    TRACE [2020-07-06 14:06:55] httpcore._async.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:19, data:302e393335383236333534363139373038350a>
    0.9358263546197085
    
    TRACE [2020-07-06 14:06:56] httpcore._async.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:20, data:302e32333035343334303439363035353433370a>
    0.23054340496055437
    
    TRACE [2020-07-06 14:06:56] httpcore._async.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:19, data:302e363734393137313537383837363534330a>
    0.6749171578876543
    
    TRACE [2020-07-06 14:06:57] httpcore._async.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:19, data:302e323836363136313930363231303630370a>
    0.2866161906210607
    
    TRACE [2020-07-06 14:06:57] httpcore._async.http2 - receive_event stream_id=1 event=<DataReceived stream_id:1, flow_controlled_length:19, data:302e393332393834313138393332393332320a>
    0.9329841189329322
    
    
    < THIS IS WHERE I KILLED THE SERVER - no diagnostics at all>
    
    
    
    
    
    
    <THIS IS WHERE I KILLED THE CLIENT (NOTE THE ^C AT THE START OF THE NEXT LINE>
    ^CTRACE [2020-07-06 14:07:14] httpcore._async.http2 - close_stream stream_id=1
    TRACE [2020-07-06 14:07:14] httpcore._async.http2 - close_connection=<httpcore._async.http2.AsyncHTTP2Connection object at 0x7fbe81857550>
    Traceback (most recent call last):
      File "async_client.py", line 31, in <module>
        asyncio.run(main())
      File "/home/myusername/Applications/python3.8.3/lib/python3.8/asyncio/runners.py", line 43, in run
        return loop.run_until_complete(main)
      File "/home/myusername/Applications/python3.8.3/lib/python3.8/asyncio/base_events.py", line 603, in run_until_complete
        self.run_forever()
      File "/home/myusername/Applications/python3.8.3/lib/python3.8/asyncio/base_events.py", line 570, in run_forever
        self._run_once()
      File "/home/myusername/Applications/python3.8.3/lib/python3.8/asyncio/base_events.py", line 1859, in _run_once
        handle._run()
      File "/home/myusername/Applications/python3.8.3/lib/python3.8/asyncio/events.py", line 81, in _run
        self._context.run(self._callback, *self._args)
      File "async_client.py", line 20, in main
        async for i in s.aiter_lines():
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 990, in aiter_lines
        async for text in self.aiter_text():
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 984, in aiter_text
        async for chunk in self.aiter_bytes():
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 973, in aiter_bytes
        async for chunk in self.aiter_raw():
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpx/_models.py", line 1006, in aiter_raw
        async for part in self._raw_stream:
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/connection_pool.py", line 49, in __aiter__
        async for chunk in self.stream:
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/base.py", line 57, in __aiter__
        async for chunk in self.aiterator:
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", line 360, in body_iter
        event = await self.connection.wait_for_event(self.stream_id, timeout)
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", line 197, in wait_for_event
        await self.receive_events(timeout)
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_async/http2.py", line 204, in receive_events
        data = await self.socket.read(self.READ_NUM_BYTES, timeout)
      File "/home/myusername/.virtualenvs/sandbox3.8/lib/python3.8/site-packages/httpcore/_backends/asyncio.py", line 134, in read
        return await asyncio.wait_for(
    KeyboardInterrupt
    

    Unfortunately, no httpcore diags were emitted at the point of terminating the server in both cases.

    Re the http/1.1 case, just want to double check what use case you have in mind. I don't think http/1.1 supports any kind of streams, you have to upgrade to websockets for that, but then that means using a websockets client to the talk to the server, rather than httpx. Or perhaps something like a chunked transfer with keep-alive?

  9. florimondmanca commented on Jul 7, 2020

    @florimondmanca
    Contributor

    Hey,

    I looked into this myself, trying to reproduce this. Turns out, we don't need HTTP/2 nor HTTPS to reproduce it: it's also visible for plain HTTP/1.1 requests, although with a slightly different behavior.

    Generate SSL certs:

    trustme-cli

    HTTP/1.1 server:

    const http = require("http");
    
    const server = http.createServer(function (req, res) {
      res.writeHead(200);
      const handle = setInterval(() => res.write(`${Math.random()}\n`), 500);
      res.on("aborted", () => clearInterval(handle));
    });
    
    server.listen(8000);

    HTTP/2 server:

    const http2 = require("http2");
    const fs = require("fs");
    const path = require("path");
    
    const server = http2.createSecureServer({
      key: fs.readFileSync(path.resolve(__dirname, "server.key")),
      cert: fs.readFileSync(path.resolve(__dirname, "server.pem")),
    });
    
    server.on("stream", (stream, headers, flags) => {
      stream.respond({
        ":status": 200,
        "content-type": "text/event-stream",
      });
      const handle = setInterval(() => stream.write(`${Math.random()}\n`), 500);
      stream.on("aborted", () => clearInterval(handle));
    });
    
    server.listen(8000);

    Client (usage: python client.py sync, python client.py asyncio http2):

    from pathlib import Path
    import httpcore
    import ssl
    import asyncio
    import trio
    
    
    async def async_main(**kwargs) -> None:
        scheme = kwargs.pop("scheme")
        async with httpcore.AsyncConnectionPool(**kwargs) as http:
            url = (scheme, b"localhost", 8000, b"/")
            headers = [(b"host", b"localhost")]
            _, _, _, _, stream = await http.request(b"GET", url, headers)
            try:
                async for line in stream:
                    print(line)
            finally:
                await stream.aclose()
    
    
    def sync_main(**kwargs) -> None:
        scheme = kwargs.pop("scheme")
        with httpcore.SyncConnectionPool(**kwargs) as http:
            url = (scheme, b"localhost", 8000, b"/")
            headers = [(b"host", b"localhost")]
            _, _, _, _, stream = http.request(b"GET", url, headers)
            try:
                for line in stream:
                    print(line)
            finally:
                stream.close()
    
    
    if __name__ == "__main__":
        import sys
    
        name = sys.argv[1]
        if len(sys.argv) == 3:
            ssl_context = ssl.create_default_context()
            ssl_context.load_verify_locations(str(Path(__file__).parent / "client.pem"))
            kwargs = {"scheme": b"https", "http2": True, "ssl_context": ssl_context}
        else:
            kwargs = {"scheme": b"http"}
    
        main = {
            "sync": lambda: sync_main(**kwargs),
            "asyncio": lambda: asyncio.run(async_main(**kwargs)),
            "trio": lambda: trio.run(lambda: async_main(**kwargs)),
        }[name]
    
        main()

    Here's what I've seen:

    HTTP/1.1:

    Environment Behavior
    sync, HTTP/1.1 ProtocolError
    asyncio, HTTP/1.1 ProtocolError
    trio, HTTP/1.1 ProtocolError
    sync, HTTP/2 Loop forever on b""
    asyncio, HTTP/2 Loop forever on b""
    trio, HTTP/2 ReadError (from BrokenResourceError)

    The ProtocolError we get in the HTTP/1.1 case is because h11 is fed with a b"" and treats that as "peer closed the connection without having sent close frames", which is indeed a protocol violation:

    httpcore._exceptions.ProtocolError: peer closed connection without sending complete message body (incomplete chunked read)

    So if we catch that b"" and raise an exception that gets turned into a ReadError like in the trio / HTTP/2 case, we should be all set.

  10. nawarnoori commented on Jul 7, 2020

    @nawarnoori
    Author

    Excellent, thank you so much for putting a fix together, yeah, raising a ReadError in all cases is most accurate I believe, since it communicates explicitly that reading is no longer possible (as theoretically writing still is).

  11. florimondmanca commented on Jul 7, 2020

    @florimondmanca
    Contributor

    I’m not sure what the behavior would be if we were sending data instead of receiving. I think that case is already handled because I assume all 3 cases will fail loudly with an exception.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workinghttp/2

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions