{"record":{"id":"bca324d2bfb4f6f7","repo":"invoke-ai/InvokeAI","slug":"gemini-response-payload-was-not-a-json-object","errorCode":null,"errorMessage":"Gemini response payload was not a JSON object","messagePattern":"Gemini response payload was not a JSON object","errorType":"exception","errorClass":"ExternalProviderRequestError","httpStatus":null,"severity":"error","filePath":"invokeai/app/services/external_generation/providers/gemini.py","lineNumber":126,"sourceCode":"            params={\"key\": api_key},\n            json=payload,\n            timeout=120,\n        )\n\n        if not response.ok:\n            if response.status_code == 429:\n                retry_after = _parse_retry_after(response.headers.get(\"retry-after\"))\n                raise ExternalProviderRateLimitError(\n                    f\"Gemini rate limit exceeded. {f'Retry after {retry_after:.0f}s.' if retry_after else 'Please try again later.'}\",\n                    retry_after=retry_after,\n                )\n            raise ExternalProviderRequestError(\n                f\"Gemini request failed with status {response.status_code} for model '{model_id}': {response.text}\"\n            )\n\n        data = response.json()\n        if not isinstance(data, dict):\n            raise ExternalProviderRequestError(\"Gemini response payload was not a JSON object\")\n        images: list[ExternalGeneratedImage] = []\n        text_parts: list[str] = []\n        finish_messages: list[str] = []\n        candidates = data.get(\"candidates\")\n        if not isinstance(candidates, list):\n            raise ExternalProviderRequestError(\"Gemini response payload missing candidates\")\n        for candidate in candidates:\n            if not isinstance(candidate, dict):\n                continue\n            finish_message = candidate.get(\"finishMessage\")\n            finish_reason = candidate.get(\"finishReason\")\n            if isinstance(finish_message, str):\n                finish_messages.append(finish_message)\n            elif isinstance(finish_reason, str):\n                finish_messages.append(f\"Finish reason: {finish_reason}\")\n            for part in _iter_response_parts(candidate):\n                inline_data = part.get(\"inline_data\") or part.get(\"inlineData\")\n                if isinstance(inline_data, dict):","sourceCodeStart":108,"sourceCodeEnd":144,"githubUrl":"https://github.com/invoke-ai/InvokeAI/blob/0b6a024f2ff6a86bfb953dcdb9cc504ef7397a06/invokeai/app/services/external_generation/providers/gemini.py#L108-L144","documentation":"ExternalProviderRequestError raised when Gemini responds 200 OK but response.json() yields something other than a JSON object (dict) — for example a JSON array, string, number, or null. The provider expects the standard GenerateContentResponse object shape and refuses to parse anything else. This almost always indicates the configured endpoint/proxy is not the real Gemini API or an intermediary mangled the response.","triggerScenarios":"data = response.json() returns a non-dict: a custom external_gemini_base_url (proxy/gateway) returning an error array or plain value with status 200; an auth-gateway returning JSON like {\"list\": ...} shapes; a misconfigured mock server in tests; a captive portal or JSON-API wrapper returning a top-level list.","commonSituations":"Pointing external_gemini_base_url at an OpenAI-compatible proxy that returns a different schema; corporate proxy intercepting responses; upgrading Gemini behind a translation layer that changed the envelope; unit-test fake returning the wrong top-level type.","solutions":["Log/print the raw response body before parsing to see what was actually returned.","Unset any custom external_gemini_base_url so the official https://generativelanguage.googleapis.com endpoint is used.","If using a proxy, configure it to pass through Google's response unchanged rather than re-wrapping it.","Fix test mocks to return a dict-shaped GenerateContentResponse."],"exampleFix":"// before: assuming a proxy speaks Gemini's schema\nbase_url = \"https://my-proxy.internal/v1\"\n// after: use the official endpoint (or verify the proxy passthrough)\nbase_url = None  # provider defaults to https://generativelanguage.googleapis.com/v1beta","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"def is_gemini_response(data: object) -> bool:\n    return (\n        isinstance(data, dict)\n        and isinstance(data.get(\"candidates\"), list)\n        and len(data[\"candidates\"]) > 0\n    )\n# use on the raw JSON before trusting the envelope:\n# data = response.json(); if not is_gemini_response(data): fail fast","tryCatchPattern":"try:\n    result = provider.generate(request)\nexcept ExternalProviderRequestError as e:\n    if \"not a JSON object\" in str(e):\n        logger.error(\"Non-Gemini response shape; check external_gemini_base_url/proxy\")\n    raise","preventionTips":["Avoid custom external_gemini_base_url values unless the proxy passes Google's schema through unchanged.","In tests, make mocks return a realistic dict-shaped GenerateContentResponse.","Log raw response bodies when a parse error occurs to spot proxies/interceptors quickly.","Verify TLS-intercepting corporate proxies don't rewrite API responses."],"tags":["json","response-shape","gemini","proxy","schema"],"backgroundTag":"unexpected-response-shape","analyzedSha":"0b6a024f2ff6a86bfb953dcdb9cc504ef7397a06","analyzedAt":"2026-08-29T04:46:49.967Z","schemaVersion":2},"datasetVersion":"2026-08-29T07:17:48.351Z"}