Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

h11._util.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE #96

Closed
didip opened this issue Jun 18, 2019 · 109 comments · Fixed by #103 or #524
Labels
bug Something isn't working discussion external Root cause pending resolution in an external dependency
Milestone

Comments

@didip
Copy link

didip commented Jun 18, 2019

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
@didip
Copy link
Author

didip commented Jun 18, 2019

I think this is caused by the client code in http3.

@tomchristie tomchristie transferred this issue from encode/uvicorn Jun 19, 2019
@tomchristie
Copy link
Member

Yup - the traceback is from http3 there - have moved this issue across.

@lch277
Copy link

lch277 commented Jul 16, 2019

This problem still exists in the lastest version 0.6.7

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

@tomchristie
Copy link
Member

@lch277 Could you provide a simple reproducible example so that we can replicate the issue?

@lch277
Copy link

lch277 commented Jul 16, 2019

There are two web servers.
testapp.py

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

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:

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.

@tomchristie tomchristie reopened this Jul 23, 2019
@tomchristie
Copy link
Member

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)

@yeraydiazdiaz
Copy link
Contributor

yeraydiazdiaz commented Jul 23, 2019

I can reproduce it. I've narrowed it down to this "caller" script:

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:

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:

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:

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

@yeraydiazdiaz
Copy link
Contributor

Relevant snippet from the h11 docs:

Connection.next_event Raises: RemoteProtocolError – The peer has misbehaved. You should close the connection (possibly after sending some kind of 4xx response)

@tomchristie
Copy link
Member

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)

@didip
Copy link
Author

didip commented Jul 23, 2019

I believe you can turn on gunicorn keepalive setting with —keepalive N

@yeraydiazdiaz
Copy link
Contributor

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 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 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.

@sethmlarson
Copy link
Contributor

sethmlarson commented Jul 23, 2019

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

@yeraydiazdiaz
Copy link
Contributor

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 sets the reader's EOF flag which prevents further reading from it.

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 but I am really out of my depth on that code so I might be completely wrong.

#rabbithole

@pgjones
Copy link

pgjones commented Jul 24, 2019

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.

@tomchristie tomchristie added the bug Something isn't working label Jul 25, 2019
@yeraydiazdiaz
Copy link
Contributor

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 🌟

@tomchristie
Copy link
Member

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

@sethmlarson
Copy link
Contributor

urllib3 has a is_connection_dropped() function that doesn't tell the whole story. Have to look at httplib as well.

@tomchristie
Copy link
Member

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.)

@tomchristie
Copy link
Member

Closed via #145
Released to PyPI as 0.6.8

@lch277
Copy link

lch277 commented Aug 23, 2019

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:

  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

@sethmlarson
Copy link
Contributor

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.

@sethmlarson sethmlarson reopened this Aug 23, 2019
@tomchristie
Copy link
Member

And are you testing against an external URL, or a service that's running locally?

@dmig-alarstudios
Copy link

No: #96 (comment)
2 services locally, interacting with each other.

And yes, it's 100% reproducible with requests. Definitely not an httpx issue but uvicorn.

@dmig-alarstudios
Copy link

dmig-alarstudios commented Jul 31, 2020

DEBUG:urllib3.connectionpool:Starting new HTTP connection (1): 127.0.0.1:9093
send: b'POST /ep1/ HTTP/1.1\r\nHost: 127.0.0.1:9093\r\nUser-Agent: python-requests/2.24.0\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\nContent-Type: application/json\r\nContent-Length: 214\r\n\r\n'
send: b'some data'
reply: 'HTTP/1.1 201 Created\r\n'
header: date: Fri, 31 Jul 2020 13:53:22 GMT
header: server: uvicorn
header: content-length: 835
header: content-type: application/json
header: x-correlation-id: 23856694-cc1e-46a0-a9dc-b735eaa1ac9c
DEBUG:urllib3.connectionpool:http://127.0.0.1:9093 "POST /ep1/ HTTP/1.1" 201 835

send: b'PATCH /ep2/ HTTP/1.1\r\nHost: 127.0.0.1:9093\r\nUser-Agent: python-requests/2.24.0\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\nContent-Length: 122\r\nContent-Type: application/json\r\n\r\n'
send: b'some other data'
reply: ''
Traceback (most recent call last):
  File ".../lib/python3.8/site-packages/urllib3/connectionpool.py", line 670, in urlopen
    httplib_response = self._make_request(
  File ".../lib/python3.8/site-packages/urllib3/connectionpool.py", line 426, in _make_request
    six.raise_from(e, None)
  File "<string>", line 3, in raise_from
  File ".../lib/python3.8/site-packages/urllib3/connectionpool.py", line 421, in _make_request
    httplib_response = conn.getresponse()
  File "/usr/lib/python3.8/http/client.py", line 1332, in getresponse
    response.begin()
  File "/usr/lib/python3.8/http/client.py", line 303, in begin
    version, status, reason = self._read_status()
  File "/usr/lib/python3.8/http/client.py", line 272, in _read_status
    raise RemoteDisconnected("Remote end closed connection without"
http.client.RemoteDisconnected: Remote end closed connection without response

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File ".../lib/python3.8/site-packages/requests/adapters.py", line 439, in send
    resp = conn.urlopen(
  File ".../lib/python3.8/site-packages/urllib3/connectionpool.py", line 726, in urlopen
    retries = retries.increment(
  File ".../lib/python3.8/site-packages/urllib3/util/retry.py", line 403, in increment
    raise six.reraise(type(error), error, _stacktrace)
  File ".../lib/python3.8/site-packages/urllib3/packages/six.py", line 734, in reraise
    raise value.with_traceback(tb)
  File ".../lib/python3.8/site-packages/urllib3/connectionpool.py", line 670, in urlopen
    httplib_response = self._make_request(
  File ".../lib/python3.8/site-packages/urllib3/connectionpool.py", line 426, in _make_request
    six.raise_from(e, None)
  File "<string>", line 3, in raise_from
  File ".../lib/python3.8/site-packages/urllib3/connectionpool.py", line 421, in _make_request
    httplib_response = conn.getresponse()
  File "/usr/lib/python3.8/http/client.py", line 1332, in getresponse
    response.begin()
  File "/usr/lib/python3.8/http/client.py", line 303, in begin
    version, status, reason = self._read_status()
  File "/usr/lib/python3.8/http/client.py", line 272, in _read_status
    raise RemoteDisconnected("Remote end closed connection without"
urllib3.exceptions.ProtocolError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "test-with-requests.py", line 39, in <module>
    resp = client.patch(base_url + '/ep2/',
  File ".../lib/python3.8/site-packages/requests/sessions.py", line 602, in patch
    return self.request('PATCH', url, data=data, **kwargs)
  File ".../lib/python3.8/site-packages/requests/sessions.py", line 530, in request
    resp = self.send(prep, **send_kwargs)
  File ".../lib/python3.8/site-packages/requests/sessions.py", line 643, in send
    r = adapter.send(request, **kwargs)
  File ".../lib/python3.8/site-packages/requests/adapters.py", line 498, in send
    raise ConnectionError(err, request=request)
requests.exceptions.ConnectionError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'))

requests raises exactly same RemoteDisconnect exception. it's strange, that httpx changed behavior.

@tomchristie
Copy link
Member

Well that narrows it down to either:

  1. An issue with your application, itself. It's just never returning a response.
  2. An genuine issue with uvicorn.

You could narrow that down for yourself by running the same application with hypercorn and/or daphne and seeing what behaviour it exhibits then.

@tomchristie
Copy link
Member

tomchristie commented Jul 31, 2020

In any case, no, not an httpx issue, but we could have some clearer messaging in our exceptions here.

There ought to be an improvement with the upcoming 0.14, since we properly identify closed connections vs raising a protocol error from h11 if a socket read operation returns b""

@dmig-alarstudios
Copy link

I'm pretty sure, that's a uvicorn issue. Because running my service under hypercorn solves this problem:

DEBUG:urllib3.connectionpool:Starting new HTTP connection (1): 127.0.0.1:9093
send: b'POST /ep1/ HTTP/1.1\r\nHost: 127.0.0.1:9093\r\nUser-Agent: python-requests/2.24.0\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\nContent-Type: application/json\r\nContent-Length: 214\r\n\r\n'
send: b'some data'
reply: 'HTTP/1.1 201 \r\n'
header: content-length: 835
header: content-type: application/json
header: x-correlation-id: d4ade3c6-e50f-4a6b-9e25-13a1d609fab5
header: date: Fri, 31 Jul 2020 14:11:57 GMT
header: server: hypercorn-h11
DEBUG:urllib3.connectionpool:http://127.0.0.1:9093 "POST /ep1/ HTTP/1.1" 201 835

send: b'PATCH /ep2/ HTTP/1.1\r\nHost: 127.0.0.1:9093\r\nUser-Agent: python-requests/2.24.0\r\nAccept-Encoding: gzip, deflate\r\nAccept: */*\r\nConnection: keep-alive\r\nContent-Length: 122\r\nContent-Type: application/json\r\n\r\n'
send: b'some other data'
reply: 'HTTP/1.1 204 \r\n'
header: x-correlation-id: 9f653cf6-7cc8-4df1-903c-0fbe0d5f2693
header: date: Fri, 31 Jul 2020 14:11:57 GMT
header: server: hypercorn-h11
DEBUG:urllib3.connectionpool:http://127.0.0.1:9093 "PATCH /ep2/ HTTP/1.1" 204 0

@tomchristie
Copy link
Member

Something we might want to do... if we get a ReadTimeout, check .is_connection_dropped and include that information in the exception text.

@dmig-alarstudios
Copy link

@tomchristie since I have an environment with 100% reproducible issue, can you point me where to look at, to debug the issue in uvicorn?

@tomchristie
Copy link
Member

Start by checking it run against hypercorn. If it works on hypercorn but not uvicorn, then file an issue on the uvicorn repo.

patrickkwang pushed a commit to NCATS-Gamma/robokop-messenger that referenced this issue Aug 4, 2020
@dmig-alarstudios
Copy link

Ok, nailed it: encode/starlette#919

@elupus
Copy link

elupus commented Nov 20, 2020

Is this issue resolved from a client perspective? We seem to be getting a similar error with a rest client. I don't have a backtrace yet though not a good way to reproduce.

@lmmx
Copy link
Contributor

lmmx commented Mar 25, 2021

Not sure if this is "won't fix" now, but I'm getting the same error message which seems to stem from event = self.h11_state.next_event() and leads to httpx.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE as documented above.

Not using uvicorn, traceback below

Click to show traceback

Traceback (most recent call last):                                                                                          
  File "/home/louis/miniconda3/lib/python3.8/site-packages/httpx/_exceptions.py", line 326, in map_exceptions               
    yield                                                                                                                   
  File "/home/louis/miniconda3/lib/python3.8/site-packages/httpx/_client.py", line 1492, in _send_single_request            
    (status_code, headers, stream, ext) = await transport.arequest(                                                         
  File "/home/louis/miniconda3/lib/python3.8/site-packages/httpx/_transports/default.py", line 169, in arequest             
    return await self._pool.arequest(                                                                                       
  File "/home/louis/miniconda3/lib/python3.8/site-packages/httpcore/_async/connection_pool.py", line 218, in arequest       
    response = await connection.arequest(                                                                                   
  File "/home/louis/miniconda3/lib/python3.8/site-packages/httpcore/_async/connection.py", line 106, in arequest            
    return await self.connection.arequest(method, url, headers, stream, ext)                                                
  File "/home/louis/miniconda3/lib/python3.8/site-packages/httpcore/_async/http11.py", line 72, in arequest                 
    ) = await self._receive_response(timeout)                                                                               
  File "/home/louis/miniconda3/lib/python3.8/site-packages/httpcore/_async/http11.py", line 133, in _receive_response       
    event = await self._receive_event(timeout)                                                                              
  File "/home/louis/miniconda3/lib/python3.8/site-packages/httpcore/_async/http11.py", line 169, in _receive_event          
    event = self.h11_state.next_event()                                                                                     
  File "/home/louis/miniconda3/lib/python3.8/contextlib.py", line 131, in __exit__                                          
    self.gen.throw(type, value, traceback)                                                                                  
  File "/home/louis/miniconda3/lib/python3.8/site-packages/httpcore/_exceptions.py", line 12, in map_exceptions             
    raise to_exc(exc) from None                                                                                             
httpcore.RemoteProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE             
                                                                                                                            
The above exception was the direct cause of the following exception:                                                        
                                                                                                                            
Traceback (most recent call last):                                                                                          
  File "<string>", line 1, in <module>                                                                                      
  File "/home/louis/dev/beeb/src/beeb/nav/sched/catalogue.py", line 17, in __init__                                         
    self.async_pull_and_parse(listings)                                                                                     
  File "/home/louis/dev/beeb/src/beeb/nav/sched/catalogue.py", line 46, in async_pull_and_parse                             
    fetch_episode_metadata(listings, pbar=pbar, verbose=verbose)                                                            
  File "/home/louis/dev/beeb/src/beeb/nav/sched/async_utils.py", line 69, in fetch_episode_metadata                         
    return asyncio.run(async_fetch_episodes(listings, pbar, verbose))                                                       
  File "/home/louis/miniconda3/lib/python3.8/asyncio/runners.py", line 43, in run                                           
    return loop.run_until_complete(main)                                                                                    
  File "/home/louis/miniconda3/lib/python3.8/asyncio/base_events.py", line 616, in run_until_complete                       
    return future.result()                                                                                                  
  File "/home/louis/dev/beeb/src/beeb/nav/sched/async_utils.py", line 65, in async_fetch_episodes                           
    return await zs                                                                                                         
  File "/home/louis/miniconda3/lib/python3.8/site-packages/aiostream/core.py", line 32, in wait_stream

A bit of a 'Heisenbug', happens less than 10% of the time when requesting (many) JSON files for episode metadata from the BBC's radio schedules. Using my library beeb I get it from beeb.nav.sched.ProgrammeCatalogue("r4", n_days=30) and can greatly increase its frequency by switching my httpx.AsyncClient to use http2=True.

Not disastrous, I will probably just catch and retry, but thought I should pitch in with my experience 😃

@dmig-alarstudios
Copy link

@lmmx look at #96 (comment) -- there is a workaround

@tomchristie
Copy link
Member

A bit of a 'Heisenbug', happens less than 10% of the time when requesting (many) JSON files for episode metadata from the BBC's radio schedules.

@lmmx - Could you provide enough information to reproduce this? I'd be happy to spend some time looking into it.

@dmig-alarstudios
Copy link

also answering 'yes' to any of these questions means that this is not a httpx problem:

  • do you use BackgroundTasks?
  • do you use BaseHTTPMiddleware or middleware decorator from Starlette?

@lioncui
Copy link

lioncui commented Nov 24, 2021

i have same problem with fastapi + uvicorn.
can i use uwsgi replace uvicorn to fix it ?

@jouve
Copy link

jouve commented Dec 23, 2021

I could reproduce with this snippet:

import asyncio
async def main():
    async with AsyncClient() as session:
        await asyncio.wait([
           asyncio.create_task(session.get('http://localhost:80'))
           for _ in range(1000)
        ])
asyncio.run(main())

the ConnectionClosed seems to be generated only in this place in h11:

    def _extract_next_receive_event(self):
        state = self.their_state
        # We don't pause immediately when they enter DONE, because even in
        # DONE state we can still process a ConnectionClosed() event. But
        # if we have data in our buffer, then we definitely aren't getting
        # a ConnectionClosed() immediately and we need to pause.
        if state is DONE and self._receive_buffer:
            return PAUSED
        if state is MIGHT_SWITCH_PROTOCOL or state is SWITCHED_PROTOCOL:
            return PAUSED
        assert self._reader is not None
        event = self._reader(self._receive_buffer)
        if event is None:
            if not self._receive_buffer and self._receive_buffer_closed:
                # In some unusual cases (basically just HTTP/1.0 bodies), EOF
                # triggers an actual protocol event; in that case, we want to
                # return that event, and then the state will change and we'll
                # get called again to generate the actual ConnectionClosed().
                if hasattr(self._reader, "read_eof"):
                    event = self._reader.read_eof()
                else:
                    event = ConnectionClosed()
        if event is None:
            event = NEED_DATA
        return event

and _receive_buffer_closed seems to be closed only in

    def receive_data(self, data):
        if data:
            if self._receive_buffer_closed:
                raise RuntimeError("received close, then received more data?")
            self._receive_buffer += data
        else:
            self._receive_buffer_closed = True

meaning it was called with an empty data

in turn, this comes from httpcore:

    async def _receive_event(self, timeout: float = None) -> H11Event:
        while True:
            with map_exceptions({h11.RemoteProtocolError: RemoteProtocolError}):
                event = self._h11_state.next_event()

            if event is h11.NEED_DATA:
                data = await self._network_stream.read(
                    self.READ_NUM_BYTES, timeout=timeout
                )
                self._h11_state.receive_data(data)
            else:
                return event

so _network_stream.read returned empty data

as I'm using asyncio, it's an AsyncIOStream with this code:

    async def read(self, max_bytes: int, timeout: float = None) -> bytes:
        exc_map = {
            TimeoutError: ReadTimeout,
            anyio.BrokenResourceError: ReadError,
        }
        with map_exceptions(exc_map):
            with anyio.fail_after(timeout):
                try:
                    return await self._stream.receive(max_bytes=max_bytes)
                except anyio.EndOfStream:  # pragma: nocover
                    return b""

so I guess we reached EndOfStream on the client side then emitted the transition (their_role=SERVER, state=SEND_RESPONSE, event_type=ConnectionClosed) which is not managed.

When programming on the server side, it means client reached ConnectionClosed while the server is the sending the response/client is receiving, I think the bug does not happen since the server would not be calling receive_data while sending the response.

On the client side, it means the server reached ConnectionClosed while it is actually sending/while the client is receiving, and it trigger this bug.

@mm-matthias
Copy link

After upgrading from httpx 0.18.2 to 0.21.1 we also suffer from this problem again. As a workaround we pinned httpx to the old version where everything seems to work fine.

fyi @marns93

@zyv
Copy link

zyv commented Feb 1, 2022

@tomchristie so can this please be reopened? We can reproduce this again with thousands of connections against HTTP/2 server - Undertow.

@tomchristie
Copy link
Member

Can you make sure you've got the latest version of httpcore installed?

@lsamper
Copy link

lsamper commented May 16, 2022

Hi,
I have a similar issue with FastAPI and uvicorn. Python 3.8.

It's probably not the best place to post it, I post it here because it is thanks to this thread that I was able to create a toy program that reproduces the bug. Any help would be appreciated.

Thore are the step to reproduce it:

pip install fastapi
pip install uvicorn

server script (from @yeraydiazdiaz )

from fastapi import FastAPI

app = FastAPI()


@app.get("/")
@app.post("/")
def index():
    return {"ok": 1, "data": "welcome to test app 11111111111!"}


if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8001)

On the client:

$ curl --request POST   --url http://localhost:8001/

I have this Error that happens sometimes. It often happens multiple times in a row and after 3 to 10 queries it starts working.

I think it happens more ofter when the client is remote (through a VPN) but it also happens with the client on localhost.

WARNING:  Invalid HTTP request received.
Traceback (most recent call last):
  File "/home/lsamper/.local/share/virtualenvs/bug_http-V1yNRNQW/lib/python3.8/site-packages/uvicorn/protocols/http/h11_impl.py", line 129, in handle_events
  File "/home/lsamper/.local/share/virtualenvs/bug_http-V1yNRNQW/lib/python3.8/site-packages/h11/_connection.py", line 443, in next_event
    because the peer has finished their part of the current
  File "/home/lsamper/.local/share/virtualenvs/bug_http-V1yNRNQW/lib/python3.8/site-packages/h11/_util.py", line 76, in _reraise_as_remote_protocol_error
    # in-place modification preserved it and we can just re-raise:
  File "/home/lsamper/.local/share/virtualenvs/bug_http-V1yNRNQW/lib/python3.8/site-packages/h11/_connection.py", line 425, in next_event
    """Parse the next event out of our receive buffer, update our internal
  File "/home/lsamper/.local/share/virtualenvs/bug_http-V1yNRNQW/lib/python3.8/site-packages/h11/_connection.py", line 367, in _extract_next_receive_event
    other failures to read are indicated using other mechanisms
  File "/home/lsamper/.local/share/virtualenvs/bug_http-V1yNRNQW/lib/python3.8/site-packages/h11/_readers.py", line 72, in maybe_read_from_IDLE_client
  File "/home/lsamper/.local/share/virtualenvs/bug_http-V1yNRNQW/lib/python3.8/site-packages/h11/_util.py", line 88, in validate
    if not match:
h11._util.RemoteProtocolError: illegal request line: bytearray(b'HTTP/1.1 302 Found')
(bug_http-V1yNRNQW) lsamper@servername:~/bug_http$ pip freeze
anyio==3.6.1
asgiref==3.5.1
click==8.1.3
fastapi==0.78.0
h11==0.13.0
idna==3.3
pydantic==1.9.0
sniffio==1.2.0
starlette==0.19.1
typing-extensions==4.2.0
uvicorn==0.17.6

@fredi-python
Copy link

For me, updating my libraries fixed the issue : )

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working discussion external Root cause pending resolution in an external dependency
Projects
None yet