pythonfastapiserver-sent-eventsstarlettequart

Quart: how to get Server Sent Events (SSE) working?


I'm trying to implement an endpoint for Server Sent Events (SSE) in Quart, following the example from the official documentation: http://pgjones.gitlab.io/quart/how_to_guides/server_sent_events.html

I've copy-pasted the code and used some dummy string for data. This locked up my server, because it would endlessly stream my 'data'. So it's not quite a working example, you still have to figure out how to properly add events to the stream.

Now I've set up a proper queue using asyncio.queues and I can see the send_events() function now responding when I add something. Great!

The only problem is I'm not getting any output when calling the endpoint (with Postman). It keeps waiting for a response. If I stop the server mid-flight, I do get the SSE output generated up to that point. So the events themselves are triggered and properly formed, it's the output that isn't streamed like it should be.

I found this example that has the same issue: https://github.com/encode/starlette/issues/20#issuecomment-586169195. However, this discussion goes in a different direction and ultimately an implementation for Starlette is created. I've tried this package (I'm using FastAPI as well) but the return for the SSE endpoint is EventSourceResponse and then I get the error:

TypeError: The response value type (EventSourceResponse) is not valid

Right... so Quart doesn't like the response value. I see no way of making that work with Quart, and since the example from the Quart docs doesn't work, it looks like the only option is to ditch Quart.

Or is there another solution?

Code

I've got a dataclass with only event and data properties:

@dataclass
class ServerSentEvent:
    data: str
    event: str

    def encode(self) -> bytes:
        message = f'data: {self.data}'
        message = f'{message}\nevent: {self.event}'
        message = f'{message}\r\n\r\n'
        return message.encode('utf-8')

And then the endpoint - note I've included some print lines and add something to the queue initially to test (elsewhere in the code events would be added to the queue in the same way):

queues = []

@bp.get('/sse')
async def sse():
    """Server Sent Events"""

    if 'text/event-stream' not in request.accept_mimetypes:
        abort(400)

    async def send_events():
        while True:
            print('waiting for event')
            event = await queue.get()
            print('got event')
            print(event.encode())
            yield event.encode()

    queue = Queue()
    queues.append(queue)

    await queue.put(ServerSentEvent(**{'event': 'subscribed', 'data': 'will send new events'}))
    print('-- SSE connected')

    response = await make_response(
        send_events(),
        {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Transfer-Encoding': 'chunked',
            'Connection': 'keep-alive'
        }
    )
    response.timeout = None
    return response

Solution

  • Since the code itself didn't seem to be the problem, I looked more closely at the Hypercorn and nginx servers. I came across a post on Serverfault about nginx possibly buffering the output, thus preventing the SSE output from streaming: https://serverfault.com/a/801629

    The linked answer goes more in-depth about what exactly happens and the possible solutions, so I will refrain from repeating that and mention the solution that also solved my problem. It's adding the header:

    X-Accel-Buffering: no
    

    ...to the response.

    For my code, updating:

    response = await make_response(
        send_events(),
        {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Transfer-Encoding': 'chunked',
            'Connection': 'keep-alive'
        }
    )
    

    to:

    response = await make_response(
        send_events(),
        {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Transfer-Encoding': 'chunked',
            'Connection': 'keep-alive',
            'X-Accel-Buffering': 'no'
        }
    )
    

    Thanks to @pgjones for providing suggestions to help me resolve this!