moeru-ai/airi · error · Error

OpenRouter audio response has no body

Error message

OpenRouter audio response has no body

What it means

Thrown by the OpenRouter audio fetch wrapper when response.ok is true but response.body is null/undefined, preventing SSE chunk collection. This is a defensive guard because the rest of the pipeline (collectAudioChunks) requires a ReadableStream to iterate. It indicates the transport returned a success status without a streamable body, which the audio codec cannot handle.

Source

Thrown at packages/stage-ui/src/libs/providers/providers/openrouter-audio-speech/index.ts:116

    const response = await globalThis.fetch(new URL('chat/completions', baseUrl), {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
        ...OPENROUTER_ATTRIBUTION_HEADERS,
      },
      body: JSON.stringify({
        model,
        messages: [{ role: 'user', content: ttsPrompt(body.input ?? '') }],
        modalities: ['text', 'audio'],
        audio: { voice: body.voice, format: 'pcm16' },
        stream: true,
      }),
    })
    if (!response.ok)
      throw new Error(`OpenRouter audio request failed: ${response.status} ${await response.text()}`)
    if (!response.body)
      throw new Error('OpenRouter audio response has no body')

    const wav = toWavFromPCM16(decodeBase64Pcm(await collectAudioChunks(response.body)), 24000)
    return new Response(new Blob([wav], { type: 'audio/wav' }), {
      status: 200,
      headers: { 'Content-Type': 'audio/wav' },
    })
  }
}

export const providerOpenRouterAudioSpeech = defineProvider<OpenRouterAudioConfig>({
  id: 'openrouter-audio-speech',
  name: 'OpenRouter',
  nameLocalize: ({ t }) => t('settings.pages.providers.provider.openrouter-audio-speech.title'),
  description: 'openrouter.ai',
  descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.openrouter-audio-speech.description'),
  tasks: ['text-to-speech'],
  icon: 'i-lobe-icons:openrouter',
  createProviderConfig: () => openRouterAudioConfigSchema,

View on GitHub (pinned to 27111382b4)

Solutions

  1. Verify the runtime provides a standards-compliant fetch where successful streaming responses expose response.body (browsers and Node 18+ do).
  2. Check for proxies or service workers between the app and openrouter.ai that may synthesize or buffer responses; bypass them for this endpoint.
  3. If intermittently hitting this during OpenRouter incidents, retry once and report upstream unavailability if it persists.
  4. As a fallback, treat a null body on a 2xx as an upstream protocol error and surface it distinctly from a transport error so ops can distinguish.

Example fix

// before
if (!response.body)
  throw new Error('OpenRouter audio response has no body')

// after: distinguish missing-body from environment limitation
if (!response.body) {
  throw new Error(
    'OpenRouter returned 2xx but no streaming body; ' +
    'verify fetch implementation supports ReadableStream responses ' +
    '(status=' + response.status + ')',
  )
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Detect environments where fetch streaming bodies are unsupported
function supportsStreamingResponseBody(): boolean {
  return typeof ReadableStream !== 'undefined' && typeof globalThis.fetch === 'function'
    && typeof new Response(new ReadableStream<Uint8Array>()).body !== 'undefined'
}

Type guard

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

Try / catch

try {
  const response = await audioFetch(input, init)
  if (!response.ok)
    throw new Error(`OpenRouter audio request failed: ${response.status}`)
  if (!hasReadableBody(response))
    throw new Error('OpenRouter audio response has no body; streaming unsupported in this environment')
  // proceed with collectAudioChunks(response.body)
}
catch (error) {
  // Distinguish transport incompatibility from upstream failure for ops
  reportTransportIssue(error)
  throw error
}

Prevention

When it happens

Trigger: A 2xx response from OpenRouter whose body was already consumed or stripped, or a fetch implementation / intermediary (proxy, service worker, HTTP/2 shim) that does not expose streaming bodies. Also possible if a misconfigured proxy buffers and detaches the body, or if the runtime lacks ReadableStream support on Response.

Common situations: Running in an environment where fetch does not return a streaming body (older Node without web streams polyfill); a corporate proxy or CDN that strips Transfer-Encoding: chunked; an OpenRouter edge response that returned 200 with empty body on partial outages; service workers intercepting the request and returning a synthesized Response without a body.

Related errors


AI-assisted analysis of moeru-ai/airi@27111382b4 (2026-08-12). Data as JSON: /api/errors/12d1e6184b3863fc. Report an issue: GitHub.