{"record":{"id":"6ff449343e27db37","repo":"moeru-ai/airi","slug":"invalid-request-body","errorCode":null,"errorMessage":"Invalid request body","messagePattern":"Invalid request body","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/provider-inference/src/providers/cloud/google-gemini-audio-speech/index.ts","lineNumber":71,"sourceCode":"]\n\nfunction normalizeBaseUrl(baseUrl: string | undefined) {\n  const value = baseUrl?.trim() || DEFAULT_BASE_URL\n  return value.endsWith('/') ? value : `${value}/`\n}\n\nfunction decodeBase64(base64: string) {\n  const binary = atob(base64)\n  const bytes = new Uint8Array(binary.length)\n  for (let index = 0; index < binary.length; index++)\n    bytes[index] = binary.charCodeAt(index)\n  return bytes\n}\n\nfunction createAudioFetch(apiKey: string, baseUrl: string) {\n  return async (_input: RequestInfo | URL, init?: RequestInit) => {\n    if (!init?.body || typeof init.body !== 'string')\n      throw new Error('Invalid request body')\n\n    const body = JSON.parse(init.body) as {\n      input?: string\n      model?: string\n      voice?: string\n      temperature?: number\n    }\n    if (!body.input)\n      throw new Error('Missing input text for Gemini TTS')\n    if (!body.model)\n      throw new Error('Missing model for Gemini TTS')\n\n    const response = await globalThis.fetch(new URL(`models/${body.model}:generateContent`, baseUrl), {\n      method: 'POST',\n      headers: { 'x-goog-api-key': apiKey, 'Content-Type': 'application/json' },\n      body: JSON.stringify({\n        contents: [{ parts: [{ text: body.input }] }],\n        generationConfig: {","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/moeru-ai/airi/blob/f679616c34f1cf6d282c8d64264242af944b3fed/packages/provider-inference/src/providers/cloud/google-gemini-audio-speech/index.ts#L53-L89","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Ensure you call the provider through its speech() API so the SDK serializes the request body as a JSON string","Check that no custom fetch wrapper strips or replaces init.body before this wrapper sees it","Pass plain JSON-serializable speech options (input text, model, voice) and avoid setting body yourself to FormData/streams","Read the body in any custom middleware before consuming it, so downstream wrappers still receive the string"],"exampleFix":"// before\nprovider.speech({ fetch: () => fetch(url, { method: 'POST' }) })\n// after\nconst audio = await provider.speech().generate({ text: 'hello', model: 'gemini-2.5-flash-preview-tts', voice: 'Kore' })","handlingStrategy":"validation","validationCode":"const body = init?.body\nif (typeof body !== 'string' || !body) throw new Error('speech request requires a JSON string body')\nJSON.parse(body)","typeGuard":"function hasStringBody(init?: RequestInit): init is RequestInit & { body: string } {\n  return typeof init?.body === 'string' && init.body.length > 0\n}","tryCatchPattern":"try {\n  const audio = await provider.speech().generate(options)\n} catch (e) {\n  if (e instanceof Error && e.message === 'Invalid request body') {\n    // fix request construction: body must be a JSON string\n  }\n}","preventionTips":["Always call TTS through the provider's speech() API rather than the raw fetch wrapper","Do not insert middleware that replaces the JSON body with FormData or streams","When testing the wrapper directly, pass a JSON.stringify'd body","Keep request serialization inside the SDK client"],"tags":["tts","request-body","validation"],"backgroundTag":"empty-required-field","analyzedSha":"f679616c34f1cf6d282c8d64264242af944b3fed","analyzedAt":"2026-09-08T13:18:15.005Z","contentChangedAt":"2026-09-08T13:18:15.005Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}