moeru-ai/airi · error · Error

Streaming transcription response is missing a readable body.

Error message

Streaming transcription response is missing a readable body.

What it means

Thrown by the shared streaming-transcription helper when the upload POST returned 2xx but `response.body` is null, so there is no SSE stream to pipe through `createSSETransformer()`. The response must be a streaming body — a body-less success means the transport contract for server-sent events is broken before any transcript delta can arrive.

Solutions

  1. Confirm the backend really streams SSE for this endpoint (curl -N and watch for events).
  2. Bypass or reconfigure proxies/CDNs for the transcription route (disable buffering, preserve transfer-encoding).
  3. In tests, make fetch stubs return a real `ReadableStream` body.
  4. If the environment cannot stream, use a buffered (non-streaming) transcription call instead of this helper.

Example fix

// before
if (!response.body)
  throw new Error('Streaming transcription response is missing a readable body.')
// after — combine with the status check and name the likely culprit
if (!response.ok || !response.body)
  throw new Error(`Streaming transcription unusable response: status=${response.status} body=${response.body ? 'present' : 'missing — SSE stripped by proxy/runtime?'}`)
Defensive patterns

Strategy: type-guard

Validate before calling

// pre-flight the runtime before first use
if (typeof ReadableStream === 'undefined')
  throw new Error('This runtime cannot consume SSE transcription — use buffered mode')

Type guard

function isStreamingResponse(res: Response): res is Response & { body: ReadableStream<Uint8Array> } {
  return res.ok && res.body instanceof ReadableStream
}

Try / catch

if (!isStreamingResponse(response))
  // degrade to a buffered transcription call rather than piping
  return bufferedTranscribe(audioStream)

Prevention

When it happens

Trigger: Intermediary (proxy, service worker, antivirus TLS inspection) converting the streamed response into a buffered, body-less one; runtime fetch lacking ReadableStream response support; server answering 204/HEAD-like empty responses on success; test environment mocking fetch without bodies.

Common situations: Corporate proxy stripping SSE; happy-path unit test with a stubbed fetch returning `{ ok: true, body: undefined }`; Electron main-process fetch shim without stream plumbing; misconfigured CDN buffering `/transcriptions` endpoints.

Related errors


AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18). Data as JSON: /api/errors/460ab8dc288af50b. Report an issue: GitHub.

Appendix: source

Thrown at packages/stage-ui/src/libs/providers/stream-transcription/index.ts:157

      const requestTarget = options.baseURL instanceof URL
        ? options.baseURL
        : new URL(typeof options.baseURL === 'string' ? options.baseURL : 'http://localhost')
      const response = await fetcher(requestTarget, {
        body: audioStream,
        // Browser fetch requires half-duplex mode for a ReadableStream body.
        // Keep this at the transport boundary so every SSE transcription
        // provider receives the required request option.
        duplex: 'half',
        headers: options.headers,
        method: 'POST',
        signal: options.abortSignal,
      } as RequestInit & { duplex: 'half' })

      if (!response.ok)
        throw new Error(`Streaming transcription request failed with status ${response.status}`)

      if (!response.body)
        throw new Error('Streaming transcription response is missing a readable body.')

      await response.body
        .pipeThrough(createSSETransformer())
        .pipeTo(new WritableStream<AIRIStreamTranscriptionDelta>({
          write: (chunk) => {
            fullStreamCtrl?.enqueue(chunk)
            if (chunk.type === 'transcript.text.delta') {
              text += chunk.delta
              textStreamCtrl?.enqueue(chunk.delta)
            }
            else if (chunk.type === 'transcript.text.snapshot') {
              text = chunk.text
            }
          },
          close: () => {
            fullStreamCtrl?.close()
            textStreamCtrl?.close()
          },

View on GitHub (pinned to 677329427f)