{"record":{"id":"12d1e6184b3863fc","repo":"moeru-ai/airi","slug":"openrouter-audio-response-has-no-body","errorCode":null,"errorMessage":"OpenRouter audio response has no body","messagePattern":"OpenRouter audio response has no body","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/stage-ui/src/libs/providers/providers/openrouter-audio-speech/index.ts","lineNumber":116,"sourceCode":"    const response = await globalThis.fetch(new URL('chat/completions', baseUrl), {\n      method: 'POST',\n      headers: {\n        'Authorization': `Bearer ${apiKey}`,\n        'Content-Type': 'application/json',\n        ...OPENROUTER_ATTRIBUTION_HEADERS,\n      },\n      body: JSON.stringify({\n        model,\n        messages: [{ role: 'user', content: ttsPrompt(body.input ?? '') }],\n        modalities: ['text', 'audio'],\n        audio: { voice: body.voice, format: 'pcm16' },\n        stream: true,\n      }),\n    })\n    if (!response.ok)\n      throw new Error(`OpenRouter audio request failed: ${response.status} ${await response.text()}`)\n    if (!response.body)\n      throw new Error('OpenRouter audio response has no body')\n\n    const wav = toWavFromPCM16(decodeBase64Pcm(await collectAudioChunks(response.body)), 24000)\n    return new Response(new Blob([wav], { type: 'audio/wav' }), {\n      status: 200,\n      headers: { 'Content-Type': 'audio/wav' },\n    })\n  }\n}\n\nexport const providerOpenRouterAudioSpeech = defineProvider<OpenRouterAudioConfig>({\n  id: 'openrouter-audio-speech',\n  name: 'OpenRouter',\n  nameLocalize: ({ t }) => t('settings.pages.providers.provider.openrouter-audio-speech.title'),\n  description: 'openrouter.ai',\n  descriptionLocalize: ({ t }) => t('settings.pages.providers.provider.openrouter-audio-speech.description'),\n  tasks: ['text-to-speech'],\n  icon: 'i-lobe-icons:openrouter',\n  createProviderConfig: () => openRouterAudioConfigSchema,","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/moeru-ai/airi/blob/677329427f32468c74b17f3ec47eeca4e05bec65/packages/stage-ui/src/libs/providers/providers/openrouter-audio-speech/index.ts#L98-L134","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nif (!response.ok)\n  throw new Error(`OpenRouter audio request failed: ...`)\nif (!response.body)\n  throw new Error('OpenRouter audio response has no body')\n// after — one combined guard with an actionable message\nif (!response.ok || !response.body)\n  throw new Error(`OpenRouter audio unusable response: status=${response.status} body=${response.body ? 'yes' : 'missing (stream stripped?)'}`)","handlingStrategy":"type-guard","validationCode":"// nothing caller-side can guarantee a body; validate the response instead\nif (!response.body)\n  throw new Error('No streaming body — check proxy/runtime support before using OpenRouter audio TTS')","typeGuard":"function hasStreamingBody(res: Response): res is Response & { body: ReadableStream<Uint8Array> } {\n  return res.ok && res.body instanceof ReadableStream\n}","tryCatchPattern":"if (!hasStreamingBody(response))\n  // fall back to a buffered completion instead of collecting chunks\n  return bufferedAudioCompletion(request)\nconst wav = toWavFromPCM16(decodeBase64Pcm(await collectAudioChunks(response.body)), 24000)","preventionTips":["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."],"tags":["network","streaming","fetch","tts","openrouter"],"backgroundTag":"empty-response-body","analyzedSha":"677329427f32468c74b17f3ec47eeca4e05bec65","analyzedAt":"2026-08-18T17:29:58.153Z","contentChangedAt":"2026-08-18T17:29:58.153Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}