fullmoon icon indicating copy to clipboard operation
fullmoon copied to clipboard

SSE Implementation in Fullmoon

Open stet opened this issue 10 months ago • 1 comments

I was testing Fullmoon and when I focused on SSE I had some issues.

Issue 1: fullmoon.streamContent("sse", {...}) Causes Type Error

When attempting to use the fullmoon.streamContent("sse", {...}) pattern for Server-Sent Events, an error occurs in fullmoon.lua:1463:

./.lua/fullmoon.lua:1463: bad argument #1 to ? (string expected)

The problematic code in fullmoon.lua around line 1463 appears to be:

local function streamWrap(func) return function(...) return coroutine.yield(func(...)()) or true end

This fails when passing "sse" as the first argument to streamContent().

Issue 2: Returning Empty String from Route Handler Closes SSE Connection

When implementing a manual workaround using coroutines, returning an empty string from the route handler causes Redbean to add a Content-Length: 0 header, which makes browsers immediately close the SSE connection.

Reproduction Steps

  1. Set up a Fullmoon route that attempts to use SSE:

fullmoon.setRoute("/api/sse", function(r) -- Set appropriate headers SetHeader("Content-Type", "text/event-stream") SetHeader("Cache-Control", "no-cache")

  -- Method 1: Using streamContent - causes error in fullmoon.lua:1463
  return fullmoon.streamContent("sse", {
      event = "ping",
      data = "test"
  })

  -- Method 2: Manual coroutine - adds Content-Length and closes connection
  local co = coroutine.create(function()
      Write("data: test\n\n")
      coroutine.yield()
  end)
  coroutine.resume(co)
  return ""

Current Workaround

I implemented a working solution, but it requires several non-obvious steps:

fullmoon.setRoute("/api/sse", function(r) -- Set headers SetHeader("Content-Type", "text/event-stream") SetHeader("Cache-Control", "no-cache")

  -- Create coroutine
  local co = coroutine.create(function()
      Write("data: test\n\n")
      coroutine.yield()
      -- More SSE code...
  

  -- CRITICAL: These two lines prevent Content-Length from being set
  SetStatus(200)
  SetHeader("Transfer-Encoding", "chunked")

  -- Start coroutine
  coroutine.resume(co)

  -- CRITICAL: Must return true, not a string
  return true

Expected Behavior

  1. fullmoon.streamContent("sse", {...}) should work correctly for SSE
  2. There should be a simpler way to maintain an SSE connection in Fullmoon

Suggested Solution

Add a dedicated SSE helper to Fullmoon that handles these details, something like:

fullmoon.serveSSE(function() -- Simple SSE implementation that handles coroutine, chunked encoding -- and proper response handling automatically Write("data: test\n\n") coroutine.yield()

Environment

  • Redbean version: 3.0.0
  • Fullmoon version: 0.384
  • Test browsers: Safari, Chrome

stet avatar Mar 03 '25 18:03 stet

@stet, I may be missing something in what you're trying to do, but you can't really return streamContent(), as it's most likely doesn't do what you need (not your issue for sure, as these aspects are not well documented). You can only return from the sse route when all the SSE-related work is done and you return in the same way as you return from all other routes (by executing a serve* request or returning true or an empty string). At that point all the work is already done and it's not possible in redbean to "re-enter" the handler again. That's why the streamContent method does this implicitly for you, allowing to send some content back, but still retaining control in the Lua handler until all the processing is done. See the example in examples/htmxsse.lua.

I will look into providing better error reporting for this case, although it doesn't seem to be immediately available.

pkulchenko avatar Mar 05 '25 03:03 pkulchenko