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 speech provider when the request succeeded (`response.ok`) but `response.body` is null/undefined, i.e. there is no readable stream to collect SSE audio chunks from. The code requests `stream: true`, so a body-less 2xx means the transport or an intermediary broke the streaming contract — the PCM/WAV assembly cannot even start.
Solutions
- Check the runtime: run where native `fetch` with ReadableStream bodies is available (modern browsers, Electron renderer, Node 18+ undici).
- Remove or configure intermediaries to pass SSE through untouched (no buffering, no content-encoding rewrites).
- If a fetch wrapper logs bodies, make it tee (`response.body.tee()`) instead of consuming the original.
- As a fallback path, request a non-streaming completion and read base64 audio from the JSON when a body is unavailable.
Example fix
// before
if (!response.ok)
throw new Error(`OpenRouter audio request failed: ...`)
if (!response.body)
throw new Error('OpenRouter audio response has no body')
// after — one combined guard with an actionable message
if (!response.ok || !response.body)
throw new Error(`OpenRouter audio unusable response: status=${response.status} body=${response.body ? 'yes' : 'missing (stream stripped?)'}`) Defensive patterns
Strategy: type-guard
Validate before calling
// nothing caller-side can guarantee a body; validate the response instead
if (!response.body)
throw new Error('No streaming body — check proxy/runtime support before using OpenRouter audio TTS') Type guard
function hasStreamingBody(res: Response): res is Response & { body: ReadableStream<Uint8Array> } {
return res.ok && res.body instanceof ReadableStream
} Try / catch
if (!hasStreamingBody(response)) // fall back to a buffered completion instead of collecting chunks return bufferedAudioCompletion(request) const wav = toWavFromPCM16(decodeBase64Pcm(await collectAudioChunks(response.body)), 24000)
Prevention
- Run in runtimes with native streaming fetch (modern browsers, Electron renderer, Node 18+).
- Keep SSE intermediaries (proxies, service workers) pass-through.
- Tee bodies in logging interceptors instead of consuming them.
- Implement a buffered fallback path for body-less environments.
When it happens
Trigger: An intermediary (corporate proxy, service worker, Electron `<webview>` fetch shim, HTTP/1.0 hop) strips or buffers the streamed body; a runtime where `fetch` does not expose streaming bodies (older Node without `dom.highWaterMark`-era undici, jsdom); accidental double-read of the body upstream consumed it before this check.
Common situations: Running the renderer inside an environment whose fetch is polyfilled without stream support; a debugging proxy (e.g. Charles/fiddler) decompressing and dechunking SSE; response already consumed by a logging interceptor.
Related errors
- OpenRouter audio request failed
- streaming models upstream
- Streaming transcription request failed with status
- Streaming transcription response is missing a readable body.
- audio voices upstream
AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18).
Data as JSON: /api/errors/12d1e6184b3863fc.
Report an issue: GitHub.
Appendix: 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 677329427f)