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

  1. 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.
  2. Curl the endpoint directly: `curl http://<base>/speakers` and confirm JSON output.
  3. Inspect any reverse proxy logs for HTML error pages (502/503) being returned.
  4. 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

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


AI-assisted analysis of moeru-ai/airi@9c213115f8 (2026-09-02). Data as JSON: /api/errors/dd910ab4625862ec. Report an issue: GitHub.