moeru-ai/airi · error · Error
Speech engine answered /${endpoint} with a body that is not
Error message
Speech engine answered /${endpoint} with a body that is not JSON. Check that the Base URL points at a VOICEVOX-compatible engine. What it means
decodeJson reads the engine response as text and tries JSON.parse. VOICEVOX-family engines return JSON for their API endpoints, so a non-JSON body means the configured Base URL is not reaching a VOICEVOX-compatible engine (e.g. an HTML error page, login portal, or a different service). The library rethrows with a targeted message instead of surfacing the raw JSON.parse SyntaxError.
Source
Thrown at packages/stage-ui/src/libs/providers/providers/voicevox/engine.ts:176
const trimmed = baseUrl.trim()
return trimmed.endsWith('/') ? trimmed : `${trimmed}/`
}
function buildUrl(engineRequest: VoicevoxEngineRequest): URL {
const url = new URL(VOICEVOX_ENGINE_PATHS[engineRequest.endpoint], normalizeBaseUrl(engineRequest.baseUrl))
for (const [key, value] of Object.entries(engineRequest.query ?? {}))
url.searchParams.set(key, value)
return url
}
async function decodeJson<T>(response: Response, endpoint: string): Promise<T> {
const body = await response.text()
try {
return JSON.parse(body) as T
}
catch {
throw new Error(`Speech engine answered /${endpoint} with a body that is not JSON. Check that the Base URL points at a VOICEVOX-compatible engine.`)
}
}
async function request(
engineRequest: VoicevoxEngineRequest,
options?: VoicevoxEngineRequestOptions,
): Promise<Response> {
let url: URL
try {
url = buildUrl(engineRequest)
}
catch {
throw new Error('The Base URL is not an absolute http:// or https:// address.')
}
const doFetch = options?.fetch ?? globalThis.fetch
const response = await doFetch(url, {
method: POST_ENDPOINTS.has(engineRequest.endpoint) ? 'POST' : 'GET',View on GitHub (pinned to 9c213115f8)
Solutions
- Check the provider Base URL — it must point at the VOICEVOX engine API host:port (e.g. http://127.0.0.1:50021), not a webpage.
- Curl the endpoint directly: `curl http://<base>/speakers` and confirm JSON output.
- Inspect any reverse proxy logs for HTML error pages (502/503) being returned.
- Confirm the engine container/process is running and its port is exposed.
Example fix
// before baseUrl: 'http://127.0.0.1:50021/dashboard' // after baseUrl: 'http://127.0.0.1:50021'
Defensive patterns
Strategy: validation
Validate before calling
const res = await fetch(`${baseUrl}/speakers`)
const text = await res.text()
try { JSON.parse(text) }
catch { throw new Error('Base URL does not point at a VOICEVOX-compatible engine') } Try / catch
try {
await synthesize(request)
}
catch (error) {
if (errorMessageFrom(error).includes('not JSON'))
notifyUser('Check the VOICEVOX Base URL — the endpoint did not return JSON')
else
throw error
} Prevention
- Store Base URL without trailing paths; point it at the engine API root.
- Smoke-test the Base URL with `curl <base>/speakers` after configuring it.
- Check proxy configs return JSON errors or pass engine responses through unmodified.
When it happens
Trigger: Any engine endpoint call (e.g. /audio_query, /speakers) where the HTTP response body fails JSON.parse — HTML pages, plain-text proxy errors, empty bodies with content-type mismatch, or a captive portal response.
Common situations: Base URL typo pointing at a web page; a reverse proxy (nginx/Caddy) returning an HTML 502; pointing the URL at the VOICEVOX web UI instead of its API port; Docker port not actually mapped to the engine.
Related errors
- The Base URL is not an absolute http:// or https:// address.
- Speech engine answered ${response.status} ${response.statusT
- MiMo transcription failed: ${response.status} ${response.sta
- MiniMax TTS request failed: ${response.status} ${response.st
- OpenRouter audio request failed: ${response.status} ${await
AI-assisted analysis of moeru-ai/airi@9c213115f8 (2026-09-02).
Data as JSON: /api/errors/dd910ab4625862ec.
Report an issue: GitHub.