{"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/27111382b4a79a7e983289d6e983a06af185ed0f/packages/stage-ui/src/libs/providers/providers/openrouter-audio-speech/index.ts#L98-L134","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Verify the runtime provides a standards-compliant fetch where successful streaming responses expose response.body (browsers and Node 18+ do).","Check for proxies or service workers between the app and openrouter.ai that may synthesize or buffer responses; bypass them for this endpoint.","If intermittently hitting this during OpenRouter incidents, retry once and report upstream unavailability if it persists.","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."],"exampleFix":"// before\nif (!response.body)\n  throw new Error('OpenRouter audio response has no body')\n\n// after: distinguish missing-body from environment limitation\nif (!response.body) {\n  throw new Error(\n    'OpenRouter returned 2xx but no streaming body; ' +\n    'verify fetch implementation supports ReadableStream responses ' +\n    '(status=' + response.status + ')',\n  )\n}","handlingStrategy":"try-catch","validationCode":"// Detect environments where fetch streaming bodies are unsupported\nfunction supportsStreamingResponseBody(): boolean {\n  return typeof ReadableStream !== 'undefined' && typeof globalThis.fetch === 'function'\n    && typeof new Response(new ReadableStream<Uint8Array>()).body !== 'undefined'\n}","typeGuard":"function hasReadableBody(response: Response): response is Response & { body: ReadableStream<Uint8Array> } {\n  return response.ok && response.body instanceof ReadableStream\n}","tryCatchPattern":"try {\n  const response = await audioFetch(input, init)\n  if (!response.ok)\n    throw new Error(`OpenRouter audio request failed: ${response.status}`)\n  if (!hasReadableBody(response))\n    throw new Error('OpenRouter audio response has no body; streaming unsupported in this environment')\n  // proceed with collectAudioChunks(response.body)\n}\ncatch (error) {\n  // Distinguish transport incompatibility from upstream failure for ops\n  reportTransportIssue(error)\n  throw error\n}","preventionTips":["Confirm the runtime exposes response.body on streaming fetches before using audio TTS.","Avoid proxies/service workers that buffer SSE responses for this endpoint.","Treat a null body on 2xx as a transport defect, not an upstream error, when reporting."],"tags":["network","streaming","tts","openrouter","fetch","transport"],"backgroundTag":null,"analyzedSha":"27111382b4a79a7e983289d6e983a06af185ed0f","analyzedAt":"2026-08-12T18:33:34.132Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}