moeru-ai/airi · error · Error
streaming voices upstream returned malformed body
Error message
streaming voices upstream returned malformed body
What it means
Thrown by the official streaming speech provider's `listVoices()` when `/api/v1/audio/voices/streaming` answered 200 but `data.voices` is not an array. Before the check the handler stores `data.recommended` into the per-provider recommendation map, so a malformed body still updates recommendations and then fails the catalog mapping. Same wire family as unspeech `ListVoicesResponse`.
Source
Thrown at packages/stage-ui/src/libs/providers/providers/official/index.ts:325
const data = await res.json() as {
voices?: {
id: string
name: string
description?: string
labels?: Record<string, unknown>
languages?: { code: string, title: string }[]
preview_audio_url?: string
}[]
recommended?: Record<string, string>
}
// Mirror the HTTP provider: stash the server's per-locale recommendations
// so setupOfficialSpeechAutoPick can seed a curated default voice when
// the streaming provider becomes active.
recommendedVoicesByProvider[OFFICIAL_SPEECH_STREAMING_PROVIDER_ID] = (data.recommended && typeof data.recommended === 'object') ? data.recommended : {}
if (!Array.isArray(data.voices))
throw new Error('streaming voices upstream returned malformed body')
return data.voices.map((v) => {
const rawGender = typeof v.labels?.gender === 'string' ? (v.labels.gender as string) : undefined
return {
id: v.id,
name: v.name,
provider: OFFICIAL_SPEECH_STREAMING_PROVIDER_ID,
description: v.description || undefined,
gender: rawGender?.toLowerCase() || undefined,
previewURL: v.preview_audio_url || undefined,
languages: Array.isArray(v.languages) ? v.languages : [],
}
})
},
},
})
export const providerOfficialTranscription = defineProvider({View on GitHub (pinned to 677329427f)
Solutions
- Inspect the raw body with curl and compare it to the expected `{ voices, recommended }` shape.
- Redeploy server and client from the same revision.
- Have the server always emit `voices: []` when the catalog is empty rather than omitting the field.
- Catch this in the caller and show an empty voice list instead of breaking settings.
Example fix
// before
if (!Array.isArray(data.voices))
throw new Error('streaming voices upstream returned malformed body')
// after — tolerate an absent catalog while keeping recommendations
if (!Array.isArray(data.voices)) {
console.warn('streaming voices payload lacked voices[]', data)
return []
} Defensive patterns
Strategy: type-guard
Validate before calling
const data: unknown = await res.json()
if (!isStreamingVoicesPayload(data))
console.warn('streaming voices payload malformed', data) Type guard
function isStreamingVoicesPayload(data: unknown): data is { voices: Array<{ id: string, name: string }>, recommended?: Record<string, string> } {
if (typeof data !== 'object' || data === null)
return false
return Array.isArray((data as { voices?: unknown }).voices)
} Try / catch
return isStreamingVoicesPayload(data) ? data.voices.map(toVoiceInfo) : []
Prevention
- Make `voices: []` the server's empty-catalog representation.
- Contract-test the streaming voices wire shape.
- Log raw payloads on validation failure to catch version skew early.
- Keep the recommendation map update idempotent so a malformed body never corrupts defaults.
When it happens
Trigger: 200 response with an error envelope or `null`; version skew where the streaming voices route returns a different shape than `{ voices: [...], recommended?: {...} }`; an edge returning 200 HTML that `res.json()` happens not to choke on (rare) or a JSON body without the key.
Common situations: Rolling deploy mixing old server and new client; streaming voice endpoint still experimental on the server and returning an empty object when the upstream has no voice catalog; proxy rewriting failures to 200.
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[]
- audio models upstream returned malformed body
- streaming voices upstream ${res.status}: ${await res.text().
- audio voices upstream ${res.status}: ${await res.text().catc
AI-assisted analysis of moeru-ai/airi@677329427f (2026-08-18).
Data as JSON: /api/errors/6c548184c28cce6c.
Report an issue: GitHub.