Server Sent Events / ReadableStream not working

Hey everyone! I’m building a SvelteKit site that needs to send a stream of logs to the client while it talks to an API. I opted to use SSE for this purpose, as that seemed most fitting.

Everything works correctly in dev mode locally, but when in production on Cloudflare Pages, the request for the ReadableStream endpoint closes immediately with status 200, while not doing anything or sending a response.

I’ve been furiously searching around for answers regarding Cloudflare and SSE, but nothing I’ve found yet has indicated other users having similar problems. Am I doing something wrong? Does Cloudflare Pages even support SSE?


Relevant parts of the codebase:

Update. Through a long process (see below), I found a fix for the issue.

  • Setting cache level to bypass for my entire /api/ namespace. Didn’t work :x:
  • Adding console.log()s to reveal crucial steps in the reading of the ReadableStream. The ReadableStream is returned, but returns undefined and immediately closes.
    • It appears that the api is working, but nothing is just received by the reader.
  • Removing the text decoder fromconst reader = res.body.pipeThrough(new TextDecoderStream()).getReader();. Didn’t work :x:
  • At this point, I assumed that Cloudflare must be closing the worker that’s pushing the SSE before it’s done. I found out about waitUntil and how to use it from SvelteKit. I implemented in a kind of sketchy way that I assumed should work still, but it Didn’t work :x:
    • In hindsight, it’s kind of obvious that this probably isn’t the issue, as the endpoint worked perfectly, it’s just that the logs weren’t being passed.
  • I tried adding the Connection: "keep-alive" and "Cache-Control": "no-cache" headers. Didn’t work :x:
  • Finally, I tried refactoring my backend code to not create a ReadableStream with a controller, but create a TransformStream and write to its WriteableStream while passing its ReadableStream to the client. This worked :white_check_mark:
    • There ended up being no need for waitUntil, so my code ended up pretty simple.

Simplified server code:

const { readable, writable } = new TransformStream();
const writer = writable.getWriter();

async function myBackendCode() {
      // other backend code here
      let myValue = 123
      writer.write(new TextEncoder().encode("This is a new log line that will be sent to the client. "+myvalue+' \n')
}
myBackendCode() // if you use await here, everything will be sent to the client at once

return new Response(readable, {
    headers: {
        "Content-Type": "text/event-stream",
        Connection: "keep-alive",
        "Cache-Control": "no-cache"
    }
});

Simplified client code:

const res = await fetch("/api/endpoint")
const reader = res.body.pipeThrough(new TextDecoderStream()).getReader();
// eslint-disable-next-line no-constant-condition
while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    console.log("received: "+value);
}

It’s honestly a miracle I had the willpower to push through this and try doing every possible fix. If I ran into this issue again I’m not sure if I would find the debugging worthwhile, I could probably refactor the code such that more of the logic would be on the client and there wouldn’t be any such SSE involved. With what I’ve read about SSE too, I’m not sure if I would reach for it again, even though it is a very useful technology.

And I know I’m here doing a public service on the Cloudflare forums for the one person Googling this problem, but I’m honestly not sure if this is even related to Cloudflare, or if the technology is just brittle. The stream APIs are kind of annoying anyway, but the helper functions I wrote for my project make it a bit more straightforward to use.

This topic was automatically closed 2 days after the last reply. New replies are no longer allowed.