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

WebSocket silently blocks handler indefinitely #4877

Closed
kjander0 opened this issue Jul 27, 2020 · 5 comments
Closed

WebSocket silently blocks handler indefinitely #4877

kjander0 opened this issue Jul 27, 2020 · 5 comments
Labels

Comments

@kjander0
Copy link

kjander0 commented Jul 27, 2020

🐞 Describe the bug
I have a simple client/server using aiohttp WebSockets. If the client disconnects unexpectedly (link down, ctrl+C, etc), then the server WebSocket handler blocks indefinitely.

💡 To Reproduce
Server:

from aiohttp import web                                                                             
import asyncio                                                                                      
                                                                                                    
async def ws_handler(request):                                                                      
    print('handler begin')                                                                          
                                                                                                    
    ws = web.WebSocketResponse()                                                                    
    await ws.prepare(request)                                                                       
                                                                                                    
    for i in range(10):                                                                             
        # echo data back to client                                                                  
        data = await ws.receive_str()                                                               
        await ws.send_str(data)                                                                     
                                                                                                    
    print('handler done') # code not reached if link goes down                                              
                                                                                                    
    return ws                                                                                       
                                                                                                    
app = web.Application()                                                                             
app.add_routes([web.get('/ws', ws_handler)])                                                        
web.run_app(app)

Client:

import aiohttp                                                                                      
import asyncio                                                                                      
                                                                                                    
async def main():                                                                                   
    async with aiohttp.ClientSession() as client:                                                   
        async with client.ws_connect(f'http://localhost:8080/ws') as ws:                            
            for i in range(10):                                                                     
                await ws.send_str(f'stuff {i}')                                                     
                print(await ws.receive_str())                                                       
                await asyncio.sleep(1)                                                              
                                                                                                    
loop = asyncio.get_event_loop()                                                                     
loop.run_until_complete(main())

End the client early with ctrl+C. The server blocks waiting for the client.

💡 Expected behavior
I would expect an exception to be raised on the server as it waits for client data. Investigation shows that
BaseProtocol.connection_lost() callback is run, but does not result in an exception being raised.

📋 Your version of the Python
Python 3.7.7
aiohttp 3.6.2

📋 Your version of the aiohttp/yarl/multidict distributions

$ python -m pip show aiohttp
Name: aiohttp
Version: 3.6.2
Summary: Async http client/server framework (asyncio)
Home-page: https://github.com/aio-libs/aiohttp
Author: Nikolay Kim
Author-email: fafhrd91@gmail.com
License: Apache 2
Location: /home/kander/.local/lib/python3.7/site-packages
Requires: multidict, yarl, attrs, async-timeout, chardet
Required-by:

$ python -m pip show multidict
Name: multidict
Version: 4.7.5
Summary: multidict implementation
Home-page: https://github.com/aio-libs/multidict
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache 2
Location: /home/kander/.local/lib/python3.7/site-packages
Requires: 
Required-by: yarl, aiohttp

$ python -m pip show yarl
Name: yarl
Version: 1.4.2
Summary: Yet another URL library
Home-page: https://github.com/aio-libs/yarl/
Author: Andrew Svetlov
Author-email: andrew.svetlov@gmail.com
License: Apache 2
Location: /home/kander/.local/lib/python3.7/site-packages
Requires: multidict, idna
Required-by: aiohttp

📋 Additional context
Fedora 31

possibly related bug: #3122

@kjander0 kjander0 added the bug label Jul 27, 2020
@TBBle
Copy link
Contributor

TBBle commented Aug 1, 2020

I believe this is a known issue, and the API will probably be changed in 4.0 to raise an exception during sending. See #3391 (comment).

I'd suggest enabling heartbeats in your WebSocketResponse, so that the receive will be interrupted if the heartbeat times out, as without regular sending, a TCP socket may stay alive for a long time if the other end closed uncleanly.

Then you can terminate your loop. I don't remember off-hand if the heartbeat failure raises an exception from the receive, or if you need to then check ws.closed before sending, per #3391.

@kjander0
Copy link
Author

kjander0 commented Aug 3, 2020

I think the situation is a bit worse than with #3391. I have tried both heartbeats and testing for ws.closed before each ws command, but the handler ends up blocked still.

I can see that the handler is stalled on DataQueue.read() where it is waiting for it's self._waiter future to end. However, the result of this future is never set (even when the socket connection is detected as being lost). I'm guessing the connection_lost() callback should propagate an exception to the _waiter somehow.

@kjander0
Copy link
Author

workaround: I plan on using receive_timeout to eventually be notified of dead connection. Initially I didn't think receive timeout was raising an exception, but it looks like it does.

@derlih
Copy link
Contributor

derlih commented Oct 25, 2020

I wasn't able to reproduce the blocking handler behavior with the following test on 32833c3:

async def test_ws_not_blocks_on_disconnect(aiohttp_client) -> None:
    handler_finished = asyncio.Future()

    async def handler(request):
        try:
            ws = web.WebSocketResponse()
            await ws.prepare(request)
            for i in range(10):
                data = await ws.receive_str()
                await ws.send_str(data)

            return ws
        finally:
            handler_finished.set_result(None)

    app = web.Application()
    app.router.add_route("GET", "/", handler)

    client = await aiohttp_client(app)
    with pytest.raises(KeyboardInterrupt):
        async with client.ws_connect("/") as ws:
            for i in range(5):
                await ws.send_str(f"stuff {i}")
                print(await ws.receive_str())
            raise KeyboardInterrupt()

    assert handler_finished.done()

In the issue description the code should hit the line print('handler done')
but I assume that it shouldn't, because the exception will be thrown when the client is disconnected.
That's why in my test I set_result in finally block.

@Dreamsorcerer
Copy link
Member

Based on the above comments, I'll close this in favour of the linked issue.

@Dreamsorcerer Dreamsorcerer closed this as not planned Won't fix, can't repro, duplicate, stale Aug 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants