Skip to content

v1.18.0 breaks Server-Sent Events (SSE) real-time streaming - first chunk delayed by pre-read logic #266

@hcnode

Description

@hcnode

Bug Description:

Summary

@hono/node-server v1.18.0 introduced a regression that breaks real-time Server-Sent Events (SSE) streaming. The new pre-read logic delays the first chunk delivery, causing browsers to show "no response" and breaking the real-time nature of SSE connections.

Affected Versions

  • Working: v1.17.1 and earlier
  • Broken: v1.18.0 and later

Root Cause

The v1.18.0 release introduced a change described as "feat: always respond res.body" which added pre-read logic that attempts to read the first 2 chunks of a ReadableStream before sending headers and data to the client.

Problematic code in src/listener.ts (lines ~138-175):

if (res.body) {
  const reader = res.body.getReader()
  const values: Uint8Array[] = []
  
  // This pre-read logic breaks real-time SSE streaming
  for (let i = 0; i < 2; i++) {
    currentReadPromise = reader.read()
    const chunk = await readWithoutBlocking(currentReadPromise)
    // ... waits to read chunks before sending anything
  }
  
  // Headers/data only sent after pre-reading
  outgoing.writeHead(res.status, resHeaderRecord)
}

Impact

  1. Browser DevTools shows "no response": SSE connections appear stuck/pending
  2. Delayed first events: Users don't see SSE data until multiple chunks are buffered
  3. Breaks real-time applications: Chat apps, live updates, progress indicators, etc.
  4. Production issue: Affects any application using SSE with async data generation

Reproduction

// SSE endpoint that generates data asynchronously
app.get('/events', (c) => {
  const stream = new ReadableStream({
    async start(controller) {
      controller.enqueue('data: first event\n\n')
      
      // Any async operation (DB query, API call, etc.)
      await new Promise(resolve => setTimeout(resolve, 100))
      controller.enqueue('data: second event\n\n')
      controller.close()
    }
  })
  
  c.header('Content-Type', 'text/event-stream')
  return c.body(stream)
})

Expected: First event appears immediately in browser
Actual (v1.18.0): Browser shows "no response" until second event is ready

Suggested Fix

Add a fast-path for SSE responses that bypasses the pre-read logic:

// Fast-path for Server-Sent Events
const contentTypeHeader = resHeaderRecord["content-type"] as string | undefined
if (contentTypeHeader && /^text\/event-stream\b/i.test(contentTypeHeader) && res.body) {
  // Stream immediately without pre-reading
  outgoing.writeHead(res.status, resHeaderRecord)
  flushHeaders(outgoing)
  await writeFromReadableStream(res.body, outgoing)
  return
}

Environment

  • Node.js: Any version
  • Browser: All browsers (Chrome DevTools most visible)
  • Use case: Any SSE implementation with async data generation

This regression significantly impacts real-time web applications and should be prioritized for the next patch release.

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