moeru-ai/airi · error · Error
audio models upstream returned malformed body
Error message
audio models upstream returned malformed body
What it means
Thrown by the official speech provider's `listModels()` when `/api/v1/audio/models` returned HTTP 200 but the JSON body has no `models` array (`Array.isArray(data.models)` is false). It is a contract violation between the client's expected wire shape `{ models: [...], default?: string }` and what the server actually returned — not a transport failure.
Source
Thrown at packages/stage-ui/src/libs/providers/providers/official/index.ts:136
...originalSpeech(model),
...extraOptions,
}
result.fetch = withCredentials()
return result
}
return provider
},
validationRequiredWhen: () => false,
extraMethods: {
listModels: async (): Promise<ModelInfo[]> => {
defaultSpeechModelId = null
const res = await globalThis.fetch(`${SERVER_URL}/api/v1/audio/models`, { headers: authHeaders() })
if (!res.ok)
throw new Error(`audio models upstream ${res.status}: ${await res.text().catch(() => '')}`.slice(0, 256))
const data = await res.json() as { models?: { id: string, name: string, description?: string }[], default?: string | null }
if (!Array.isArray(data.models))
throw new Error('audio models upstream returned malformed body')
defaultSpeechModelId = typeof data.default === 'string' && data.default.length > 0 ? data.default : null
return data.models.map(m => ({
id: m.id,
name: m.name,
description: m.description,
provider: OFFICIAL_SPEECH_PROVIDER_ID,
}))
},
listVoices: async (_config, _provider, model): Promise<VoiceInfo[]> => {
// Voice catalogs are model-scoped on the server side. Pass the active
// model through so Azure / cosyvoice / future provider voices route to
// the right adapter. If model discovery has not completed yet, keep the
// legacy `auto` request as a startup fallback.
const target = model && model.length > 0 ? model : 'auto'
const url = new URL(`${SERVER_URL}/api/v1/audio/voices`)
url.searchParams.set('model', target)View on GitHub (pinned to 677329427f)
Solutions
- curl the endpoint with a valid bearer token and inspect the actual body: does it contain `models: [...]`?
- Align client and server versions — redeploy `server/apps/api` from the same revision as `packages/stage-ui`.
- If a proxy rewrites error responses to 200, disable that behavior for `/api/v1/audio/*`.
- If the body legitimately has no models, configure at least one TTS backend in `UNSPEECH_UPSTREAM` so the server emits a non-empty catalog.
Example fix
// before
const data = await res.json() as { models?: ... }
if (!Array.isArray(data.models))
throw new Error('audio models upstream returned malformed body')
// after — keep a defensive fallback catalog shape and log the offending body
const data = await res.json() as { models?: ... }
if (!Array.isArray(data.models)) {
console.warn('audio models payload lacked models[]', data)
return []
} Defensive patterns
Strategy: type-guard
Validate before calling
const res = await fetch(url, { headers: authHeaders() })
const data: unknown = await res.json()
if (!Array.isArray((data as { models?: unknown }).models))
// log body and degrade instead of throwing Type guard
function isModelsCatalog(data: unknown): data is { models: Array<{ id: string, name: string, description?: string }>, default?: string | null } {
if (typeof data !== 'object' || data === null)
return false
const models = (data as { models?: unknown }).models
return Array.isArray(models) && models.every(m => typeof (m as { id?: unknown })?.id === 'string')
} Try / catch
try {
return data.models.map(toModelInfo)
}
catch {
throw new Error('audio models upstream returned malformed body')
} Prevention
- Contract-test the `/api/v1/audio/models` shape in server CI.
- Deploy client and server together; the `models[]` key is part of the wire contract.
- Log the raw body when validation fails so skew is diagnosable.
- Prefer degrading to an empty catalog over crashing the settings page.
When it happens
Trigger: Server responds 200 with `null`/`{}` (gateway healthy but upstream catalog empty), a differently-shaped payload from an older/newer server revision, or a JSON body like `{ "error": ... }` delivered with status 200 by a misconfigured proxy that rewrites error statuses.
Common situations: Client and server versions skew across a deploy (new client expects `models[]`, old server returns a different key); an edge/CDN intercepting the request and returning a 200 HTML or JSON error page; the unspeech backend returning an envelope without `models` when no TTS provider is configured.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- audio voices upstream returned malformed body
- streaming models upstream missing models[]
- streaming voices upstream returned malformed body
- audio models upstream ${res.status}: ${await res.text().catc
- MiniMax TTS request failed: ${response.status} ${response.st
AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18).
Data as JSON: /api/errors/cb1a64b965c35aa3.
Report an issue: GitHub.