Repository: encode/httpx

Issue:
h11._util.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE
I intermittently got this error when load testing uvicorn endpoint.

This error comes from a proxy endpoint where I am also using `encode/http3` to perform HTTP client calls.

```
  File "/project/venv/lib/python3.7/site-packages/http3/client.py", line 365, in post
    timeout=timeout,
  File "/project/venv/lib/python3.7/site-packages/http3/client.py", line 497, in request
    timeout=timeout,
  File "/project/venv/lib/python3.7/site-packages/http3/client.py", line 112, in send
    allow_redirects=allow_redirects,
  File "/project/venv/lib/python3.7/site-packages/http3/client.py", line 145, in send_handling_redirects
    request, verify=verify, cert=cert, timeout=timeout
  File "/project/venv/lib/python3.7/site-packages/http3/dispatch/connection_pool.py", line 121, in send
    raise exc
  File "/project/venv/lib/python3.7/site-packages/http3/dispatch/connection_pool.py", line 116, in send
    request, verify=verify, cert=cert, timeout=timeout
  File "/project/venv/lib/python3.7/site-packages/http3/dispatch/connection.py", line 59, in send
    response = await self.h11_connection.send(request, timeout=timeout)
  File "/project/venv/lib/python3.7/site-packages/http3/dispatch/http11.py", line 65, in send
    event = await self._receive_event(timeout)
  File "/project/venv/lib/python3.7/site-packages/http3/dispatch/http11.py", line 109, in _receive_event
    event = self.h11_state.next_event()
  File "/project/venv/lib/python3.7/site-packages/h11/_connection.py", line 439, in next_event
    exc._reraise_as_remote_protocol_error()
  File "/project/venv/lib/python3.7/site-packages/h11/_util.py", line 72, in _reraise_as_remote_protocol_error
    raise self
  File "/project/venv/lib/python3.7/site-packages/h11/_connection.py", line 422, in next_event
    self._process_event(self.their_role, event)
  File "/project/venv/lib/python3.7/site-packages/h11/_connection.py", line 238, in _process_event
    self._cstate.process_event(role, type(event), server_switch_event)
  File "/project/venv/lib/python3.7/site-packages/h11/_state.py", line 238, in process_event
    self._fire_event_triggered_transitions(role, event_type)
  File "/project/venv/lib/python3.7/site-packages/h11/_state.py", line 253, in _fire_event_triggered_transitions
    .format(event_type.__name__, role, self.states[role]))
h11._util.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE
```
h11._util.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE
I intermittently got this error when load testing uvicorn endpoint.

This error comes from a proxy endpoint where I am also using `encode/http3` to perform HTTP client calls.

```
  File "/project/venv/lib/python3.7/site-packages/http3/client.py", line 365, in post
    timeout=timeout,
  File "/project/venv/lib/python3.7/site-packages/http3/client.py", line 497, in request
    timeout=timeout,
  File "/project/venv/lib/python3.7/site-packages/http3/client.py", line 112, in send
    allow_redirects=allow_redirects,
  File "/project/venv/lib/python3.7/site-packages/http3/client.py", line 145, in send_handling_redirects
    request, verify=verify, cert=cert, timeout=timeout
  File "/project/venv/lib/python3.7/site-packages/http3/dispatch/connection_pool.py", line 121, in send
    raise exc
  File "/project/venv/lib/python3.7/site-packages/http3/dispatch/connection_pool.py", line 116, in send
    request, verify=verify, cert=cert, timeout=timeout
  File "/project/venv/lib/python3.7/site-packages/http3/dispatch/connection.py", line 59, in send
    response = await self.h11_connection.send(request, timeout=timeout)
  File "/project/venv/lib/python3.7/site-packages/http3/dispatch/http11.py", line 65, in send
    event = await self._receive_event(timeout)
  File "/project/venv/lib/python3.7/site-packages/http3/dispatch/http11.py", line 109, in _receive_event
    event = self.h11_state.next_event()
  File "/project/venv/lib/python3.7/site-packages/h11/_connection.py", line 439, in next_event
    exc._reraise_as_remote_protocol_error()
  File "/project/venv/lib/python3.7/site-packages/h11/_util.py", line 72, in _reraise_as_remote_protocol_error
    raise self
  File "/project/venv/lib/python3.7/site-packages/h11/_connection.py", line 422, in next_event
    self._process_event(self.their_role, event)
  File "/project/venv/lib/python3.7/site-packages/h11/_connection.py", line 238, in _process_event
    self._cstate.process_event(role, type(event), server_switch_event)
  File "/project/venv/lib/python3.7/site-packages/h11/_state.py", line 238, in process_event
    self._fire_event_triggered_transitions(role, event_type)
  File "/project/venv/lib/python3.7/site-packages/h11/_state.py", line 253, in _fire_event_triggered_transitions
    .format(event_type.__name__, role, self.states[role]))
h11._util.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE
```


Discussion:
I think this is caused by the client code in `http3`.
Yup - the traceback is from `http3` there - have moved this issue across.
This problem still exists in the lastest version 0.6.7
```python
ERROR: Exception in ASGI application
Traceback (most recent call last):
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 370, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\starlette\applications.py", line 133, in __call__
    await self.error_middleware(scope, receive, send)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\starlette\middleware\errors.py", line 122, in __call__
    raise exc from None
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\starlette\middleware\errors.py", line 100, in __call__
    await self.app(scope, receive, _send)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\starlette\middleware\base.py", line 25, in __call__
    response = await self.dispatch_func(request, self.call_next)
  File "subapp.py", line 56, in sso_middleware
    'code': auth_code
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\client.py", line 406, in post
    timeout=timeout,
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\client.py", line 548, in request
    timeout=timeout,
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\client.py", line 145, in send
    allow_redirects=allow_redirects,
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\client.py", line 178, in send_handling_redirects
    request, verify=verify, cert=cert, timeout=timeout
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\connection_pool.py", line 130, in send
    raise exc
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\connection_pool.py", line 121, in send
    request, verify=verify, cert=cert, timeout=timeout
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\connection.py", line 59, in send
    response = await self.h11_connection.send(request, timeout=timeout)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\http11.py", line 58, in send
    http_version, status_code, headers = await self._receive_response(timeout)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\http11.py", line 130, in _receive_response
    event = await self._receive_event(timeout)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\http11.py", line 161, in _receive_event
    event = self.h11_state.next_event()
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_connection.py", line 439, in next_event
    exc._reraise_as_remote_protocol_error()
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_util.py", line 72, in _reraise_as_remote_protocol_error
    raise self
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_connection.py", line 422, in next_event
    self._process_event(self.their_role, event)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_connection.py", line 238, in _process_event
    self._cstate.process_event(role, type(event), server_switch_event)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_state.py", line 238, in process_event
    self._fire_event_triggered_transitions(role, event_type)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_state.py", line 253, in _fire_event_triggered_transitions
    .format(event_type.__name__, role, self.states[role]))
h11._util.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE
```
@lch277 Could you provide a simple reproducible example so that we can replicate the issue?
There are two web servers.
`testapp.py`
```python
from fastapi import FastAPI
from starlette.requests import Request
from starlette.responses import JSONResponse
import http3

app = FastAPI()
http_client = http3.AsyncClient()

@app.middleware('http')
async def sso_middleware(request: Request, call_next):
    r = await http_client.post('http://127.0.0.1:8001')
    if r.status_code != 200:
        return JSONResponse({'ok': 0, 'data': {'status_code': r.status_code}})
    ret = r.json()
    await r.close()
    print(ret)
    response = await call_next(request)
    return response

@app.get('/')
def index(request: Request):
    return {"ok": 1, "data": "welcome to test app!"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)
    pass
```
`testapp1.py`
```python
from fastapi import FastAPI
from starlette.requests import Request

app = FastAPI()

@app.get('/')
@app.post('/')
def index(request: Request):
    return {"ok": 1, "data": "welcome to test app 11111111111!"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8001)
    pass
```
The reproduce steps:
1. run the two servers
2. open the `http://127.0.0.1:8000/` in browser
3. refresh the page. the response is 200 and ok. 
4. then stop refreshing, do some other works for seconds
5. switch back and refresh again, the first refresh result is 500 most of the time

This happens, not every time. 
The logs again:
```python
INFO: ('127.0.0.1', 14501) - "GET / HTTP/1.1" 200
{'ok': 1, 'data': 'welcome to test app 11111111111!'}
INFO: ('127.0.0.1', 14501) - "GET / HTTP/1.1" 200
INFO: ('127.0.0.1', 14553) - "GET / HTTP/1.1" 500
ERROR: Exception in ASGI application
Traceback (most recent call last):
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 370, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\starlette\applications.py", line 133, in __call__
    await self.error_middleware(scope, receive, send)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\starlette\middleware\errors.py", line 122, in __call__
    raise exc from None
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\starlette\middleware\errors.py", line 100, in __call__
    await self.app(scope, receive, _send)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\starlette\middleware\base.py", line 25, in __call__
    response = await self.dispatch_func(request, self.call_next)
  File "testapp.py", line 13, in sso_middleware
    r = await http_client.post('http://127.0.0.1:8001')
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\client.py", line 406, in post
    timeout=timeout,
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\client.py", line 548, in request
    timeout=timeout,
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\client.py", line 145, in send
    allow_redirects=allow_redirects,
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\client.py", line 178, in send_handling_redirects
    request, verify=verify, cert=cert, timeout=timeout
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\connection_pool.py", line 130, in send
    raise exc
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\connection_pool.py", line 121, in send
    request, verify=verify, cert=cert, timeout=timeout
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\connection.py", line 59, in send
    response = await self.h11_connection.send(request, timeout=timeout)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\http11.py", line 58, in send
    http_version, status_code, headers = await self._receive_response(timeout)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\http11.py", line 130, in _receive_response
    event = await self._receive_event(timeout)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\http11.py", line 161, in _receive_event
    event = self.h11_state.next_event()
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_connection.py", line 439, in next_event
    exc._reraise_as_remote_protocol_error()
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_util.py", line 72, in _reraise_as_remote_protocol_error
    raise self
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_connection.py", line 422, in next_event
    self._process_event(self.their_role, event)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_connection.py", line 238, in _process_event
    self._cstate.process_event(role, type(event), server_switch_event)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_state.py", line 238, in process_event
    self._fire_event_triggered_transitions(role, event_type)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_state.py", line 253, in _fire_event_triggered_transitions
    .format(event_type.__name__, role, self.states[role]))
h11._util.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE
{'ok': 1, 'data': 'welcome to test app 11111111111!'}
INFO: ('127.0.0.1', 14570) - "GET / HTTP/1.1" 200
```

Sorry for my poor English.
There's a really useful thing for a contirbutor to help progress here.

* Can someone else reproduce this?
* What's the simplest possible case you can reduce it down too? (Eg. Can you remove FastAPI from the equation, and replicate against either a plain WSGI or ASGI service)
I can reproduce it. I've narrowed it down to this "caller" script:

```python
import asyncio
import sys

import httpx

http_client = httpx.AsyncClient()


async def request(port):
    print("Performing request")
    resp = await http_client.get(f"http://localhost:{port}")
    if resp.status_code != 200:
        raise Exception("Unexpected non-200 response")
    print("Got response", resp.content, "\n")
    await resp.close()


async def main(port, timeout=5):
    await request(port)
    await asyncio.sleep(timeout)
    await request(port)


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Please specify a port number to connect to")
    else:
        port = sys.argv[1]
        asyncio.run(main(port))
```

It is not reproducible against a simple Flask WSGI server:

```python
from flask import Flask

app = Flask(__name__)


@app.route("/", methods=["GET", "POST"])
def index():
    return {"ok": 1, "data": "welcome to test app 11111111111!"}


if __name__ == "__main__":
    app.run(port=5000)
```

The script outputs:

```
Performing request
Got response b'{"data":"welcome to test app 11111111111!","ok":1}\n'

Performing request
Got response b'{"data":"welcome to test app 11111111111!","ok":1}\n
```

But it fails with ASGI servers, interestingly it fails for both FastAPI+Uvicorn and Quart+Hypercorn:

```python
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
@app.post("/")
def index(request):
    return {"ok": 1, "data": "welcome to test app 11111111111!"}


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8001)
```

The Quart code:

```python
from quart import Quart

app = Quart(__name__)


@app.route("/", methods=["GET", "POST"])
async def hello():
    return "Hello, World!"


if __name__ == "__main__":
    app.run(port=5001)
```

For both implementations the script's output is:

```
Performing request
Got response b'Hello, World!'

Performing request
Traceback (most recent call last):
  File "/Users/yeray/.pyenv/versions/httpx/lib/python3.7/site-packages/h11/_state.py", line 249, in _fire_event_triggered_transitions
    new_state = EVENT_TRIGGERED_TRANSITIONS[role][state][event_type]
KeyError: <class 'h11._events.ConnectionClosed'>

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "client_script.py", line 29, in <module>
    asyncio.run(main(port))
  File "/Users/yeray/.pyenv/versions/3.7.3/lib/python3.7/asyncio/runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "/Users/yeray/.pyenv/versions/3.7.3/lib/python3.7/asyncio/base_events.py", line 584, in run_until_complete
    return future.result()
  File "client_script.py", line 21, in main
    await request(port)
  File "client_script.py", line 11, in request
    resp = await http_client.get(f"http://localhost:{port}")
  File "/Users/yeray/code/personal/_forks/httpx/httpx/client.py", line 316, in get
    timeout=timeout,
  File "/Users/yeray/code/personal/_forks/httpx/httpx/client.py", line 548, in request
    timeout=timeout,
  File "/Users/yeray/code/personal/_forks/httpx/httpx/client.py", line 144, in send
    allow_redirects=allow_redirects,
  File "/Users/yeray/code/personal/_forks/httpx/httpx/client.py", line 177, in send_handling_redirects
    request, verify=verify, cert=cert, timeout=timeout
  File "/Users/yeray/code/personal/_forks/httpx/httpx/dispatch/connection_pool.py", line 128, in send
    raise exc
  File "/Users/yeray/code/personal/_forks/httpx/httpx/dispatch/connection_pool.py", line 119, in send
    request, verify=verify, cert=cert, timeout=timeout
  File "/Users/yeray/code/personal/_forks/httpx/httpx/dispatch/connection.py", line 54, in send
    response = await self.h11_connection.send(request, timeout=timeout)
  File "/Users/yeray/code/personal/_forks/httpx/httpx/dispatch/http11.py", line 58, in send
    http_version, status_code, headers = await self._receive_response(timeout)
  File "/Users/yeray/code/personal/_forks/httpx/httpx/dispatch/http11.py", line 130, in _receive_response
    event = await self._receive_event(timeout)
  File "/Users/yeray/code/personal/_forks/httpx/httpx/dispatch/http11.py", line 161, in _receive_event
    event = self.h11_state.next_event()
  File "/Users/yeray/.pyenv/versions/httpx/lib/python3.7/site-packages/h11/_connection.py", line 439, in next_event
    exc._reraise_as_remote_protocol_error()
  File "/Users/yeray/.pyenv/versions/httpx/lib/python3.7/site-packages/h11/_util.py", line 72, in _reraise_as_remote_protocol_error
    raise self
  File "/Users/yeray/.pyenv/versions/httpx/lib/python3.7/site-packages/h11/_connection.py", line 422, in next_event
    self._process_event(self.their_role, event)
  File "/Users/yeray/.pyenv/versions/httpx/lib/python3.7/site-packages/h11/_connection.py", line 238, in _process_event
    self._cstate.process_event(role, type(event), server_switch_event)
  File "/Users/yeray/.pyenv/versions/httpx/lib/python3.7/site-packages/h11/_state.py", line 238, in process_event
    self._fire_event_triggered_transitions(role, event_type)
  File "/Users/yeray/.pyenv/versions/httpx/lib/python3.7/site-packages/h11/_state.py", line 253, in _fire_event_triggered_transitions
    .format(event_type.__name__, role, self.states[role]))
h11._util.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE
```
Relevant snippet from the [h11 docs](https://h11.readthedocs.io/en/latest/api.html#h11.Connection.next_event):

> `Connection.next_event` Raises: RemoteProtocolError – The peer has misbehaved. You should close the connection (possibly after sending some kind of 4xx response)



Okay, so it's related to an issue with keep-alive connections on HTTP/1.1.

I can replicate fine with `uwsgi`, but not `gunicorn`. (Difference being that gunicorn doesn't *itself* support keep-alive, but instead relies on being proxied behing nginx in order to support persistent connections)
I believe you can turn on gunicorn keepalive setting with —keepalive N
Did some more debugging, I'm a bit out of my depth on this lower level details so bear with me 😅 

During keep-alive situations the asyncio `StreamReader` and `StreamWriter` objects part of the `HTTP11Connection` are reused. I noticed once the reader reads the first response its [`at_eof`](https://docs.python.org/3/library/asyncio-stream.html#asyncio.StreamReader.at_eof) is returning `True`. When trying to read the second response it returns an empty bytes which is interpreted by h11 as the server has closed the connection instead of sending the response.

The [StreamReader.read docs](https://docs.python.org/3/library/asyncio-stream.html#asyncio.StreamReader.read) mention:

> If EOF was received and the internal buffer is empty, return an empty bytes object.

I would've thought that the internal buffer of the reader would not be empty and would hold the new response data but it returns the empty bytes.

I find unlikely that both FastAPI and Quart are misbehaving in the exact same way and not sending response data, so is it possible we need to "refresh" the reader somehow after it reading EOF? I can't see any obvious API to do such a thing, aside from creating a whole new connection which would obviously defeat the purpose.

Sorry if it's a silly question, but I can't really find any other reason for this error, which btw happens irregardless of the waiting in the caller scripts, two consecutive keep-alive requests will trigger it.
Or we need to not feed an empty string into the `h11` state machine. Maybe instead if we get an EOF back from `StreamReader` we don't forward it to `h11`?

See this comment: https://github.com/python-hyper/h11/blob/master/h11/_connection.py#L320
I had a look at the asyncio code looks to me like there's no real way of reusing the reader? 😕 

Once the first EOF is read the [protocol's `eof_received` method](https://github.com/python/cpython/blob/v3.7.4/Lib/asyncio/streams.py#L260) [sets the reader's EOF flag](https://github.com/python/cpython/blob/v3.7.4/Lib/asyncio/streams.py#L419-L421) which [prevents further reading from it](https://github.com/python/cpython/blob/v3.7.4/Lib/asyncio/streams.py#L639-L640).

I thought I'd look into how `aiohttp` might be handling this and seems they use custom transport and protocol instances. The latter seems to [handle EOF differently](https://github.com/aio-libs/aiohttp/blob/master/aiohttp/client_proto.py#L111-L113) but I am *really* out of my depth on that code so I might be completely wrong.

#rabbithole
When I run this against the latest Hypercorn using @yeraydiazdiaz example it works, with this output,
```
Performing request
Got response b'/Users/pgjones/quart/quart-benchmark/servers/quart_server.py-fib(12)=144' 

Performing request
Got response b'/Users/pgjones/quart/quart-benchmark/servers/quart_server.py-fib(12)=144' 
```
crucially though I changed the time between the requests to **2 seconds** as the hypercorn keep alive timeout default is **5 seconds**. I think this is why it failed for Hypercorn and Uvicorn as both have a 5 second keep alive timeout. 

This is what Hypercorn receives (`>`) and sends (`<`),
```
> b'GET / HTTP/1.1\r\nhost: localhost:8000\r\nuser-agent: python-httpx/0.6.7\r\naccept: */*\r\naccept-encoding: gzip, deflate\r\nconnection: keep-alive\r\n\r\n'
< b'HTTP/1.1 200 \r\ncontent-type: text/html; charset=utf-8\r\ncontent-length: 13\r\ndate: Wed, 24 Jul 2019 21:22:21 GMT\r\nserver: hypercorn-h11\r\n\r\n'
< b'Hello, World!'
< b''
> b'GET / HTTP/1.1\r\nhost: localhost:8000\r\nuser-agent: python-httpx/0.6.7\r\naccept: */*\r\naccept-encoding: gzip, deflate\r\nconnection: keep-alive\r\n\r\n'
< b'HTTP/1.1 200 \r\ncontent-type: text/html; charset=utf-8\r\ncontent-length: 13\r\ndate: Wed, 24 Jul 2019 21:22:23 GMT\r\nserver: hypercorn-h11\r\n\r\n'
< b'Hello, World!'
< b''
> b''
```

I think this issue is that httpx should convert the error raised by h11 to something like ConnectionClosedByRemote.
Right, so if understand correctly, the issue is handling the server closing keep-alive connections after a timeout.

My initial approach would be to have httpx handle the exception, reestablish the connection and retry, I'll see if I can put together a PR for this.

Thanks for looking into it @pgjones  🌟 
So the thing that's not clear to me on first sight, is why the disconnect isn't being caught at the point we start writing the next response... https://github.com/encode/httpx/blob/master/httpx/dispatch/http11.py#L50

We've got the logic there to handle the disconnect case, and we're writing to the network and calling drain.

That *actually* needs to be the point at which we figure out if we're still connected or not, because if we wait until getting the response, then for non-idempotent requests it's ambiguous to us as the client if we're *actually* okay to resend. (Or if the server did in fact handle the request, but some intermediary has disconnected us before we started seeing the response).

So, questions:

* How does `urllib3` determine if the network connection is still alive, when re-acquring connections from the pool?
* How does `aiohttp` determine if the network connection is still alive, when re-acquring connections from the pool?

/cc @sethmlarson 
urllib3 has a [`is_connection_dropped()`](https://github.com/urllib3/urllib3/blob/master/src/urllib3/util/connection.py#L7) function that doesn't tell the whole story. Have to look at httplib as well.
Okay, so I think the right approach would be for us to have an equivelent `is_connection_dropped` on the `Reader` class in https://github.com/encode/httpx/blob/master/httpx/concurrency.py replacing the existing logic of "suck it and see".

Does that make sense to you too, @yeraydiazdiaz? #143 is a great start - nice test case for the issue that can still be used here.

(Actually I think we'll also want to combine the `Reader` and `Writer` interfaces into a single `SocketConnection`, but that's a different story.)
Closed via #145 
Released to PyPI as 0.6.8
Unfortunately, I get the same exception again in 0.6.8.

I use httpx in an oauth2 authorize flow. When I get authorization code from the auth server, I use httpx client to get an access token. After the access toke expired, the first invocation to obtain a new access token is almost always failed, the second invocation(refresh the page) is ok. The situation is similar to last time.

Some facts:
1. The httpx client is a long-live single instance
2. The invocation interval is long because it depends on the access token expire time(2 hours) and only a few users use it

Logs:
```python
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/client.py", line 687, in get
    timeout=timeout,
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/client.py", line 634, in request
    async_response = concurrency_backend.run(coroutine, *args, **kwargs)
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/concurrency.py", line 243, in run
    return self.loop.run_until_complete(coroutine(*args, **kwargs))
  File "uvloop/loop.pyx", line 1451, in uvloop.loop.Loop.run_until_complete
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/client.py", line 144, in send
    allow_redirects=allow_redirects,
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/client.py", line 177, in send_handling_redirects
    request, verify=verify, cert=cert, timeout=timeout
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/dispatch/connection_pool.py", line 118, in send
    raise exc
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/dispatch/connection_pool.py", line 113, in send
    request, verify=verify, cert=cert, timeout=timeout
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/dispatch/connection.py", line 54, in send
    response = await self.h11_connection.send(request, timeout=timeout)
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/dispatch/http11.py", line 52, in send
    http_version, status_code, headers = await self._receive_response(timeout)
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/dispatch/http11.py", line 124, in _receive_response
    event = await self._receive_event(timeout)
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/dispatch/http11.py", line 155, in _receive_event
    event = self.h11_state.next_event()
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/h11/_connection.py", line 439, in next_event
    exc._reraise_as_remote_protocol_error()
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/h11/_util.py", line 72, in _reraise_as_remote_protocol_error
    raise self
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/h11/_connection.py", line 422, in next_event
    self._process_event(self.their_role, event)
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/h11/_connection.py", line 238, in _process_event
    self._cstate.process_event(role, type(event), server_switch_event)
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/h11/_state.py", line 238, in process_event
    self._fire_event_triggered_transitions(role, event_type)
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/h11/_state.py", line 253, in _fire_event_triggered_transitions
    .format(event_type.__name__, role, self.states[role]))
h11._util.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE
```

Thanks for reporting this issue @lch277, I'm going to reopen this issue since it seems similar.

This would be a great time to have had our debug logging in place so we can exactly track down the sequence of events in your request. Maybe we should speed up the integration of those logs to help solve issues like this.
I also had this error with httpx 0.7.2 while communicating with a FastAPI based application running straight on Uvicorn (no gunicorn).  This specific issue was against a brand new endpoint I just added in, but other similar endpoints have worked fine with requests for quite some time now.  I have not yet had a chance to dig into logs, will do that later today and try to post back something more useful here.

A quick check reveals my FastAPI server appears to have crashed, of course without any sort of decent logging, at the same time this request was made.  I'm tentatively pointing my blaming finger at my server application crashing mid-request.
@iwoloschin Yes but we should be able to handle that sort of issue gracefully from the HTTPX side. You shouldn't be getting an `h11` exception.
@sethmlarson I won't argue about that :).  I'd much prefer only having to think about HTTPX exceptions when using HTTPX!

I've added some logging and kicked off another test.  It's not a fast test and the issue appears to have been intermittent, so I might not have any results for a while and if it's truly intermittent I may never get results (but hey, no errors is good too, right...?).
@iwoloschin Are you aware of `HTTPX_DEBUG=1` for debugging high-level events happening within HTTPX? :)
Nope!  I'll take a look at that.

My re-test failed with a `httpx.exceptions.ReadTimeout` on a different endpoint that should take less than a second.  I'm leaning towards my problem with the h11 exception being my server crashing.  I'll still second a vote for encapsulating exceptions to only throw HTTPX exceptions.
> I'll still second a vote for encapsulating exceptions to only throw HTTPX exceptions.

Agreed, yup.
So, is this still an issue? And if so, what can we do to resolve it?

- Should we introduce a sort of « catch all » wrapper exception? (That doesn’t seem easily feasible to me without introducing cruft.)
- Perhaps we could build up a small reproduction example in which we’d forcibly crash or close the remote server and see what happens, ie whether we get the h11 exception again?
I just ran into this issue again.

In my case, I sort of expect something stupid to happen, I'm deliberately `GET`ing an endpoint on a server that is attempting to reboot, in an attempt to take an action after the reboot is complete & the HTTP service (FastAPI) is back up.  I'm fine to catch some exceptions here (`HTTPError` or `ConnectionRefusedError`), it kind of makes sense to throw an exception if you can't reach the server, but I think catching an `h11` exception here is confusing, especially since I don't normally have to touch `h11`, `httpx` manages all of that for me (and it's lovely when it works!)  Without really being knowledgable about the internals, I'd put in a strong vote for `httpx` raising an `HTTPError`-based exception here.  If someone wants to point me in the right direction I can try to help with that...but I know very little about how `httpx` or `h11` work internally so it wouldn't be fast 😄 .

I haven't dug into it yet, but the timing appears to coincide with when the HTTP Service is stopped.  I might be able to come up with a small, reproducible example but it might take a few days.
Quick addendum, if I put a short sleep in, to try and avoid hitting the endpoint as the HTTP service is being stopped it appears to avoid the `h11` exception.  I then got `OSError: [Errno 113] Host is unreachable`, which...is correct, but also not exactly intuitive.
> Should we introduce a sort of « catch all » wrapper exception? (That doesn’t seem easily feasible to me without introducing cruft.)

This one doesn't *look* like it ought to be a catch-all case. Seems to be an*expected* possible behaviour that we ought to explicitly catch and raise as a ConnectionClosed or whatever?

> Perhaps we could build up a small reproduction example in which we’d forcibly crash or close the remote server and see what happens, ie whether we get the h11 exception again?

Certainly it'd be *really* helpful to be able to reliable reproduce this yup.
Here's a simple reproducible example.  It uses two separate files and requires `fastapi` and `httpx`.

FastAPI Server:
```python
from fastapi import FastAPI

app = FastAPI()

@app.get("/hello")
async def hello():
    return "World"

# To Run:
# gunicorn -k uvicorn.workers.UvicornWorker -b 0.0.0.0:80  test:app
```

Client:
```python
#!/usr/bin/env python3

import httpx


def main():
    try:
        while True:
            r = httpx.get("http://localhost/hello")
            print(r.text)
    except KeyboardInterrupt:
        print("done")


if __name__ == "__main__":
    main()
```

In one terminal run the server (I'm using `gunicorn` with a `uvicorn` worker since that's how I normally run my FastAPI applications).

In a second terminal, launch the test (I just did `./test.py`).  Let it run for a second and then kill the *server* with a `CTRL-C` in the first terminal.  The test client, in the second terminal, should die with the expected `h11._util.RemoteProtocolError` exception.  In a quick test (~5 tries) I got the expected error 4 times and a `ConnectionRefused` error once, which kind of makes sense since it appears to be a race condition of whether or not the server finishes sending the response before it dies.

For what it's worth I'm running on macOS (Mojave) with Homebrew Python3.7 in a virtual environment with the latest `httpx` and `fastapi`.
Thanks @iwoloschin, that's very helpful. I was able to reproduce using the even simpler setup below (plain ASGI app + uvicorn):

```python
# app.py
async def app(scope, receive, send):
    assert scope["type"] == "http"
    await send(
        {
            "type": "http.response.start",
            "status": 200,
            "headers": [[b"content-type", b"text/plain"]],
        }
    )
    await send({"type": "http.response.body", "body": b"Hello, world!"})

# uvicorn app:app
```

I received the `h11` error 3 times out of 5, the 2 other failures were an `OSError`. I'm running the same setup as you (except for Python 3.7 via `pyenv`).

Another piece of info: I get the same result whether I use the top-level API level, a `Client`, or an `AsyncClient`.
Good digging folks!
@florimondmanca I'm also getting this one, but quite reliably with ssl. Works well without ssl.
I'm also using a simple ASGI app + uvicorn + h11 in the AsyncClient.
```
import asyncio
import pytest

from httpx import AsyncClient

from uvicorn.config import Config
from uvicorn.main import Server

global_call_count = 0

class App:
    def __init__(self, scope):
        self.scope = scope
        self.call_count = 0

    async def __call__(self, receive, send):
        global global_call_count
        
        self.call_count += 1
        global_call_count += 1

        await send({"type": "http.response.start", "status": 200, "headers": []})
        await send({"type": "http.response.body", "body": bytes(f"That's the body! - {self.call_count} - {global_call_count}", "utf-8"), "more_body": False})


class CustomServer(Server):
    pass


@pytest.mark.asyncio
async def test_baseline_h11(certfile_and_keyfile):
    certfile, keyfile = certfile_and_keyfile

    client = AsyncClient(http_versions=["HTTP/1.1"])
    config = Config(
        app=App,
        loop="asyncio",
        limit_max_requests=3,
        http='h11',
        # ssl_certfile=certfile,
        # ssl_keyfile=keyfile
    )
    server = CustomServer(config=config)

    # Prepare the coroutine to serve the request
    run_request = server.serve()
    # Prepare the coroutine to make the request
    make_request = client.get("http://127.0.0.1:8000")

    # Reset the global counter
    global global_call_count
    global_call_count = 0

    # Run coroutines
    results = await asyncio.gather(*[
        run_request,
        client.get("http://127.0.0.1:8000"),
        client.get("http://127.0.0.1:8000"),
        client.get("http://127.0.0.1:8000"),
    ])

    assert [x.text for x in results if x] == ["That's the body! - 1 - 2", "That's the body! - 1 - 3", "That's the body! - 1 - 4"]
```
Actually, my test case has a mistake, it tries to communicate over HTTP instead of HTTPS.
And fails in this case

```
13:39:34.627 - httpx.dispatch.connection - connected http_version='HTTP/1.1'
DEBUG:    connected http_version='HTTP/1.1'
13:39:34.627 - httpx.dispatch.http11 - send_headers method='GET' target='/' headers=Headers({'host': '127.0.0.1:8000', 'user-agent': 'python-httpx/0.7.6', 'accept': '*/*', 'accept-encoding': 'gzip, deflate', 'connection': 'keep-alive'})
DEBUG:    send_headers method='GET' target='/' headers=Headers({'host': '127.0.0.1:8000', 'user-agent': 'python-httpx/0.7.6', 'accept': '*/*', 'accept-encoding': 'gzip, deflate', 'connection': 'keep-alive'})
13:39:34.628 - httpx.dispatch.http11 - receive_event event=NEED_DATA
DEBUG:    receive_event event=NEED_DATA

...

    def _fire_event_triggered_transitions(self, role, event_type):
        state = self.states[role]
        try:
>           new_state = EVENT_TRIGGERED_TRANSITIONS[role][state][event_type]
E           KeyError: <class 'h11._events.ConnectionClosed'>

venv/lib/python3.7/site-packages/h11/_state.py:249: KeyError

...

self = <h11._state.ConnectionState object at 0x103f8b450>, role = SERVER, event_type = <class 'h11._events.ConnectionClosed'>

    def _fire_event_triggered_transitions(self, role, event_type):
        state = self.states[role]
        try:
            new_state = EVENT_TRIGGERED_TRANSITIONS[role][state][event_type]
        except KeyError:
            raise LocalProtocolError(
                "can't handle event type {} when role={} and state={}".format(
>                   event_type.__name__, role, self.states[role]
                )
            )
E           h11._util.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE

venv/lib/python3.7/site-packages/h11/_state.py:253: RemoteProtocolError
```
I guess it's just another facet of the problem
just hit this issue (triggered by killing the server during long polling), and this is my stab at pondering  the stack trace I'm seeing. 

- h11 cannot process event given the current state: "  h11._util.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE"
- h11 docs  say " If either side violates the protocol, you'll get a h11.ProtocolError exception."

So we have two choices: 

1. detect and circumvent the connectionclosed event from getting to h11 causing it to blow up
2.  decode the h11.ProtocolError and throw a more appropriate event

The issue with #2 is that h11 doesn't put details in their exception outside of a string, so either we're fixing that in h11 (adding more structured detailed inside the exception), or we're parsing the msg from the exception.

Thoughts?  I'm going to see if there's a way to prevent the ConnectionClose event blowing up h11 (choice number #1).  


So I've done some further analysis, and it seems that h11 doesn't give us an opportunity for a detailed exception.  The documentation was hopeful  (see h11's apidocs for  [Connection.next_event](https://h11.readthedocs.io/en/latest/_modules/h11/_connection.html#Connection.next_event) ) but 
neither their_state or error_status_hint allows us to create anything but a generic error.  I could whip up a pull request based on this and some of the test case listed in earlier comments.  Let me know.



```diff --git a/httpx/dispatch/http11.py b/httpx/dispatch/http11.py
index b1781bf..926560f 100644
--- a/httpx/dispatch/http11.py
+++ b/httpx/dispatch/http11.py
@@ -6,6 +6,7 @@ from ..concurrency.base import BaseSocketStream, ConcurrencyBackend, TimeoutFlag
 from ..config import TimeoutConfig, TimeoutTypes
 from ..models import AsyncRequest, AsyncResponse
 from ..utils import get_logger
+from ..exceptions import ProtocolError, ConnectionClosed
 
 H11Event = typing.Union[
     h11.Request,
@@ -161,7 +162,13 @@ class HTTP11Connection:
         Read a single `h11` event, reading more data from the network if needed.
         """
         while True:
-            event = self.h11_state.next_event()
+            try:
+                event = self.h11_state.next_event()
+            except h11.RemoteProtocolError as e:
+                logger.debug(f"h11.RemoteProtocolError exception their_state={self.h11_state.their_state} error_status_hint={e.error_status_hint}")
+                if self.stream.is_connection_closed():
+                    raise ConnectionClosed(e)
+                raise ProtocolError(e)
 
             if isinstance(event, h11.Data):
                 logger.trace(f"receive_event event=Data(<{len(event.data)} bytes>)")
diff --git a/httpx/exceptions.py b/httpx/exceptions.py
index 81df38b..01f4f87 100644
--- a/httpx/exceptions.py
+++ b/httpx/exceptions.py
@@ -149,6 +149,10 @@ class InvalidURL(HTTPError):
     URL was missing a hostname, or was not one of HTTP/HTTPS.
     """
 
+class ConnectionClosed(HTTPError):
+    """
+    Expected more data from peer, but connection was closed.
+    """
 
 class CookieConflict(HTTPError):
     """


```
h11 could give more detailed exceptions if that's useful (or just clearer error messages), but it's not clear to me what the actual problem you're trying to solve is. You have a server that's closing the connection before it finishes sending the response. Do you want to treat that differently from other kinds of server misbehavior? What information do you actually need here?
> h11 could give more detailed exceptions if that's useful (or just clearer error messages), but it's not clear to me what the actual problem you're trying to solve is. You have a server that's closing the connection before it finishes sending the response. Do you want to treat that differently from other kinds of server misbehavior? What information do you actually need here?

It is common in http client libraries to give specific exceptions for transport layer issues, like readtimeout, connectionrefused.   This dropped connection indicates a different class of issue than a typical protocol violation (and the user may want to take a different tactic in handling it, e.g. switch to a different server within a cluster).

However, from httpx's perspective, I believe it would react the same either way (e.g. it cannot attempt to retry on it's own).  I think the only value in making a distinct exception for this fault case is to make it easier for the httpx user to understand and handle the the failure.    So I think h11's behavior is fine as is.  

I hope my analysis has been helpful, but I think @tomchristie should weigh in now.
@toppk - Your diff in https://github.com/encode/httpx/issues/96#issuecomment-552730324 looks about right to me. We just need to be wrapping this case up properly and exposing it as a `ConnectionClosed` exception.

I *think* we should probably be checking for the event type `ConnectionClosed`, rather than inspecting `if self.stream.is_connection_closed():` to determine if we're raising a protocol error, or a connection closed exception type.
> @toppk - Your diff in [#96 (comment)](https://github.com/encode/httpx/issues/96#issuecomment-552730324) looks about right to me. We just need to be wrapping this case up properly and exposing it as a `ConnectionClosed` exception.
> 
> I _think_ we should probably be checking for the event type `ConnectionClosed`, rather than inspecting `if self.stream.is_connection_closed():` to determine if we're raising a protocol error, or a connection closed exception type.

By design of h11, the ConnectionClosed isn't expected, there is no event type 'ConnectionClosed' exposed back to the caller, only a RemoteProtocolError exception.    Or said another way, h11 only exposes ConnectionClosed event when the protocol expects the connection to be closed.  

\<verbose details>
The current model of h11 isn't to process unexpected events ( review [h11._state](https://github.com/python-hyper/h11/blob/master/h11/_state.py) for the design), but just to raise an exception.  The little exception characterization that exists in occurs in [Connection.next_state](https://github.com/python-hyper/h11/blob/68e32dbb475d5241f077afaa278b6ef248b1f9bd/h11/_connection.py#L429), and simple sets a recommend http error code (in an attribute named error_status_hint, which in our failure mode is left at the generic 400)
\</verbose details>

Although `if self.stream.is_connection_closed()` is just a heuristic, but it seems to be helpful, considering otherwise we'd just be sending up httpx.exceptions.ProtocolError.   

I think if you wanted to avoid the heuristic, without redesigning h11, I think the simplest improvement h11 could make is to add the state of the protocol inside the exception  created at [h11._state.ConnectionState._fire_event_triggered_transitions](https://github.com/python-hyper/h11/blob/68e32dbb475d5241f077afaa278b6ef248b1f9bd/h11/_state.py#L251) rather than just as a string, and bubble that up into the RemoteProtocolError.   Then we could examine RemoteProtocolError for the unhandled ConnectionClosed event, and infer that is the root cause.    

Or we could just parse the string in the exception we get right now :)
Misunderstanding on my part then. Examine the connection. 👍
Also this particular exception can only happen if you've passed b"" to h11
to tell it that the connection was closed, in case tracking that is somehow
easier.

On Tue, Nov 12, 2019, 11:03 Tom Christie <notifications@github.com> wrote:

> Misunderstanding on my part then. Examine the connection. 👍
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/encode/httpx/issues/96?email_source=notifications&email_token=AAEU42HAHPMMB63Z7OYYFQ3QTL4WXA5CNFSM4HZGWBRKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOED3QWCY#issuecomment-553061131>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAEU42EYGW5TNGGPZPHEDOLQTL4WXANCNFSM4HZGWBRA>
> .
>

I think this is caused by the client code in `http3`.
Yup - the traceback is from `http3` there - have moved this issue across.
This problem still exists in the lastest version 0.6.7
```python
ERROR: Exception in ASGI application
Traceback (most recent call last):
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 370, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\starlette\applications.py", line 133, in __call__
    await self.error_middleware(scope, receive, send)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\starlette\middleware\errors.py", line 122, in __call__
    raise exc from None
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\starlette\middleware\errors.py", line 100, in __call__
    await self.app(scope, receive, _send)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\starlette\middleware\base.py", line 25, in __call__
    response = await self.dispatch_func(request, self.call_next)
  File "subapp.py", line 56, in sso_middleware
    'code': auth_code
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\client.py", line 406, in post
    timeout=timeout,
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\client.py", line 548, in request
    timeout=timeout,
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\client.py", line 145, in send
    allow_redirects=allow_redirects,
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\client.py", line 178, in send_handling_redirects
    request, verify=verify, cert=cert, timeout=timeout
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\connection_pool.py", line 130, in send
    raise exc
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\connection_pool.py", line 121, in send
    request, verify=verify, cert=cert, timeout=timeout
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\connection.py", line 59, in send
    response = await self.h11_connection.send(request, timeout=timeout)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\http11.py", line 58, in send
    http_version, status_code, headers = await self._receive_response(timeout)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\http11.py", line 130, in _receive_response
    event = await self._receive_event(timeout)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\http11.py", line 161, in _receive_event
    event = self.h11_state.next_event()
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_connection.py", line 439, in next_event
    exc._reraise_as_remote_protocol_error()
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_util.py", line 72, in _reraise_as_remote_protocol_error
    raise self
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_connection.py", line 422, in next_event
    self._process_event(self.their_role, event)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_connection.py", line 238, in _process_event
    self._cstate.process_event(role, type(event), server_switch_event)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_state.py", line 238, in process_event
    self._fire_event_triggered_transitions(role, event_type)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_state.py", line 253, in _fire_event_triggered_transitions
    .format(event_type.__name__, role, self.states[role]))
h11._util.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE
```
@lch277 Could you provide a simple reproducible example so that we can replicate the issue?
There are two web servers.
`testapp.py`
```python
from fastapi import FastAPI
from starlette.requests import Request
from starlette.responses import JSONResponse
import http3

app = FastAPI()
http_client = http3.AsyncClient()

@app.middleware('http')
async def sso_middleware(request: Request, call_next):
    r = await http_client.post('http://127.0.0.1:8001')
    if r.status_code != 200:
        return JSONResponse({'ok': 0, 'data': {'status_code': r.status_code}})
    ret = r.json()
    await r.close()
    print(ret)
    response = await call_next(request)
    return response

@app.get('/')
def index(request: Request):
    return {"ok": 1, "data": "welcome to test app!"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8000)
    pass
```
`testapp1.py`
```python
from fastapi import FastAPI
from starlette.requests import Request

app = FastAPI()

@app.get('/')
@app.post('/')
def index(request: Request):
    return {"ok": 1, "data": "welcome to test app 11111111111!"}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8001)
    pass
```
The reproduce steps:
1. run the two servers
2. open the `http://127.0.0.1:8000/` in browser
3. refresh the page. the response is 200 and ok. 
4. then stop refreshing, do some other works for seconds
5. switch back and refresh again, the first refresh result is 500 most of the time

This happens, not every time. 
The logs again:
```python
INFO: ('127.0.0.1', 14501) - "GET / HTTP/1.1" 200
{'ok': 1, 'data': 'welcome to test app 11111111111!'}
INFO: ('127.0.0.1', 14501) - "GET / HTTP/1.1" 200
INFO: ('127.0.0.1', 14553) - "GET / HTTP/1.1" 500
ERROR: Exception in ASGI application
Traceback (most recent call last):
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\uvicorn\protocols\http\h11_impl.py", line 370, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\starlette\applications.py", line 133, in __call__
    await self.error_middleware(scope, receive, send)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\starlette\middleware\errors.py", line 122, in __call__
    raise exc from None
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\starlette\middleware\errors.py", line 100, in __call__
    await self.app(scope, receive, _send)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\starlette\middleware\base.py", line 25, in __call__
    response = await self.dispatch_func(request, self.call_next)
  File "testapp.py", line 13, in sso_middleware
    r = await http_client.post('http://127.0.0.1:8001')
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\client.py", line 406, in post
    timeout=timeout,
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\client.py", line 548, in request
    timeout=timeout,
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\client.py", line 145, in send
    allow_redirects=allow_redirects,
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\client.py", line 178, in send_handling_redirects
    request, verify=verify, cert=cert, timeout=timeout
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\connection_pool.py", line 130, in send
    raise exc
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\connection_pool.py", line 121, in send
    request, verify=verify, cert=cert, timeout=timeout
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\connection.py", line 59, in send
    response = await self.h11_connection.send(request, timeout=timeout)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\http11.py", line 58, in send
    http_version, status_code, headers = await self._receive_response(timeout)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\http11.py", line 130, in _receive_response
    event = await self._receive_event(timeout)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\http3\dispatch\http11.py", line 161, in _receive_event
    event = self.h11_state.next_event()
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_connection.py", line 439, in next_event
    exc._reraise_as_remote_protocol_error()
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_util.py", line 72, in _reraise_as_remote_protocol_error
    raise self
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_connection.py", line 422, in next_event
    self._process_event(self.their_role, event)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_connection.py", line 238, in _process_event
    self._cstate.process_event(role, type(event), server_switch_event)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_state.py", line 238, in process_event
    self._fire_event_triggered_transitions(role, event_type)
  File "C:\Users\lch\.virtualenvs\authcenter-5r0y-06W\lib\site-packages\h11\_state.py", line 253, in _fire_event_triggered_transitions
    .format(event_type.__name__, role, self.states[role]))
h11._util.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE
{'ok': 1, 'data': 'welcome to test app 11111111111!'}
INFO: ('127.0.0.1', 14570) - "GET / HTTP/1.1" 200
```

Sorry for my poor English.
There's a really useful thing for a contirbutor to help progress here.

* Can someone else reproduce this?
* What's the simplest possible case you can reduce it down too? (Eg. Can you remove FastAPI from the equation, and replicate against either a plain WSGI or ASGI service)
I can reproduce it. I've narrowed it down to this "caller" script:

```python
import asyncio
import sys

import httpx

http_client = httpx.AsyncClient()


async def request(port):
    print("Performing request")
    resp = await http_client.get(f"http://localhost:{port}")
    if resp.status_code != 200:
        raise Exception("Unexpected non-200 response")
    print("Got response", resp.content, "\n")
    await resp.close()


async def main(port, timeout=5):
    await request(port)
    await asyncio.sleep(timeout)
    await request(port)


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("Please specify a port number to connect to")
    else:
        port = sys.argv[1]
        asyncio.run(main(port))
```

It is not reproducible against a simple Flask WSGI server:

```python
from flask import Flask

app = Flask(__name__)


@app.route("/", methods=["GET", "POST"])
def index():
    return {"ok": 1, "data": "welcome to test app 11111111111!"}


if __name__ == "__main__":
    app.run(port=5000)
```

The script outputs:

```
Performing request
Got response b'{"data":"welcome to test app 11111111111!","ok":1}\n'

Performing request
Got response b'{"data":"welcome to test app 11111111111!","ok":1}\n
```

But it fails with ASGI servers, interestingly it fails for both FastAPI+Uvicorn and Quart+Hypercorn:

```python
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
@app.post("/")
def index(request):
    return {"ok": 1, "data": "welcome to test app 11111111111!"}


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="127.0.0.1", port=8001)
```

The Quart code:

```python
from quart import Quart

app = Quart(__name__)


@app.route("/", methods=["GET", "POST"])
async def hello():
    return "Hello, World!"


if __name__ == "__main__":
    app.run(port=5001)
```

For both implementations the script's output is:

```
Performing request
Got response b'Hello, World!'

Performing request
Traceback (most recent call last):
  File "/Users/yeray/.pyenv/versions/httpx/lib/python3.7/site-packages/h11/_state.py", line 249, in _fire_event_triggered_transitions
    new_state = EVENT_TRIGGERED_TRANSITIONS[role][state][event_type]
KeyError: <class 'h11._events.ConnectionClosed'>

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "client_script.py", line 29, in <module>
    asyncio.run(main(port))
  File "/Users/yeray/.pyenv/versions/3.7.3/lib/python3.7/asyncio/runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "/Users/yeray/.pyenv/versions/3.7.3/lib/python3.7/asyncio/base_events.py", line 584, in run_until_complete
    return future.result()
  File "client_script.py", line 21, in main
    await request(port)
  File "client_script.py", line 11, in request
    resp = await http_client.get(f"http://localhost:{port}")
  File "/Users/yeray/code/personal/_forks/httpx/httpx/client.py", line 316, in get
    timeout=timeout,
  File "/Users/yeray/code/personal/_forks/httpx/httpx/client.py", line 548, in request
    timeout=timeout,
  File "/Users/yeray/code/personal/_forks/httpx/httpx/client.py", line 144, in send
    allow_redirects=allow_redirects,
  File "/Users/yeray/code/personal/_forks/httpx/httpx/client.py", line 177, in send_handling_redirects
    request, verify=verify, cert=cert, timeout=timeout
  File "/Users/yeray/code/personal/_forks/httpx/httpx/dispatch/connection_pool.py", line 128, in send
    raise exc
  File "/Users/yeray/code/personal/_forks/httpx/httpx/dispatch/connection_pool.py", line 119, in send
    request, verify=verify, cert=cert, timeout=timeout
  File "/Users/yeray/code/personal/_forks/httpx/httpx/dispatch/connection.py", line 54, in send
    response = await self.h11_connection.send(request, timeout=timeout)
  File "/Users/yeray/code/personal/_forks/httpx/httpx/dispatch/http11.py", line 58, in send
    http_version, status_code, headers = await self._receive_response(timeout)
  File "/Users/yeray/code/personal/_forks/httpx/httpx/dispatch/http11.py", line 130, in _receive_response
    event = await self._receive_event(timeout)
  File "/Users/yeray/code/personal/_forks/httpx/httpx/dispatch/http11.py", line 161, in _receive_event
    event = self.h11_state.next_event()
  File "/Users/yeray/.pyenv/versions/httpx/lib/python3.7/site-packages/h11/_connection.py", line 439, in next_event
    exc._reraise_as_remote_protocol_error()
  File "/Users/yeray/.pyenv/versions/httpx/lib/python3.7/site-packages/h11/_util.py", line 72, in _reraise_as_remote_protocol_error
    raise self
  File "/Users/yeray/.pyenv/versions/httpx/lib/python3.7/site-packages/h11/_connection.py", line 422, in next_event
    self._process_event(self.their_role, event)
  File "/Users/yeray/.pyenv/versions/httpx/lib/python3.7/site-packages/h11/_connection.py", line 238, in _process_event
    self._cstate.process_event(role, type(event), server_switch_event)
  File "/Users/yeray/.pyenv/versions/httpx/lib/python3.7/site-packages/h11/_state.py", line 238, in process_event
    self._fire_event_triggered_transitions(role, event_type)
  File "/Users/yeray/.pyenv/versions/httpx/lib/python3.7/site-packages/h11/_state.py", line 253, in _fire_event_triggered_transitions
    .format(event_type.__name__, role, self.states[role]))
h11._util.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE
```
Relevant snippet from the [h11 docs](https://h11.readthedocs.io/en/latest/api.html#h11.Connection.next_event):

> `Connection.next_event` Raises: RemoteProtocolError – The peer has misbehaved. You should close the connection (possibly after sending some kind of 4xx response)



Okay, so it's related to an issue with keep-alive connections on HTTP/1.1.

I can replicate fine with `uwsgi`, but not `gunicorn`. (Difference being that gunicorn doesn't *itself* support keep-alive, but instead relies on being proxied behing nginx in order to support persistent connections)
I believe you can turn on gunicorn keepalive setting with —keepalive N
Did some more debugging, I'm a bit out of my depth on this lower level details so bear with me 😅 

During keep-alive situations the asyncio `StreamReader` and `StreamWriter` objects part of the `HTTP11Connection` are reused. I noticed once the reader reads the first response its [`at_eof`](https://docs.python.org/3/library/asyncio-stream.html#asyncio.StreamReader.at_eof) is returning `True`. When trying to read the second response it returns an empty bytes which is interpreted by h11 as the server has closed the connection instead of sending the response.

The [StreamReader.read docs](https://docs.python.org/3/library/asyncio-stream.html#asyncio.StreamReader.read) mention:

> If EOF was received and the internal buffer is empty, return an empty bytes object.

I would've thought that the internal buffer of the reader would not be empty and would hold the new response data but it returns the empty bytes.

I find unlikely that both FastAPI and Quart are misbehaving in the exact same way and not sending response data, so is it possible we need to "refresh" the reader somehow after it reading EOF? I can't see any obvious API to do such a thing, aside from creating a whole new connection which would obviously defeat the purpose.

Sorry if it's a silly question, but I can't really find any other reason for this error, which btw happens irregardless of the waiting in the caller scripts, two consecutive keep-alive requests will trigger it.
Or we need to not feed an empty string into the `h11` state machine. Maybe instead if we get an EOF back from `StreamReader` we don't forward it to `h11`?

See this comment: https://github.com/python-hyper/h11/blob/master/h11/_connection.py#L320
I had a look at the asyncio code looks to me like there's no real way of reusing the reader? 😕 

Once the first EOF is read the [protocol's `eof_received` method](https://github.com/python/cpython/blob/v3.7.4/Lib/asyncio/streams.py#L260) [sets the reader's EOF flag](https://github.com/python/cpython/blob/v3.7.4/Lib/asyncio/streams.py#L419-L421) which [prevents further reading from it](https://github.com/python/cpython/blob/v3.7.4/Lib/asyncio/streams.py#L639-L640).

I thought I'd look into how `aiohttp` might be handling this and seems they use custom transport and protocol instances. The latter seems to [handle EOF differently](https://github.com/aio-libs/aiohttp/blob/master/aiohttp/client_proto.py#L111-L113) but I am *really* out of my depth on that code so I might be completely wrong.

#rabbithole
When I run this against the latest Hypercorn using @yeraydiazdiaz example it works, with this output,
```
Performing request
Got response b'/Users/pgjones/quart/quart-benchmark/servers/quart_server.py-fib(12)=144' 

Performing request
Got response b'/Users/pgjones/quart/quart-benchmark/servers/quart_server.py-fib(12)=144' 
```
crucially though I changed the time between the requests to **2 seconds** as the hypercorn keep alive timeout default is **5 seconds**. I think this is why it failed for Hypercorn and Uvicorn as both have a 5 second keep alive timeout. 

This is what Hypercorn receives (`>`) and sends (`<`),
```
> b'GET / HTTP/1.1\r\nhost: localhost:8000\r\nuser-agent: python-httpx/0.6.7\r\naccept: */*\r\naccept-encoding: gzip, deflate\r\nconnection: keep-alive\r\n\r\n'
< b'HTTP/1.1 200 \r\ncontent-type: text/html; charset=utf-8\r\ncontent-length: 13\r\ndate: Wed, 24 Jul 2019 21:22:21 GMT\r\nserver: hypercorn-h11\r\n\r\n'
< b'Hello, World!'
< b''
> b'GET / HTTP/1.1\r\nhost: localhost:8000\r\nuser-agent: python-httpx/0.6.7\r\naccept: */*\r\naccept-encoding: gzip, deflate\r\nconnection: keep-alive\r\n\r\n'
< b'HTTP/1.1 200 \r\ncontent-type: text/html; charset=utf-8\r\ncontent-length: 13\r\ndate: Wed, 24 Jul 2019 21:22:23 GMT\r\nserver: hypercorn-h11\r\n\r\n'
< b'Hello, World!'
< b''
> b''
```

I think this issue is that httpx should convert the error raised by h11 to something like ConnectionClosedByRemote.
Right, so if understand correctly, the issue is handling the server closing keep-alive connections after a timeout.

My initial approach would be to have httpx handle the exception, reestablish the connection and retry, I'll see if I can put together a PR for this.

Thanks for looking into it @pgjones  🌟 
So the thing that's not clear to me on first sight, is why the disconnect isn't being caught at the point we start writing the next response... https://github.com/encode/httpx/blob/master/httpx/dispatch/http11.py#L50

We've got the logic there to handle the disconnect case, and we're writing to the network and calling drain.

That *actually* needs to be the point at which we figure out if we're still connected or not, because if we wait until getting the response, then for non-idempotent requests it's ambiguous to us as the client if we're *actually* okay to resend. (Or if the server did in fact handle the request, but some intermediary has disconnected us before we started seeing the response).

So, questions:

* How does `urllib3` determine if the network connection is still alive, when re-acquring connections from the pool?
* How does `aiohttp` determine if the network connection is still alive, when re-acquring connections from the pool?

/cc @sethmlarson 
urllib3 has a [`is_connection_dropped()`](https://github.com/urllib3/urllib3/blob/master/src/urllib3/util/connection.py#L7) function that doesn't tell the whole story. Have to look at httplib as well.
Okay, so I think the right approach would be for us to have an equivelent `is_connection_dropped` on the `Reader` class in https://github.com/encode/httpx/blob/master/httpx/concurrency.py replacing the existing logic of "suck it and see".

Does that make sense to you too, @yeraydiazdiaz? #143 is a great start - nice test case for the issue that can still be used here.

(Actually I think we'll also want to combine the `Reader` and `Writer` interfaces into a single `SocketConnection`, but that's a different story.)
Closed via #145 
Released to PyPI as 0.6.8
Unfortunately, I get the same exception again in 0.6.8.

I use httpx in an oauth2 authorize flow. When I get authorization code from the auth server, I use httpx client to get an access token. After the access toke expired, the first invocation to obtain a new access token is almost always failed, the second invocation(refresh the page) is ok. The situation is similar to last time.

Some facts:
1. The httpx client is a long-live single instance
2. The invocation interval is long because it depends on the access token expire time(2 hours) and only a few users use it

Logs:
```python
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/client.py", line 687, in get
    timeout=timeout,
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/client.py", line 634, in request
    async_response = concurrency_backend.run(coroutine, *args, **kwargs)
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/concurrency.py", line 243, in run
    return self.loop.run_until_complete(coroutine(*args, **kwargs))
  File "uvloop/loop.pyx", line 1451, in uvloop.loop.Loop.run_until_complete
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/client.py", line 144, in send
    allow_redirects=allow_redirects,
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/client.py", line 177, in send_handling_redirects
    request, verify=verify, cert=cert, timeout=timeout
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/dispatch/connection_pool.py", line 118, in send
    raise exc
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/dispatch/connection_pool.py", line 113, in send
    request, verify=verify, cert=cert, timeout=timeout
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/dispatch/connection.py", line 54, in send
    response = await self.h11_connection.send(request, timeout=timeout)
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/dispatch/http11.py", line 52, in send
    http_version, status_code, headers = await self._receive_response(timeout)
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/dispatch/http11.py", line 124, in _receive_response
    event = await self._receive_event(timeout)
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/httpx/dispatch/http11.py", line 155, in _receive_event
    event = self.h11_state.next_event()
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/h11/_connection.py", line 439, in next_event
    exc._reraise_as_remote_protocol_error()
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/h11/_util.py", line 72, in _reraise_as_remote_protocol_error
    raise self
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/h11/_connection.py", line 422, in next_event
    self._process_event(self.their_role, event)
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/h11/_connection.py", line 238, in _process_event
    self._cstate.process_event(role, type(event), server_switch_event)
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/h11/_state.py", line 238, in process_event
    self._fire_event_triggered_transitions(role, event_type)
  File "/root/.local/share/virtualenvs/authcenter-zRBZbntZ/lib/python3.7/site-packages/h11/_state.py", line 253, in _fire_event_triggered_transitions
    .format(event_type.__name__, role, self.states[role]))
h11._util.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE
```

Thanks for reporting this issue @lch277, I'm going to reopen this issue since it seems similar.

This would be a great time to have had our debug logging in place so we can exactly track down the sequence of events in your request. Maybe we should speed up the integration of those logs to help solve issues like this.
I also had this error with httpx 0.7.2 while communicating with a FastAPI based application running straight on Uvicorn (no gunicorn).  This specific issue was against a brand new endpoint I just added in, but other similar endpoints have worked fine with requests for quite some time now.  I have not yet had a chance to dig into logs, will do that later today and try to post back something more useful here.

A quick check reveals my FastAPI server appears to have crashed, of course without any sort of decent logging, at the same time this request was made.  I'm tentatively pointing my blaming finger at my server application crashing mid-request.
@iwoloschin Yes but we should be able to handle that sort of issue gracefully from the HTTPX side. You shouldn't be getting an `h11` exception.
@sethmlarson I won't argue about that :).  I'd much prefer only having to think about HTTPX exceptions when using HTTPX!

I've added some logging and kicked off another test.  It's not a fast test and the issue appears to have been intermittent, so I might not have any results for a while and if it's truly intermittent I may never get results (but hey, no errors is good too, right...?).
@iwoloschin Are you aware of `HTTPX_DEBUG=1` for debugging high-level events happening within HTTPX? :)
Nope!  I'll take a look at that.

My re-test failed with a `httpx.exceptions.ReadTimeout` on a different endpoint that should take less than a second.  I'm leaning towards my problem with the h11 exception being my server crashing.  I'll still second a vote for encapsulating exceptions to only throw HTTPX exceptions.
> I'll still second a vote for encapsulating exceptions to only throw HTTPX exceptions.

Agreed, yup.
So, is this still an issue? And if so, what can we do to resolve it?

- Should we introduce a sort of « catch all » wrapper exception? (That doesn’t seem easily feasible to me without introducing cruft.)
- Perhaps we could build up a small reproduction example in which we’d forcibly crash or close the remote server and see what happens, ie whether we get the h11 exception again?
I just ran into this issue again.

In my case, I sort of expect something stupid to happen, I'm deliberately `GET`ing an endpoint on a server that is attempting to reboot, in an attempt to take an action after the reboot is complete & the HTTP service (FastAPI) is back up.  I'm fine to catch some exceptions here (`HTTPError` or `ConnectionRefusedError`), it kind of makes sense to throw an exception if you can't reach the server, but I think catching an `h11` exception here is confusing, especially since I don't normally have to touch `h11`, `httpx` manages all of that for me (and it's lovely when it works!)  Without really being knowledgable about the internals, I'd put in a strong vote for `httpx` raising an `HTTPError`-based exception here.  If someone wants to point me in the right direction I can try to help with that...but I know very little about how `httpx` or `h11` work internally so it wouldn't be fast 😄 .

I haven't dug into it yet, but the timing appears to coincide with when the HTTP Service is stopped.  I might be able to come up with a small, reproducible example but it might take a few days.
Quick addendum, if I put a short sleep in, to try and avoid hitting the endpoint as the HTTP service is being stopped it appears to avoid the `h11` exception.  I then got `OSError: [Errno 113] Host is unreachable`, which...is correct, but also not exactly intuitive.
> Should we introduce a sort of « catch all » wrapper exception? (That doesn’t seem easily feasible to me without introducing cruft.)

This one doesn't *look* like it ought to be a catch-all case. Seems to be an*expected* possible behaviour that we ought to explicitly catch and raise as a ConnectionClosed or whatever?

> Perhaps we could build up a small reproduction example in which we’d forcibly crash or close the remote server and see what happens, ie whether we get the h11 exception again?

Certainly it'd be *really* helpful to be able to reliable reproduce this yup.
Here's a simple reproducible example.  It uses two separate files and requires `fastapi` and `httpx`.

FastAPI Server:
```python
from fastapi import FastAPI

app = FastAPI()

@app.get("/hello")
async def hello():
    return "World"

# To Run:
# gunicorn -k uvicorn.workers.UvicornWorker -b 0.0.0.0:80  test:app
```

Client:
```python
#!/usr/bin/env python3

import httpx


def main():
    try:
        while True:
            r = httpx.get("http://localhost/hello")
            print(r.text)
    except KeyboardInterrupt:
        print("done")


if __name__ == "__main__":
    main()
```

In one terminal run the server (I'm using `gunicorn` with a `uvicorn` worker since that's how I normally run my FastAPI applications).

In a second terminal, launch the test (I just did `./test.py`).  Let it run for a second and then kill the *server* with a `CTRL-C` in the first terminal.  The test client, in the second terminal, should die with the expected `h11._util.RemoteProtocolError` exception.  In a quick test (~5 tries) I got the expected error 4 times and a `ConnectionRefused` error once, which kind of makes sense since it appears to be a race condition of whether or not the server finishes sending the response before it dies.

For what it's worth I'm running on macOS (Mojave) with Homebrew Python3.7 in a virtual environment with the latest `httpx` and `fastapi`.
Thanks @iwoloschin, that's very helpful. I was able to reproduce using the even simpler setup below (plain ASGI app + uvicorn):

```python
# app.py
async def app(scope, receive, send):
    assert scope["type"] == "http"
    await send(
        {
            "type": "http.response.start",
            "status": 200,
            "headers": [[b"content-type", b"text/plain"]],
        }
    )
    await send({"type": "http.response.body", "body": b"Hello, world!"})

# uvicorn app:app
```

I received the `h11` error 3 times out of 5, the 2 other failures were an `OSError`. I'm running the same setup as you (except for Python 3.7 via `pyenv`).

Another piece of info: I get the same result whether I use the top-level API level, a `Client`, or an `AsyncClient`.
Good digging folks!
@florimondmanca I'm also getting this one, but quite reliably with ssl. Works well without ssl.
I'm also using a simple ASGI app + uvicorn + h11 in the AsyncClient.
```
import asyncio
import pytest

from httpx import AsyncClient

from uvicorn.config import Config
from uvicorn.main import Server

global_call_count = 0

class App:
    def __init__(self, scope):
        self.scope = scope
        self.call_count = 0

    async def __call__(self, receive, send):
        global global_call_count
        
        self.call_count += 1
        global_call_count += 1

        await send({"type": "http.response.start", "status": 200, "headers": []})
        await send({"type": "http.response.body", "body": bytes(f"That's the body! - {self.call_count} - {global_call_count}", "utf-8"), "more_body": False})


class CustomServer(Server):
    pass


@pytest.mark.asyncio
async def test_baseline_h11(certfile_and_keyfile):
    certfile, keyfile = certfile_and_keyfile

    client = AsyncClient(http_versions=["HTTP/1.1"])
    config = Config(
        app=App,
        loop="asyncio",
        limit_max_requests=3,
        http='h11',
        # ssl_certfile=certfile,
        # ssl_keyfile=keyfile
    )
    server = CustomServer(config=config)

    # Prepare the coroutine to serve the request
    run_request = server.serve()
    # Prepare the coroutine to make the request
    make_request = client.get("http://127.0.0.1:8000")

    # Reset the global counter
    global global_call_count
    global_call_count = 0

    # Run coroutines
    results = await asyncio.gather(*[
        run_request,
        client.get("http://127.0.0.1:8000"),
        client.get("http://127.0.0.1:8000"),
        client.get("http://127.0.0.1:8000"),
    ])

    assert [x.text for x in results if x] == ["That's the body! - 1 - 2", "That's the body! - 1 - 3", "That's the body! - 1 - 4"]
```
Actually, my test case has a mistake, it tries to communicate over HTTP instead of HTTPS.
And fails in this case

```
13:39:34.627 - httpx.dispatch.connection - connected http_version='HTTP/1.1'
DEBUG:    connected http_version='HTTP/1.1'
13:39:34.627 - httpx.dispatch.http11 - send_headers method='GET' target='/' headers=Headers({'host': '127.0.0.1:8000', 'user-agent': 'python-httpx/0.7.6', 'accept': '*/*', 'accept-encoding': 'gzip, deflate', 'connection': 'keep-alive'})
DEBUG:    send_headers method='GET' target='/' headers=Headers({'host': '127.0.0.1:8000', 'user-agent': 'python-httpx/0.7.6', 'accept': '*/*', 'accept-encoding': 'gzip, deflate', 'connection': 'keep-alive'})
13:39:34.628 - httpx.dispatch.http11 - receive_event event=NEED_DATA
DEBUG:    receive_event event=NEED_DATA

...

    def _fire_event_triggered_transitions(self, role, event_type):
        state = self.states[role]
        try:
>           new_state = EVENT_TRIGGERED_TRANSITIONS[role][state][event_type]
E           KeyError: <class 'h11._events.ConnectionClosed'>

venv/lib/python3.7/site-packages/h11/_state.py:249: KeyError

...

self = <h11._state.ConnectionState object at 0x103f8b450>, role = SERVER, event_type = <class 'h11._events.ConnectionClosed'>

    def _fire_event_triggered_transitions(self, role, event_type):
        state = self.states[role]
        try:
            new_state = EVENT_TRIGGERED_TRANSITIONS[role][state][event_type]
        except KeyError:
            raise LocalProtocolError(
                "can't handle event type {} when role={} and state={}".format(
>                   event_type.__name__, role, self.states[role]
                )
            )
E           h11._util.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE

venv/lib/python3.7/site-packages/h11/_state.py:253: RemoteProtocolError
```
I guess it's just another facet of the problem
just hit this issue (triggered by killing the server during long polling), and this is my stab at pondering  the stack trace I'm seeing. 

- h11 cannot process event given the current state: "  h11._util.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE"
- h11 docs  say " If either side violates the protocol, you'll get a h11.ProtocolError exception."

So we have two choices: 

1. detect and circumvent the connectionclosed event from getting to h11 causing it to blow up
2.  decode the h11.ProtocolError and throw a more appropriate event

The issue with #2 is that h11 doesn't put details in their exception outside of a string, so either we're fixing that in h11 (adding more structured detailed inside the exception), or we're parsing the msg from the exception.

Thoughts?  I'm going to see if there's a way to prevent the ConnectionClose event blowing up h11 (choice number #1).  


So I've done some further analysis, and it seems that h11 doesn't give us an opportunity for a detailed exception.  The documentation was hopeful  (see h11's apidocs for  [Connection.next_event](https://h11.readthedocs.io/en/latest/_modules/h11/_connection.html#Connection.next_event) ) but 
neither their_state or error_status_hint allows us to create anything but a generic error.  I could whip up a pull request based on this and some of the test case listed in earlier comments.  Let me know.



```diff --git a/httpx/dispatch/http11.py b/httpx/dispatch/http11.py
index b1781bf..926560f 100644
--- a/httpx/dispatch/http11.py
+++ b/httpx/dispatch/http11.py
@@ -6,6 +6,7 @@ from ..concurrency.base import BaseSocketStream, ConcurrencyBackend, TimeoutFlag
 from ..config import TimeoutConfig, TimeoutTypes
 from ..models import AsyncRequest, AsyncResponse
 from ..utils import get_logger
+from ..exceptions import ProtocolError, ConnectionClosed
 
 H11Event = typing.Union[
     h11.Request,
@@ -161,7 +162,13 @@ class HTTP11Connection:
         Read a single `h11` event, reading more data from the network if needed.
         """
         while True:
-            event = self.h11_state.next_event()
+            try:
+                event = self.h11_state.next_event()
+            except h11.RemoteProtocolError as e:
+                logger.debug(f"h11.RemoteProtocolError exception their_state={self.h11_state.their_state} error_status_hint={e.error_status_hint}")
+                if self.stream.is_connection_closed():
+                    raise ConnectionClosed(e)
+                raise ProtocolError(e)
 
             if isinstance(event, h11.Data):
                 logger.trace(f"receive_event event=Data(<{len(event.data)} bytes>)")
diff --git a/httpx/exceptions.py b/httpx/exceptions.py
index 81df38b..01f4f87 100644
--- a/httpx/exceptions.py
+++ b/httpx/exceptions.py
@@ -149,6 +149,10 @@ class InvalidURL(HTTPError):
     URL was missing a hostname, or was not one of HTTP/HTTPS.
     """
 
+class ConnectionClosed(HTTPError):
+    """
+    Expected more data from peer, but connection was closed.
+    """
 
 class CookieConflict(HTTPError):
     """


```
h11 could give more detailed exceptions if that's useful (or just clearer error messages), but it's not clear to me what the actual problem you're trying to solve is. You have a server that's closing the connection before it finishes sending the response. Do you want to treat that differently from other kinds of server misbehavior? What information do you actually need here?
> h11 could give more detailed exceptions if that's useful (or just clearer error messages), but it's not clear to me what the actual problem you're trying to solve is. You have a server that's closing the connection before it finishes sending the response. Do you want to treat that differently from other kinds of server misbehavior? What information do you actually need here?

It is common in http client libraries to give specific exceptions for transport layer issues, like readtimeout, connectionrefused.   This dropped connection indicates a different class of issue than a typical protocol violation (and the user may want to take a different tactic in handling it, e.g. switch to a different server within a cluster).

However, from httpx's perspective, I believe it would react the same either way (e.g. it cannot attempt to retry on it's own).  I think the only value in making a distinct exception for this fault case is to make it easier for the httpx user to understand and handle the the failure.    So I think h11's behavior is fine as is.  

I hope my analysis has been helpful, but I think @tomchristie should weigh in now.
@toppk - Your diff in https://github.com/encode/httpx/issues/96#issuecomment-552730324 looks about right to me. We just need to be wrapping this case up properly and exposing it as a `ConnectionClosed` exception.

I *think* we should probably be checking for the event type `ConnectionClosed`, rather than inspecting `if self.stream.is_connection_closed():` to determine if we're raising a protocol error, or a connection closed exception type.
> @toppk - Your diff in [#96 (comment)](https://github.com/encode/httpx/issues/96#issuecomment-552730324) looks about right to me. We just need to be wrapping this case up properly and exposing it as a `ConnectionClosed` exception.
> 
> I _think_ we should probably be checking for the event type `ConnectionClosed`, rather than inspecting `if self.stream.is_connection_closed():` to determine if we're raising a protocol error, or a connection closed exception type.

By design of h11, the ConnectionClosed isn't expected, there is no event type 'ConnectionClosed' exposed back to the caller, only a RemoteProtocolError exception.    Or said another way, h11 only exposes ConnectionClosed event when the protocol expects the connection to be closed.  

\<verbose details>
The current model of h11 isn't to process unexpected events ( review [h11._state](https://github.com/python-hyper/h11/blob/master/h11/_state.py) for the design), but just to raise an exception.  The little exception characterization that exists in occurs in [Connection.next_state](https://github.com/python-hyper/h11/blob/68e32dbb475d5241f077afaa278b6ef248b1f9bd/h11/_connection.py#L429), and simple sets a recommend http error code (in an attribute named error_status_hint, which in our failure mode is left at the generic 400)
\</verbose details>

Although `if self.stream.is_connection_closed()` is just a heuristic, but it seems to be helpful, considering otherwise we'd just be sending up httpx.exceptions.ProtocolError.   

I think if you wanted to avoid the heuristic, without redesigning h11, I think the simplest improvement h11 could make is to add the state of the protocol inside the exception  created at [h11._state.ConnectionState._fire_event_triggered_transitions](https://github.com/python-hyper/h11/blob/68e32dbb475d5241f077afaa278b6ef248b1f9bd/h11/_state.py#L251) rather than just as a string, and bubble that up into the RemoteProtocolError.   Then we could examine RemoteProtocolError for the unhandled ConnectionClosed event, and infer that is the root cause.    

Or we could just parse the string in the exception we get right now :)
Misunderstanding on my part then. Examine the connection. 👍
Also this particular exception can only happen if you've passed b"" to h11
to tell it that the connection was closed, in case tracking that is somehow
easier.

On Tue, Nov 12, 2019, 11:03 Tom Christie <notifications@github.com> wrote:

> Misunderstanding on my part then. Examine the connection. 👍
>
> —
> You are receiving this because you commented.
> Reply to this email directly, view it on GitHub
> <https://github.com/encode/httpx/issues/96?email_source=notifications&email_token=AAEU42HAHPMMB63Z7OYYFQ3QTL4WXA5CNFSM4HZGWBRKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOED3QWCY#issuecomment-553061131>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAEU42EYGW5TNGGPZPHEDOLQTL4WXANCNFSM4HZGWBRA>
> .
>


Resolving patch:
diff --git a/httpx/dispatch/http11.py b/httpx/dispatch/http11.py
--- a/httpx/dispatch/http11.py
+++ b/httpx/dispatch/http11.py
@@ -4,6 +4,7 @@
 
 from ..concurrency.base import BaseSocketStream, ConcurrencyBackend, TimeoutFlag
 from ..config import TimeoutConfig, TimeoutTypes
+from ..exceptions import ConnectionClosed, ProtocolError
 from ..models import AsyncRequest, AsyncResponse
 from ..utils import get_logger
 
@@ -161,7 +162,17 @@ async def _receive_event(self, timeout: TimeoutConfig = None) -> H11Event:
         Read a single `h11` event, reading more data from the network if needed.
         """
         while True:
-            event = self.h11_state.next_event()
+            try:
+                event = self.h11_state.next_event()
+            except h11.RemoteProtocolError as e:
+                logger.debug(
+                    "h11.RemoteProtocolError exception "
+                    + f"their_state={self.h11_state.their_state} "
+                    + f"error_status_hint={e.error_status_hint}"
+                )
+                if self.stream.is_connection_dropped():
+                    raise ConnectionClosed(e)
+                raise ProtocolError(e)
 
             if isinstance(event, h11.Data):
                 logger.trace(f"receive_event event=Data(<{len(event.data)} bytes>)")
diff --git a/httpx/exceptions.py b/httpx/exceptions.py
--- a/httpx/exceptions.py
+++ b/httpx/exceptions.py
@@ -150,6 +150,12 @@ class InvalidURL(HTTPError):
     """
 
 
+class ConnectionClosed(HTTPError):
+    """
+    Expected more data from peer, but connection was closed.
+    """
+
+
 class CookieConflict(HTTPError):
     """
     Attempted to lookup a cookie by name, but multiple cookies existed.


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -63,6 +63,8 @@ async def app(scope, receive, send):
     assert scope["type"] == "http"
     if scope["path"].startswith("/slow_response"):
         await slow_response(scope, receive, send)
+    elif scope["path"].startswith("/premature_close"):
+        await premature_close(scope, receive, send)
     elif scope["path"].startswith("/status"):
         await status_code(scope, receive, send)
     elif scope["path"].startswith("/echo_body"):
@@ -103,6 +105,16 @@ async def slow_response(scope, receive, send):
     await send({"type": "http.response.body", "body": b"Hello, world!"})
 
 
+async def premature_close(scope, receive, send):
+    await send(
+        {
+            "type": "http.response.start",
+            "status": 200,
+            "headers": [[b"content-type", b"text/plain"]],
+        }
+    )
+
+
 async def status_code(scope, receive, send):
     status_code = int(scope["path"].replace("/status/", ""))
     await send(
diff --git a/tests/dispatch/test_connections.py b/tests/dispatch/test_connections.py
--- a/tests/dispatch/test_connections.py
+++ b/tests/dispatch/test_connections.py
@@ -1,4 +1,6 @@
-from httpx import HTTPConnection
+import pytest
+
+from httpx import HTTPConnection, exceptions
 
 
 async def test_get(server, backend):
@@ -15,6 +17,15 @@ async def test_post(server, backend):
         assert response.status_code == 200
 
 
+async def test_premature_close(server, backend):
+    with pytest.raises(exceptions.ConnectionClosed):
+        async with HTTPConnection(origin=server.url, backend=backend) as conn:
+            response = await conn.request(
+                "GET", server.url.copy_with(path="/premature_close")
+            )
+            await response.read()
+
+
 async def test_https_get_with_ssl_defaults(https_server, ca_cert_pem_file, backend):
     """
     An HTTPS request, with default SSL configuration set on the client.