{"record":{"id":"195ad2b8fe198084","repo":"can1357/oh-my-pi","slug":"gemini-files-api-context-response-is-not-a-json","errorCode":null,"errorMessage":"Gemini Files API ${context} response is not a JSON object","messagePattern":"Gemini Files API (.+?) response is not a JSON object","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/blob-broker/provider-files-gemini.ts","lineNumber":18,"sourceCode":"import type { Model } from \"@oh-my-pi/pi-ai\";\nimport type { ProviderFileClient, ProviderFileHandle, ProviderFileUploadRequest } from \"./provider-file-types\";\nimport type { FetchImpl } from \"./uploader-runtime\";\n\nconst GEMINI_FILES_ORIGIN = \"https://generativelanguage.googleapis.com\";\nconst GEMINI_FILES_UPLOAD_URL = `${GEMINI_FILES_ORIGIN}/upload/v1beta/files`;\nconst GEMINI_FILES_RESOURCE_URL = `${GEMINI_FILES_ORIGIN}/v1beta`;\n\ninterface GeminiFileResource {\n\tname: string;\n\turi: string;\n\tmimeType: string;\n\texpiresAt: number;\n}\n\nfunction responseObject(value: unknown, context: string): Record<string, unknown> {\n\tif (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n\t\tthrow new Error(`Gemini Files API ${context} response is not a JSON object`);\n\t}\n\treturn value as Record<string, unknown>;\n}\n\nasync function responseJson(response: Response, context: string): Promise<Record<string, unknown>> {\n\ttry {\n\t\treturn responseObject((await response.json()) as unknown, context);\n\t} catch (error) {\n\t\tif (error instanceof Error && error.message.startsWith(\"Gemini Files API\")) throw error;\n\t\tthrow new Error(`Gemini Files API ${context} response is not valid JSON`);\n\t}\n}\n\nfunction requireString(value: unknown, field: string): string {\n\tif (typeof value !== \"string\" || value.length === 0) {\n\t\tthrow new Error(`Gemini Files API finalize response is missing ${field}`);\n\t}\n\treturn value;","sourceCodeStart":1,"sourceCodeEnd":36,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/blob-broker/provider-files-gemini.ts#L1-L36","documentation":"The Gemini Files API client assumes every JSON response body is a JSON object (`Record<string, unknown>`) — that is the documented shape for both the upload-poll (`file`) and metadata endpoints. `responseObject` guards this assumption: if the parsed body is not an object (a string, number, boolean, null, or an array), the response does not match the API contract and the client throws rather than indexing into an unexpected shape.","triggerScenarios":"`responseObject` is called (from `responseJson`/`file`) with a parsed body that is a JSON array, a JSON primitive/string, or null — e.g. the endpoint returned a list, an error string, or `null` while still being valid JSON.","commonSituations":"Gemini API version drift (endpoint now returns an array or wrapped envelope); an API error returned as a JSON string/HTML-like body that still parses; a proxy or local mock returning `[]` or `\"ok\"`; a custom baseUrl pointing at a non-Gemini compatible server; response.json() parsing an empty-ish body into null.","solutions":["Log the raw response body (status + text) for the failing call and check whether it is an object with the expected Gemini `file` fields (`name`, `mimeType`, `expirationTime`, `state`).","Verify you are using the documented Gemini Files API endpoints/versions (generativelanguage.googleapis.com, v1beta files) and a supported apiVersion — older or newer versions may wrap results differently.","Remove or fix any proxy/mock that returns arrays or strings, and update recorded fixtures to real object-shaped responses.","Check for an out-of-date package version if Google changed the response envelope, and update so the client unwraps the new shape."],"exampleFix":"// before — assuming the body shape\nconst body = await response.json();\nconst state = body.state; // throws later or here if body is an array\n\n// after — narrow before use\nconst body: unknown = await response.json();\nif (typeof body !== \"object\" || body === null || Array.isArray(body)) {\n  throw new Error(`Gemini Files API file response is not a JSON object: ${JSON.stringify(body)}`);\n}\nconst state = (body as Record<string, unknown>).state;","handlingStrategy":"try-catch","validationCode":"// check the shape of a Gemini response before consuming it\nfunction isJsonObject(v: unknown): v is Record<string, unknown> {\n  return typeof v === \"object\" && v !== null && !Array.isArray(v);\n}\nconst body: unknown = await response.json();\nif (!isJsonObject(body)) throw new Error(`Unexpected Gemini response shape: ${JSON.stringify(body)}`);","typeGuard":"const isJsonObject = (v: unknown): v is Record<string, unknown> => typeof v === \"object\" && v !== null && !Array.isArray(v);","tryCatchPattern":"try {\n  const file = await geminiFiles.file(name);\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"not a JSON object\")) {\n    logger.error(\"Gemini Files API returned non-object body — check API version/baseUrl/proxy\", { cause: err });\n    // retry once or fall back to re-uploading\n  } else throw err;\n}","preventionTips":["Pin a supported Gemini API version and re-verify response shapes after version upgrades.","Capture and log raw response bodies on parse failures so schema drift is diagnosable.","Avoid proxies/mocks that return arrays or strings where objects are expected; keep fixtures object-shaped.","Type-guard every response body at the boundary instead of casting `as Record<string, unknown>` blindly."],"tags":["gemini","files-api","json-parsing","schema-validation"],"backgroundTag":"json-response-parse-error","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}