question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

HttpToolsProtocol: receive() hangs due to signalling mixup between request cycles

See original GitHub issue

I have a FastAPI server running with uvicorn. Starting with starlette 0.13.7 I’m hitting a race condition on a specific resource limited setup (RPi), however if my analysis is correct the root cause is a bug in uvicorn’s protocol.http.httptools_impl.

The race condition is causing starlette to keep receive()ing on a RequestResponseCycle for a bit after the response is fully sent, while the client sends a new request on the same connection:

  1. Cycle 1 is blocking on message_event in RequestResponseCycle.receive() (called before response_complete is set)
  2. HttpToolsProtocol reads the body of the new request into Cycle 2 and sets message_event
  3. Cycle 1 clears message_event and returns
  4. Receiving on Cycle 2 blocks because message_event is already cleared

If I’m not mistaken, the fix is pretty simple:

  1. Create a separate event for each cycle.
  2. In RequestResponseCycle.send, when setting response_complete also set message_event to kill any lingering receive()s

Minimal test case

This raw ASGI server imitates the behavior in Starlette that triggers the bug.

import asyncio

async def wait_for_disconnect(receive):
    while True:
        p = await receive()
        if p['type'] == 'http.disconnect':
            print('Disconnected!')
            break
           
async def app(scope, receive, send):
    await asyncio.sleep(0.2)
    m = await receive()

    if m['type'] == 'lifespan.startup':
        await send({'type': 'lifespan.startup.complete'})
    elif m['type'] == 'http.request':
        if scope['path'] == '/foo':
            asyncio.create_task(wait_for_disconnect(receive))
            await asyncio.sleep(0.2)

        await send({'type': 'http.response.start', 'status': 404})
        await send({'type': 'http.response.body', 'body': b'Not found!\n'})

Test with curl localhost:8000/foo localhost:8000/bar. You should see that it hangs at the request to /bar.

The /foo request calls receive() in the background, and this call is still running in the background when the request to /bar arrives. The http.request message for the /bar request incorrectly wakes up the background (/foo) receive, causing the receive() for /bar to hang forever, as there are no more events for this request.

Issue Analytics

  • State:closed
  • Created 3 years ago
  • Comments:8 (4 by maintainers)

github_iconTop GitHub Comments

1reaction
itayperlcommented, Oct 12, 2020

@euri10, I’m sorry if my description was a bit unclear, but this is a bug in uvicorn that should be fixed regardless of Starlette. It just didn’t manifest (in my usecase at least) with older versions of Starlette. Any code working with multiple httptools_impl.RequestResponseCycle objects in parallel can potentially hit this bug.

0reactions
euri10commented, Nov 12, 2020

@euri10, were you able to reproduce the bug using the new test case?

I can yes, this helped a lot thanks

Read more comments on GitHub >

github_iconTop Results From Across the Web

Praxis - OSCHINA - 中文开源技术交流社区
Emoji-Tools - Multiple useful tools to help Android and iOS/OSX developers with creating and modifying Emoji Font files. Tehreer-Android - Library that gives ......
Read more >
Notebooks
Telecom is an enormous market due to the ubiquity of telephones in\nwork and personal ... to voicemail,\nyou can choose from 2, 3, 4,...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found