{"record":{"id":"4caa7d2774b0d631","repo":"JuliusBrussee/caveman","slug":"cave-artifact-response-missing-artifact-id-4caa7d","errorCode":null,"errorMessage":"cave artifact response missing artifact_id","messagePattern":"cave artifact response missing artifact_id","errorType":"exception","errorClass":"RuntimeError","httpStatus":null,"severity":"error","filePath":"packages/sdk/python/caveman_cloud/core.py","lineNumber":1205,"sourceCode":"        ``workflow``, and same stub format). :attr:`artifacts` exposes ``page``\n        as the mirror-named entry point; ``page_artifact`` stays as a\n        backwards-compatible alias.\n        \"\"\"\n        if options.get(\"strategy\") == \"verbatim\":\n            return value\n        # `workflow` mirrors the TS SDK body so both SDKs send the identical wire.\n        response = self._request(\n            \"/sdk/v1/artifacts\",\n            {\"value\": value, \"options\": options, \"workflow\": self.workflow},\n            extra_headers={\n                \"x-cave-artifact-envelope\": \"value-v1\",\n                \"x-cave-artifact-source\": str(options[\"source\"]),\n            },\n        )\n        if response.get(\"stored\") is False:\n            return value\n        if response.get(\"stored\") is not True or not isinstance(response.get(\"artifact_id\"), str) or not response[\"artifact_id\"].strip():\n            raise RuntimeError(\"cave artifact response missing artifact_id\")\n        content_type = options.get(\"content_type\") or \"application/json\"\n        return (\n            f\"[cave-artifact id={response['artifact_id']} source={options['source']} type={content_type}]\\n\"\n            f\"summary: artifact stored.\\n\"\n            f\"retrieve: authenticated GET /sdk/v1/artifacts/{response['artifact_id']} or trace.artifacts.get(\\\"{response['artifact_id']}\\\").\\n\"\n            f\"[/cave-artifact]\"\n        )\n\n    def get_artifact(self, artifact_id: str) -> Any:\n        \"\"\"Retrieve and JSON-decode one artifact produced by :meth:`page_artifact`.\"\"\"\n        if not isinstance(artifact_id, str) or not artifact_id.strip():\n            raise ValueError(\"artifact_id is required\")\n        return self._get(f\"/sdk/v1/artifacts/{quote(artifact_id, safe='')}\")\n\n    @property\n    def artifacts(self) -> \"_Artifacts\":\n        \"\"\"Artifact handle mirroring the TS ``trace.artifacts`` namespace.\n","sourceCodeStart":1187,"sourceCodeEnd":1223,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/3ee70a102609e550bd2e68004bf5990a9341c851/packages/sdk/python/caveman_cloud/core.py#L1187-L1223","documentation":"`page_artifact()` posts content to the gateway and expects a response confirming storage: `stored: true` plus a non-empty string `artifact_id`. If the gateway answers with `stored` not true/false, or claims storage without a usable `artifact_id`, the SDK treats the response as corrupt and raises rather than returning a reference that could never be retrieved.","triggerScenarios":"The gateway returns a 200/2xx body without `stored` or with `stored: null`, or returns `stored: true` but `artifact_id` missing, non-string, or whitespace-only (e.g. an error JSON body served with a success status).","commonSituations":"A proxy or gateway returning an HTML/JSON error page with HTTP 200, a gateway version mismatch where the artifact endpoint changed shape, or an auth layer swallowing the request and returning an empty success body.","solutions":["Log the raw response body and check the gateway URL/auth — a malformed response usually means the request never reached the real artifact endpoint.","Verify SDK and gateway versions match (older gateways may not return `artifact_id`); upgrade the gateway or pin the SDK accordingly.","Retry the store once, then fall back to inlining the content instead of referencing an artifact.","Inspect intermediate proxies that may rewrite the response body."],"exampleFix":"// before\nref = client.page_artifact(payload, options={\"source\": \"tool\"})\n// after\ntry:\n    ref = client.page_artifact(payload, options={\"source\": \"tool\"})\nexcept RuntimeError:\n    ref = json.dumps(payload)  # fall back to inlining","handlingStrategy":"try-catch","validationCode":"resp = client._post(\"/sdk/v1/artifacts\", body)  # only if using internals\nif not isinstance(resp.get(\"artifact_id\"), str) or not resp[\"artifact_id\"].strip():\n    log.error(\"artifact store returned bad body: %s\", resp)","typeGuard":"def is_valid_artifact_response(resp: dict) -> bool:\n    return resp.get(\"stored\") is True and isinstance(resp.get(\"artifact_id\"), str) and bool(resp[\"artifact_id\"].strip())","tryCatchPattern":"try:\n    ref = client.page_artifact(payload, options={\"source\": \"tool\"})\nexcept RuntimeError as e:\n    log.error(\"artifact store failed: %s\", e)\n    ref = json.dumps(payload)  # inline fallback","preventionTips":["Log the full gateway response body on failure to spot proxies returning 200 error pages.","Pin SDK and gateway versions; test artifact round-trips (store then get) after upgrades.","Have an inline-content fallback so a broken artifact path degrades gracefully."],"tags":["python","api-response","artifacts","runtime-error"],"backgroundTag":"unexpected-api-response-shape","analyzedSha":"3ee70a102609e550bd2e68004bf5990a9341c851","analyzedAt":"2026-09-20T15:53:39.229Z","contentChangedAt":"2026-09-20T15:53:39.229Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}