Fix: h3 Invalid Stream Provided in Nuxt

Error message:
Invalid stream provided to sendStream/sendWebResponse.
ssr 2025-01-25

What Causes This Error?

This error occurs in Nuxt server routes (powered by H3/Nitro) when you try to send an invalid stream as a response. The stream must be a valid ReadableStream or Node.js stream.

The Problem

// server/api/stream.ts
export default defineEventHandler((event) => {
  // ❌ Wrong - not a valid stream
  return sendStream(event, { data: 'not a stream' })

  // ❌ Wrong - null/undefined
  return sendStream(event, null)

  // ❌ Wrong - already consumed stream
  const stream = getBodyStream()
  await stream.read()  // Consumed!
  return sendStream(event, stream)  // Error!
})

The Fix

Return Valid ReadableStream

// server/api/stream.ts
export default defineEventHandler((event) => {
  // ✅ Correct - Web ReadableStream
  const stream = new ReadableStream({
    start(controller) {
      controller.enqueue('Hello ')
      controller.enqueue('World')
      controller.close()
    }
  })

  return sendStream(event, stream)
})

Return Node.js Stream

// server/api/file.ts
import { createReadStream } from 'fs'

export default defineEventHandler((event) => {
  // ✅ Correct - Node.js readable stream
  const stream = createReadStream('/path/to/file.txt')
  return sendStream(event, stream)
})

Common Patterns

File Download

// server/api/download.ts
import { createReadStream, statSync } from 'fs'
import { join } from 'path'

export default defineEventHandler((event) => {
  const filePath = join(process.cwd(), 'files', 'document.pdf')

  // Set headers
  setHeader(event, 'Content-Type', 'application/pdf')
  setHeader(event, 'Content-Disposition', 'attachment; filename="document.pdf"')

  // Get file size for Content-Length
  const stat = statSync(filePath)
  setHeader(event, 'Content-Length', stat.size)

  // ✅ Stream the file
  return sendStream(event, createReadStream(filePath))
})

Proxy Stream

// server/api/proxy.ts
export default defineEventHandler(async (event) => {
  const response = await fetch('https://api.example.com/large-file')

  if (!response.body) {
    throw createError({ statusCode: 500, message: 'No response body' })
  }

  // ✅ Forward the stream
  setHeader(event, 'Content-Type', response.headers.get('content-type') || '')
  return sendStream(event, response.body)
})

Server-Sent Events (SSE)

// server/api/events.ts
export default defineEventHandler((event) => {
  setHeader(event, 'Content-Type', 'text/event-stream')
  setHeader(event, 'Cache-Control', 'no-cache')
  setHeader(event, 'Connection', 'keep-alive')

  // ✅ Create SSE stream
  const stream = new ReadableStream({
    start(controller) {
      const encoder = new TextEncoder()

      let count = 0
      const interval = setInterval(() => {
        count++
        controller.enqueue(encoder.encode(`data: Event ${count}\n\n`))

        if (count >= 10) {
          clearInterval(interval)
          controller.close()
        }
      }, 1000)
    }
  })

  return sendStream(event, stream)
})

Transform Stream

// server/api/transform.ts
export default defineEventHandler(async (event) => {
  const body = await readBody(event)

  // ✅ Create transform stream
  const stream = new ReadableStream({
    start(controller) {
      const encoder = new TextEncoder()
      const transformed = body.text.toUpperCase()
      controller.enqueue(encoder.encode(transformed))
      controller.close()
    }
  })

  return sendStream(event, stream)
})

Error Handling

// server/api/safe-stream.ts
export default defineEventHandler(async (event) => {
  try {
    const response = await fetch('https://api.example.com/data')

    if (!response.ok) {
      throw createError({
        statusCode: response.status,
        message: 'Upstream error'
      })
    }

    if (!response.body) {
      throw createError({
        statusCode: 500,
        message: 'No stream available'
      })
    }

    return sendStream(event, response.body)
  } catch (error) {
    // Handle stream errors
    throw createError({
      statusCode: 500,
      message: 'Stream error: ' + error.message
    })
  }
})

Checking Stream Validity

// server/api/validate-stream.ts
export default defineEventHandler(async (event) => {
  const stream = getStreamFromSomewhere()

  // ✅ Validate before sending
  if (!stream) {
    throw createError({ statusCode: 500, message: 'No stream' })
  }

  if (typeof stream.getReader !== 'function' &&
      typeof stream.pipe !== 'function') {
    throw createError({ statusCode: 500, message: 'Invalid stream type' })
  }

  return sendStream(event, stream)
})

Alternative: sendWebResponse

// server/api/response.ts
export default defineEventHandler(async (event) => {
  const stream = new ReadableStream({
    start(controller) {
      controller.enqueue(new TextEncoder().encode('Hello'))
      controller.close()
    }
  })

  // ✅ Alternative using sendWebResponse
  const response = new Response(stream, {
    headers: { 'Content-Type': 'text/plain' }
  })

  return sendWebResponse(event, response)
})

Quick Checklist

  • Stream is a valid ReadableStream or Node.js stream
  • Stream hasn’t been consumed/closed
  • Stream is not null or undefined
  • Set appropriate Content-Type header
  • Handle errors in stream creation
  • Consider using sendWebResponse for complex responses