{"record":{"id":"475e934907e9fa1f","repo":"decolua/9router","slug":"upstream-returned-empty-audio","errorCode":null,"errorMessage":"Upstream returned empty audio","messagePattern":"Upstream returned empty audio","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"open-sse/handlers/ttsProviders/_base.js","lineNumber":9,"sourceCode":"// Shared TTS helpers\nimport { Buffer } from \"node:buffer\";\n\nexport const UA = \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36\";\n\n// Convert upstream Response (binary audio) to { base64, format }\nexport async function responseToBase64(res, defaultFormat = \"mp3\") {\n  const buf = await res.arrayBuffer();\n  if (buf.byteLength < 100) throw new Error(\"Upstream returned empty audio\");\n  const ctype = res.headers.get(\"content-type\") || \"\";\n  let format = defaultFormat;\n  if (ctype.includes(\"wav\")) format = \"wav\";\n  else if (ctype.includes(\"mpeg\") || ctype.includes(\"mp3\")) format = \"mp3\";\n  else if (ctype.includes(\"ogg\")) format = \"ogg\";\n  return { base64: Buffer.from(buf).toString(\"base64\"), format };\n}\n\nexport async function throwUpstreamError(res) {\n  const text = await res.text().catch(() => \"\");\n  let msg = `Upstream error (${res.status})`;\n  try {\n    const parsed = JSON.parse(text);\n    msg = parsed?.error?.message || parsed?.message || parsed?.detail?.message || (typeof parsed?.detail === \"string\" ? parsed.detail : null) || text || msg;\n  } catch { msg = text || msg; }\n  throw new Error(msg);\n}\n","sourceCodeStart":1,"sourceCodeEnd":27,"githubUrl":"https://github.com/decolua/9router/blob/90b52e06ffd666b7929554211474d01588f6b1f8/open-sse/handlers/ttsProviders/_base.js#L1-L27","documentation":"responseToBase64() converts an upstream TTS provider's binary audio Response into base64. It guards against providers returning an HTTP 200 with a tiny/empty body (e.g. an error JSON or blank payload) by requiring at least 100 bytes of audio data; anything shorter throws 'Upstream returned empty audio'. This is the library's sanity check that real audio bytes were received before embedding them in a data URI.","triggerScenarios":"Any TTS request through deepgram, nvidia, huggingface, fishAudio, cartesia, or playht where the upstream responds 2xx but the body is <100 bytes: invalid/unsupported voice id with a silent 200, empty text input, exhausted free-tier quota returning a stub body, or a proxy/CDN stripping the response body.","commonSituations":"Developer passes a voiceId that doesn't exist on the provider; text is empty or whitespace after trimming; account has no credits so the provider returns an empty success instead of an error status; corporate proxy or bot-protection (Cloudflare) returns a short challenge page under 100 bytes with 200.","solutions":["Log the raw upstream response body/status in the provider executor to see what the <100-byte payload actually contains","Verify the voiceId and modelId against the provider's current voice list (voices change/deprecate)","Confirm the account has quota/credits and the API key is valid — providers often return empty 200s on soft quota exhaustion","Ensure the input text is non-empty and within provider length limits","Retry the request; transient upstream issues can produce truncated bodies"],"exampleFix":"// before\nconst { base64 } = await responseToBase64(res);\n// after\nif (!text || !text.trim()) throw new Error(\"TTS input text is empty\");\ntry {\n  const { base64 } = await responseToBase64(res);\n} catch (e) {\n  console.error(\"TTS upstream body:\", await res.clone?.().text?.().catch(() => \"<binary>\"));\n  throw e;\n}","handlingStrategy":"validation","validationCode":"if (typeof text !== \"string\" || !text.trim()) throw new Error(\"TTS text must be a non-empty string\");\nif (!voiceId) throw new Error(\"voiceId is required for TTS synthesis\");","typeGuard":"function isUsableAudio(buf) { return buf instanceof ArrayBuffer && buf.byteLength >= 100; }","tryCatchPattern":"try {\n  const { base64, format } = await responseToBase64(res);\n} catch (e) {\n  if (e.message === \"Upstream returned empty audio\") {\n    // inspect res.status/body, retry once or surface provider-specific guidance\n  }\n  throw e;\n}","preventionTips":["Always pass non-empty, trimmed text and a validated voiceId","Keep provider API keys and quota in good standing — empty 200s often signal soft quota exhaustion","Log upstream content-type and status before conversion to catch stub responses early","Add a retry with a different voice/model on empty-audio failures"],"tags":["tts","upstream","audio","empty-response"],"backgroundTag":"empty-upstream-response","analyzedSha":"90b52e06ffd666b7929554211474d01588f6b1f8","analyzedAt":"2026-08-30T21:05:45.952Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}