JuliusBrussee/caveman · error · RuntimeError

cave artifact response missing artifact_id

Error message

cave artifact response missing artifact_id

What it means

`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.

Solutions

  1. Log the raw response body and check the gateway URL/auth — a malformed response usually means the request never reached the real artifact endpoint.
  2. Verify SDK and gateway versions match (older gateways may not return `artifact_id`); upgrade the gateway or pin the SDK accordingly.
  3. Retry the store once, then fall back to inlining the content instead of referencing an artifact.
  4. Inspect intermediate proxies that may rewrite the response body.

Example fix

// before
ref = client.page_artifact(payload, options={"source": "tool"})
// after
try:
    ref = client.page_artifact(payload, options={"source": "tool"})
except RuntimeError:
    ref = json.dumps(payload)  # fall back to inlining
Defensive patterns

Strategy: try-catch

Validate before calling

resp = client._post("/sdk/v1/artifacts", body)  # only if using internals
if not isinstance(resp.get("artifact_id"), str) or not resp["artifact_id"].strip():
    log.error("artifact store returned bad body: %s", resp)

Type guard

def is_valid_artifact_response(resp: dict) -> bool:
    return resp.get("stored") is True and isinstance(resp.get("artifact_id"), str) and bool(resp["artifact_id"].strip())

Try / catch

try:
    ref = client.page_artifact(payload, options={"source": "tool"})
except RuntimeError as e:
    log.error("artifact store failed: %s", e)
    ref = json.dumps(payload)  # inline fallback

Prevention

When it happens

Trigger: 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).

Common situations: 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.

Understand the failure class

Background: "invalid response format", "malformed payload", "missing data field": when an API returns 200 but the response shape is wrong — this error's family across 23 libraries.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/4caa7d2774b0d631. Report an issue: GitHub.

Appendix: source

Thrown at packages/sdk/python/caveman_cloud/core.py:1205

        ``workflow``, and same stub format). :attr:`artifacts` exposes ``page``
        as the mirror-named entry point; ``page_artifact`` stays as a
        backwards-compatible alias.
        """
        if options.get("strategy") == "verbatim":
            return value
        # `workflow` mirrors the TS SDK body so both SDKs send the identical wire.
        response = self._request(
            "/sdk/v1/artifacts",
            {"value": value, "options": options, "workflow": self.workflow},
            extra_headers={
                "x-cave-artifact-envelope": "value-v1",
                "x-cave-artifact-source": str(options["source"]),
            },
        )
        if response.get("stored") is False:
            return value
        if response.get("stored") is not True or not isinstance(response.get("artifact_id"), str) or not response["artifact_id"].strip():
            raise RuntimeError("cave artifact response missing artifact_id")
        content_type = options.get("content_type") or "application/json"
        return (
            f"[cave-artifact id={response['artifact_id']} source={options['source']} type={content_type}]\n"
            f"summary: artifact stored.\n"
            f"retrieve: authenticated GET /sdk/v1/artifacts/{response['artifact_id']} or trace.artifacts.get(\"{response['artifact_id']}\").\n"
            f"[/cave-artifact]"
        )

    def get_artifact(self, artifact_id: str) -> Any:
        """Retrieve and JSON-decode one artifact produced by :meth:`page_artifact`."""
        if not isinstance(artifact_id, str) or not artifact_id.strip():
            raise ValueError("artifact_id is required")
        return self._get(f"/sdk/v1/artifacts/{quote(artifact_id, safe='')}")

    @property
    def artifacts(self) -> "_Artifacts":
        """Artifact handle mirroring the TS ``trace.artifacts`` namespace.

View on GitHub (pinned to 3ee70a1026)