Skip to content

sse() helper does not work on Deno — ReadableStream enqueues raw strings instead of Uint8Array #1845

@xpr0gamers

Description

@xpr0gamers

Description

The sse() helper combined with the generator pattern does not work on Deno. The SSE response returns 200 OK with Content-Type: text/event-stream, but no events are ever transmitted to the client. The EventStream tab in browser DevTools is completely empty.

The same code works correctly on Bun.

Root Cause

In adapter/utils.js, the createStreamHandler enqueues SSE data as raw strings into the ReadableStream:

// adapter/utils.js:168-169 (start callback)
if (init.value.toSSE)
    controller.enqueue(init.value.toSSE());  // toSSE() returns a string

// adapter/utils.js:201-202 (pull callback)
if (chunk.toSSE)
    controller.enqueue(chunk.toSSE());  // same issue

The toSSE() method (in utils.js) returns a plain string like "event: progress\ndata: {...}\n\n". This string is passed directly to controller.enqueue() without encoding.

A ReadableStream used as a Response body should contain Uint8Array chunks per the web standard. Bun is lenient and auto-encodes string chunks to bytes. Deno follows the spec strictly — the raw strings are silently not transmitted.

Reproduction

import { Elysia, sse } from 'elysia';

new Elysia()
    .get('/sse', async function* () {
        yield sse({ event: 'ping', data: { message: 'hello' } });
        yield sse({ event: 'ping', data: { message: 'world' } });
    })
    .listen(3000);
  1. Run with deno run --allow-net server.ts
  2. Open browser to http://localhost:3000/sse or use new EventSource('/sse')
  3. Expected: Two ping events received
  4. Actual: Response is 200 with text/event-stream content-type, but EventStream tab is empty. No events arrive. The EventSource error event fires due to connection close.

Works correctly when running with Bun.

Suggested Fix

Encode the string to Uint8Array before enqueuing in adapter/utils.js:

const encoder = new TextEncoder();

// In start callback:
if (init.value.toSSE)
    controller.enqueue(encoder.encode(init.value.toSSE()));

// In pull callback:
if (chunk.toSSE)
    controller.enqueue(encoder.encode(chunk.toSSE()));

Workaround

Return a manual Response with a self-built ReadableStream instead of using the sse() + generator pattern:

.get('/sse', () => {
    const encoder = new TextEncoder();
    const stream = new ReadableStream({
        async start(controller) {
            // ... produce SSE data ...
            const sseString = `event: ping\ndata: ${JSON.stringify({ message: 'hello' })}\n\n`;
            controller.enqueue(encoder.encode(sseString));
            controller.close();
        },
    });
    return new Response(stream, {
        headers: {
            'Content-Type': 'text/event-stream',
            'Cache-Control': 'no-cache',
            'Connection': 'keep-alive',
        },
    });
})

Environment

  • Elysia: 1.4.28
  • Deno: 2.x
  • OS: macOS (Darwin arm64)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions