decolua/9router · error

Upstream returned empty audio

Error message

Upstream returned empty audio

What it means

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.

Source

Thrown at open-sse/handlers/ttsProviders/_base.js:9

// Shared TTS helpers
import { Buffer } from "node:buffer";

export 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";

// Convert upstream Response (binary audio) to { base64, format }
export async function responseToBase64(res, defaultFormat = "mp3") {
  const buf = await res.arrayBuffer();
  if (buf.byteLength < 100) throw new Error("Upstream returned empty audio");
  const ctype = res.headers.get("content-type") || "";
  let format = defaultFormat;
  if (ctype.includes("wav")) format = "wav";
  else if (ctype.includes("mpeg") || ctype.includes("mp3")) format = "mp3";
  else if (ctype.includes("ogg")) format = "ogg";
  return { base64: Buffer.from(buf).toString("base64"), format };
}

export async function throwUpstreamError(res) {
  const text = await res.text().catch(() => "");
  let msg = `Upstream error (${res.status})`;
  try {
    const parsed = JSON.parse(text);
    msg = parsed?.error?.message || parsed?.message || parsed?.detail?.message || (typeof parsed?.detail === "string" ? parsed.detail : null) || text || msg;
  } catch { msg = text || msg; }
  throw new Error(msg);
}

View on GitHub (pinned to 90b52e06ff)

Solutions

  1. Log the raw upstream response body/status in the provider executor to see what the <100-byte payload actually contains
  2. Verify the voiceId and modelId against the provider's current voice list (voices change/deprecate)
  3. Confirm the account has quota/credits and the API key is valid — providers often return empty 200s on soft quota exhaustion
  4. Ensure the input text is non-empty and within provider length limits
  5. Retry the request; transient upstream issues can produce truncated bodies

Example fix

// before
const { base64 } = await responseToBase64(res);
// after
if (!text || !text.trim()) throw new Error("TTS input text is empty");
try {
  const { base64 } = await responseToBase64(res);
} catch (e) {
  console.error("TTS upstream body:", await res.clone?.().text?.().catch(() => "<binary>"));
  throw e;
}
Defensive patterns

Strategy: validation

Validate before calling

if (typeof text !== "string" || !text.trim()) throw new Error("TTS text must be a non-empty string");
if (!voiceId) throw new Error("voiceId is required for TTS synthesis");

Type guard

function isUsableAudio(buf) { return buf instanceof ArrayBuffer && buf.byteLength >= 100; }

Try / catch

try {
  const { base64, format } = await responseToBase64(res);
} catch (e) {
  if (e.message === "Upstream returned empty audio") {
    // inspect res.status/body, retry once or surface provider-specific guidance
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of decolua/9router@90b52e06ff (2026-08-30). Data as JSON: /api/errors/475e934907e9fa1f. Report an issue: GitHub.