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

Can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE #1637

Closed
Yangruipis opened this issue Sep 8, 2022 · 6 comments

Comments

@Yangruipis
Copy link

Yangruipis commented Sep 8, 2022

Related to #111
#1636 may be a solution

Reproducing

1. server code

import asyncio
from fastapi import FastAPI, Request

app = FastAPI()

# !!!middleware is required to reproduce this!!!
@app.middleware("http")
async def add_process_time_header(request: Request, call_next):
    response = await call_next(request)
    return response

@app.post(
    "/test",
)
async def test(query: dict) -> dict:
    await asyncio.sleep(1)
    return {"foo": "bar"}

2. locust client

# client.py
from locust import HttpUser, task

class WebsiteUser(HttpUser):
    @task
    def inference(self):
        rtn = self.client.post(
            "/test",
            headers={"accept": "application/json"},
            json={"query": "test"},
        )

3. call

locust -f client.py  -u 10 -r 10 --headless -t 10min -H http://localhost:8000

It will take 30s to reproduce this issue

Traceback logs

Exception in callback H11Protocol.timeout_keep_alive_handler()
handle: <TimerHandle when=3544453.523044797 H11Protocol.timeout_keep_alive_handler()>
Traceback (most recent call last):
  File "/home/r.yang/miniconda3/envs/py37/lib/python3.7/asyncio/events.py", line 88, in _run
    self._context.run(self._callback, *self._args)
  File "/home/r.yang/miniconda3/envs/py37/lib/python3.7/site-packages/uvicorn/protocols/http/h11_impl.py", line 360, in timeout_keep_alive_handler
    self.conn.send(event)
  File "/home/r.yang/miniconda3/envs/py37/lib/python3.7/site-packages/h11/_connection.py", line 510, in send
    data_list = self.send_with_data_passthrough(event)
  File "/home/r.yang/miniconda3/envs/py37/lib/python3.7/site-packages/h11/_connection.py", line 535, in send_with_data_passthrough
    self._process_event(self.our_role, event)
  File "/home/r.yang/miniconda3/envs/py37/lib/python3.7/site-packages/h11/_connection.py", line 272, in _process_event
    self._cstate.process_event(role, type(event), server_switch_event)
  File "/home/r.yang/miniconda3/envs/py37/lib/python3.7/site-packages/h11/_state.py", line 289, in process_event
    self._fire_event_triggered_transitions(role, _event_type)
  File "/home/r.yang/miniconda3/envs/py37/lib/python3.7/site-packages/h11/_state.py", line 311, in _fire_event_triggered_transitions
    ) from None
h11._util.LocalProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE

Important

  • We're using Polar.sh so you can upvote and help fund this issue.
  • We receive the funding once the issue is completed & confirmed by you.
  • Thank you in advance for helping prioritize & fund our backlog.
Fund with Polar
@Yangruipis
Copy link
Author

>>> here is more information <<<

how to start the server

uvicorn app:app --reload --host 0.0.0.0 --timeout-keep-alive 1

versions

python==3.7
uvicorn==0.18.3
fastapi==0.81.0
h11==0.13.0
starlette==0.19.1

@Kludex
Copy link
Sponsor Member

Kludex commented Oct 30, 2022

I cannot reproduce it. 👀

@liranbg
Copy link

liranbg commented Nov 20, 2022

For other people encountering such issue - This might be caused when your server has CPU starvation.

So each client, making a request, expect a keep alive that is configured by uvicorn to 5 by default. That keepalive, if the server process is starved, wont happen on time and thus, client will bail the connection with an execute of "RemoteDisconnect" while the server will show a traceback as mentioned on the PR description.

The solution here, would be to allow more CPU for the process or find the bottleneck on the server process to allow it sending the keepalive on time. (or, course, increase the default keepalive on the server from 5 to whatever is needed, but i guess, that would mask other issues)

marcinsulikowski added a commit to marcinsulikowski/uvicorn that referenced this issue Feb 6, 2024
…ests

When uvicorn receives a request very soon after it sent the previous
response, or when it receives pipelined requests, the second request may
spuriously fail if the ASGI app takes more time to process it than
uvicorn's `timeout_keep_alive`. This happens because uvicorn arms the
keep-alive timer as the last step of handling the first request, which
is after the second request was already received from the network (which
normally is the only place where uvicorn disarms the keep-alive timer).
In such cases the timer may fire while uvicorn is still waiting for
a response from the ASGI app:

    Exception in callback H11Protocol.timeout_keep_alive_handler()
    handle: <TimerHandle when=335245.867740771 H11Protocol.timeout_keep_alive_handler()>
    Traceback (most recent call last):
      File "/usr/lib/python3.12/asyncio/events.py", line 84, in _run
        self._context.run(self._callback, *self._args)
      File "venv/lib/python3.12/site-packages/uvicorn/protocols/http/h11_impl.py", line 363, in timeout_keep_alive_handler
        self.conn.send(event)
      File "venv/lib/python3.12/site-packages/h11/_connection.py", line 512, in send
        data_list = self.send_with_data_passthrough(event)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "venv/lib/python3.12/site-packages/h11/_connection.py", line 537, in send_with_data_passthrough
        self._process_event(self.our_role, event)
      File "venv/lib/python3.12/site-packages/h11/_connection.py", line 272, in _process_event
        self._cstate.process_event(role, type(event), server_switch_event)
      File "venv/lib/python3.12/site-packages/h11/_state.py", line 293, in process_event
        self._fire_event_triggered_transitions(role, _event_type)
      File "venv/lib/python3.12/site-packages/h11/_state.py", line 311, in _fire_event_triggered_transitions
        raise LocalProtocolError(
    h11._util.LocalProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE

When this happens, the connection is left in the `ERROR` state and when
a response to the second request is ready to be sent, this fails:

    ERROR:    Exception in ASGI application
    Traceback (most recent call last):
      File "venv/lib/python3.12/site-packages/uvicorn/protocols/http/h11_impl.py", line 408, in run_asgi
        result = await app(  # type: ignore[func-returns-value]
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "venv/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py", line 84, in __call__
        return await self.app(scope, receive, send)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    (...)
      File "venv/lib/python3.12/site-packages/uvicorn/protocols/http/h11_impl.py", line 490, in send
        output = self.conn.send(event=response)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "venv/lib/python3.12/site-packages/h11/_connection.py", line 512, in send
        data_list = self.send_with_data_passthrough(event)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "venv/lib/python3.12/site-packages/h11/_connection.py", line 527, in send_with_data_passthrough
        raise LocalProtocolError("Can't send data when our state is ERROR")
    h11._util.LocalProtocolError: Can't send data when our state is ERROR

We fix this by ensuring that the keep-alive timer is disarmed before
starting to process a pipelined request.

Closes encode#1637
marcinsulikowski added a commit to marcinsulikowski/uvicorn that referenced this issue Feb 6, 2024
…ests

When uvicorn receives a request very soon after it sent the previous
response, or when it receives pipelined requests, the second request may
spuriously fail if the ASGI app takes more time to process it than
uvicorn's `timeout_keep_alive`. This happens because uvicorn arms the
keep-alive timer as the last step of handling the first request, which
is after the second request was already received from the network (which
normally is the only place where uvicorn disarms the keep-alive timer).
In such cases the timer may fire while uvicorn is still waiting for
a response from the ASGI app:

    Exception in callback H11Protocol.timeout_keep_alive_handler()
    handle: <TimerHandle when=335245.867740771 H11Protocol.timeout_keep_alive_handler()>
    Traceback (most recent call last):
      File "/usr/lib/python3.12/asyncio/events.py", line 84, in _run
        self._context.run(self._callback, *self._args)
      File "venv/lib/python3.12/site-packages/uvicorn/protocols/http/h11_impl.py", line 363, in timeout_keep_alive_handler
        self.conn.send(event)
      File "venv/lib/python3.12/site-packages/h11/_connection.py", line 512, in send
        data_list = self.send_with_data_passthrough(event)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "venv/lib/python3.12/site-packages/h11/_connection.py", line 537, in send_with_data_passthrough
        self._process_event(self.our_role, event)
      File "venv/lib/python3.12/site-packages/h11/_connection.py", line 272, in _process_event
        self._cstate.process_event(role, type(event), server_switch_event)
      File "venv/lib/python3.12/site-packages/h11/_state.py", line 293, in process_event
        self._fire_event_triggered_transitions(role, _event_type)
      File "venv/lib/python3.12/site-packages/h11/_state.py", line 311, in _fire_event_triggered_transitions
        raise LocalProtocolError(
    h11._util.LocalProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE

When this happens, the connection is left in the `ERROR` state and when
a response to the second request is ready to be sent, this fails:

    ERROR:    Exception in ASGI application
    Traceback (most recent call last):
      File "venv/lib/python3.12/site-packages/uvicorn/protocols/http/h11_impl.py", line 408, in run_asgi
        result = await app(  # type: ignore[func-returns-value]
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "venv/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py", line 84, in __call__
        return await self.app(scope, receive, send)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    (...)
      File "venv/lib/python3.12/site-packages/uvicorn/protocols/http/h11_impl.py", line 490, in send
        output = self.conn.send(event=response)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "venv/lib/python3.12/site-packages/h11/_connection.py", line 512, in send
        data_list = self.send_with_data_passthrough(event)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "venv/lib/python3.12/site-packages/h11/_connection.py", line 527, in send_with_data_passthrough
        raise LocalProtocolError("Can't send data when our state is ERROR")
    h11._util.LocalProtocolError: Can't send data when our state is ERROR

We fix this by ensuring that the keep-alive timer is disarmed before
starting to process a pipelined request.

Closes encode#1637
@marcinsulikowski
Copy link
Contributor

marcinsulikowski commented Feb 6, 2024

@Yangruipis, @Kludex, I am hitting this error very frequently in a quite complex application I am working on. After looking at the problem a bit closer, it happens when the server receives a request very fast after sending a response for the previous one on the same connection, before it manages to call H11Protocol.on_response_complete. When this happens, H11Protocol.on_response_complete arms the keep-alive timer and then starts processing the second request, without disarming that timer first (it is normally only disarmed in H11Protocol.data_received).

It could not create a simple and reliable reproducer of the scenario above, but the same problem can be very reliably reproduced in another way: by pipelining two requests, when the second request takes longer than timeout_keep_alive to be processed. Here's a standalone Python script that shows the problem:

import asyncio

import uvicorn
from fastapi import FastAPI

app = FastAPI()


@app.get("/")
async def root():
    await asyncio.sleep(3)
    return {"msg": "Hello World"}


async def main():
    # Start uvicorn in a background task
    config = uvicorn.Config(app, port=8000, timeout_keep_alive=1)
    server = uvicorn.Server(config)
    uvicorn_task = asyncio.create_task(server.serve())

    # After it starts, try making two HTTP requests.
    await asyncio.sleep(1)
    print("Sending requests")
    reader, writer = await asyncio.open_connection("localhost", 8000)
    writer.write(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n")
    writer.write(b"GET / HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n")
    await writer.drain()
    while data := await reader.read(100):
        print(data.decode("utf-8"))
    print("Server closed the connection")
    server.should_exit = True
    await uvicorn_task


if __name__ == "__main__":
    asyncio.run(main())

The script should print two {"msg": "Hello World"}, wait a second and shut down. However, instead it does this:

  • It gets the first response and prints it.
  • A second after, while processing the second pipelined request, uvicorn tries to close the connection; this fails because its state is SEND_RESPONSE and leaves that connection in the ERROR state:
Exception in callback H11Protocol.timeout_keep_alive_handler()
handle: <TimerHandle when=335245.867740771 H11Protocol.timeout_keep_alive_handler()>
Traceback (most recent call last):
  File "/usr/lib/python3.12/asyncio/events.py", line 84, in _run
    self._context.run(self._callback, *self._args)
  File "venv/lib/python3.12/site-packages/uvicorn/protocols/http/h11_impl.py", line 363, in timeout_keep_alive_handler
    self.conn.send(event)
  File "venv/lib/python3.12/site-packages/h11/_connection.py", line 512, in send
    data_list = self.send_with_data_passthrough(event)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "venv/lib/python3.12/site-packages/h11/_connection.py", line 537, in send_with_data_passthrough
    self._process_event(self.our_role, event)
  File "venv/lib/python3.12/site-packages/h11/_connection.py", line 272, in _process_event
    self._cstate.process_event(role, type(event), server_switch_event)
  File "venv/lib/python3.12/site-packages/h11/_state.py", line 293, in process_event
    self._fire_event_triggered_transitions(role, _event_type)
  File "venv/lib/python3.12/site-packages/h11/_state.py", line 311, in _fire_event_triggered_transitions
    raise LocalProtocolError(
h11._util.LocalProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE
  • When the response is generated by the ASGI app and is ready to be sent, uvicorn tries to send it but because the state of the connection is ERROR, it simply disconnects:
INFO:     127.0.0.1:47416 - "GET / HTTP/1.1" 200 OK
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "venv/lib/python3.12/site-packages/uvicorn/protocols/http/h11_impl.py", line 408, in run_asgi
    result = await app(  # type: ignore[func-returns-value]
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "venv/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py", line 84, in __call__
    return await self.app(scope, receive, send)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "venv/lib/python3.12/site-packages/fastapi/applications.py", line 1106, in __call__
    await super().__call__(scope, receive, send)
  File "venv/lib/python3.12/site-packages/starlette/applications.py", line 122, in __call__
    await self.middleware_stack(scope, receive, send)
  File "venv/lib/python3.12/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "venv/lib/python3.12/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "venv/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 79, in __call__
    raise exc
  File "venv/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 68, in __call__
    await self.app(scope, receive, sender)
  File "venv/lib/python3.12/site-packages/fastapi/middleware/asyncexitstack.py", line 20, in __call__
    raise e
  File "venv/lib/python3.12/site-packages/fastapi/middleware/asyncexitstack.py", line 17, in __call__
    await self.app(scope, receive, send)
  File "venv/lib/python3.12/site-packages/starlette/routing.py", line 718, in __call__
    await route.handle(scope, receive, send)
  File "venv/lib/python3.12/site-packages/starlette/routing.py", line 276, in handle
    await self.app(scope, receive, send)
  File "venv/lib/python3.12/site-packages/starlette/routing.py", line 69, in app
    await response(scope, receive, send)
  File "venv/lib/python3.12/site-packages/starlette/responses.py", line 164, in __call__
    await send(
  File "venv/lib/python3.12/site-packages/starlette/middleware/exceptions.py", line 65, in sender
    await send(message)
  File "venv/lib/python3.12/site-packages/starlette/middleware/errors.py", line 159, in _send
    await send(message)
  File "venv/lib/python3.12/site-packages/uvicorn/protocols/http/h11_impl.py", line 490, in send
    output = self.conn.send(event=response)
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "venv/lib/python3.12/site-packages/h11/_connection.py", line 512, in send
    data_list = self.send_with_data_passthrough(event)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "venv/lib/python3.12/site-packages/h11/_connection.py", line 527, in send_with_data_passthrough
    raise LocalProtocolError("Can't send data when our state is ERROR")
h11._util.LocalProtocolError: Can't send data when our state is ERROR

See PR #2243 for a proposal which fixes the problem.

marcinsulikowski added a commit to marcinsulikowski/uvicorn that referenced this issue Feb 6, 2024
…ests

When uvicorn receives a request very soon after it sent the previous
response, or when it receives pipelined requests, the second request may
spuriously fail if the ASGI app takes more time to process it than
uvicorn's `timeout_keep_alive`. This happens because uvicorn arms the
keep-alive timer as the last step of handling the first request, which
is after the second request was already received from the network (which
normally is the only place where uvicorn disarms the keep-alive timer).
In such cases the timer may fire while uvicorn is still waiting for
a response from the ASGI app:

    Exception in callback H11Protocol.timeout_keep_alive_handler()
    handle: <TimerHandle when=335245.867740771 H11Protocol.timeout_keep_alive_handler()>
    Traceback (most recent call last):
      File "/usr/lib/python3.12/asyncio/events.py", line 84, in _run
        self._context.run(self._callback, *self._args)
      File "venv/lib/python3.12/site-packages/uvicorn/protocols/http/h11_impl.py", line 363, in timeout_keep_alive_handler
        self.conn.send(event)
      File "venv/lib/python3.12/site-packages/h11/_connection.py", line 512, in send
        data_list = self.send_with_data_passthrough(event)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "venv/lib/python3.12/site-packages/h11/_connection.py", line 537, in send_with_data_passthrough
        self._process_event(self.our_role, event)
      File "venv/lib/python3.12/site-packages/h11/_connection.py", line 272, in _process_event
        self._cstate.process_event(role, type(event), server_switch_event)
      File "venv/lib/python3.12/site-packages/h11/_state.py", line 293, in process_event
        self._fire_event_triggered_transitions(role, _event_type)
      File "venv/lib/python3.12/site-packages/h11/_state.py", line 311, in _fire_event_triggered_transitions
        raise LocalProtocolError(
    h11._util.LocalProtocolError: can't handle event type ConnectionClosed when role=SERVER and state=SEND_RESPONSE

When this happens, the connection is left in the `ERROR` state and when
a response to the second request is ready to be sent, this fails:

    ERROR:    Exception in ASGI application
    Traceback (most recent call last):
      File "venv/lib/python3.12/site-packages/uvicorn/protocols/http/h11_impl.py", line 408, in run_asgi
        result = await app(  # type: ignore[func-returns-value]
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "venv/lib/python3.12/site-packages/uvicorn/middleware/proxy_headers.py", line 84, in __call__
        return await self.app(scope, receive, send)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    (...)
      File "venv/lib/python3.12/site-packages/uvicorn/protocols/http/h11_impl.py", line 490, in send
        output = self.conn.send(event=response)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "venv/lib/python3.12/site-packages/h11/_connection.py", line 512, in send
        data_list = self.send_with_data_passthrough(event)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
      File "venv/lib/python3.12/site-packages/h11/_connection.py", line 527, in send_with_data_passthrough
        raise LocalProtocolError("Can't send data when our state is ERROR")
    h11._util.LocalProtocolError: Can't send data when our state is ERROR

We fix this by ensuring that the keep-alive timer is disarmed before
starting to process a pipelined request.

Closes encode#1637
@Kludex Kludex reopened this Feb 6, 2024
@Kludex
Copy link
Sponsor Member

Kludex commented Feb 6, 2024

I've reopened. I'll check this in some days. Thanks.

@Kludex
Copy link
Sponsor Member

Kludex commented Feb 10, 2024

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

No branches or pull requests

4 participants