moeru-ai/airi · error

Invalid request body

Error message

Invalid request body

What it means

The custom fetch wrapper for Gemini TTS intercepts the request before it goes to the network and requires the request body to be a JSON string. This error is thrown when init.body is absent or not a string, which means the underlying speech SDK did not serialize a JSON body as expected. It is an internal contract guard between the SDK client and the Gemini generateContent endpoint.

Source

Thrown at packages/provider-inference/src/providers/cloud/google-gemini-audio-speech/index.ts:71

]

function normalizeBaseUrl(baseUrl: string | undefined) {
  const value = baseUrl?.trim() || DEFAULT_BASE_URL
  return value.endsWith('/') ? value : `${value}/`
}

function decodeBase64(base64: string) {
  const binary = atob(base64)
  const bytes = new Uint8Array(binary.length)
  for (let index = 0; index < binary.length; index++)
    bytes[index] = binary.charCodeAt(index)
  return bytes
}

function createAudioFetch(apiKey: string, baseUrl: string) {
  return async (_input: RequestInfo | URL, init?: RequestInit) => {
    if (!init?.body || typeof init.body !== 'string')
      throw new Error('Invalid request body')

    const body = JSON.parse(init.body) as {
      input?: string
      model?: string
      voice?: string
      temperature?: number
    }
    if (!body.input)
      throw new Error('Missing input text for Gemini TTS')
    if (!body.model)
      throw new Error('Missing model for Gemini TTS')

    const response = await globalThis.fetch(new URL(`models/${body.model}:generateContent`, baseUrl), {
      method: 'POST',
      headers: { 'x-goog-api-key': apiKey, 'Content-Type': 'application/json' },
      body: JSON.stringify({
        contents: [{ parts: [{ text: body.input }] }],
        generationConfig: {

View on GitHub (pinned to f679616c34)

Solutions

  1. Ensure you call the provider through its speech() API so the SDK serializes the request body as a JSON string
  2. Check that no custom fetch wrapper strips or replaces init.body before this wrapper sees it
  3. Pass plain JSON-serializable speech options (input text, model, voice) and avoid setting body yourself to FormData/streams
  4. Read the body in any custom middleware before consuming it, so downstream wrappers still receive the string

Example fix

// before
provider.speech({ fetch: () => fetch(url, { method: 'POST' }) })
// after
const audio = await provider.speech().generate({ text: 'hello', model: 'gemini-2.5-flash-preview-tts', voice: 'Kore' })
Defensive patterns

Strategy: validation

Validate before calling

const body = init?.body
if (typeof body !== 'string' || !body) throw new Error('speech request requires a JSON string body')
JSON.parse(body)

Type guard

function hasStringBody(init?: RequestInit): init is RequestInit & { body: string } {
  return typeof init?.body === 'string' && init.body.length > 0
}

Try / catch

try {
  const audio = await provider.speech().generate(options)
} catch (e) {
  if (e instanceof Error && e.message === 'Invalid request body') {
    // fix request construction: body must be a JSON string
  }
}

Prevention

When it happens

Trigger: Calling the speech() API of the google-gemini-audio-speech provider with a fetch/body configuration that bypasses JSON string serialization, e.g. passing a custom fetch that supplies no body, a FormData/ReadableStream/Blob body, or invoking the returned fetch function directly without init.body.

Common situations: Misconfiguring the provider so the client sends a non-JSON body (wrong endpoint type), wrapping the fetch with middleware that drops or transforms the body, or accidentally calling the wrapped fetch directly instead of through the SDK's speech method.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


AI-assisted analysis of moeru-ai/airi@f679616c34 (2026-09-08). Data as JSON: /api/errors/6ff449343e27db37. Report an issue: GitHub.