BerriAI/litellm · error · RuntimeError

Mavvrik FOCUS destination: response missing 'url' field: {re

Error message

Mavvrik FOCUS destination: response missing 'url' field: {resp.json()}

What it means

The upload-url endpoint returned 2xx but its JSON body has no 'url' field (or it's null/empty). This indicates a Mavvrik API contract change or an unexpected (e.g. HTML) response that happened to return 200 — the code dumps resp.json() into the message for diagnosis.

Source

Thrown at litellm/integrations/focus/destinations/mavvrik_destination.py:153

        return metrics_marker

    async def _get_signed_url(self, date_str: str) -> str:
        """GET upload-url endpoint → GCS signed URL for the given date."""
        params: Final = {"name": date_str, "type": "metrics", "datetime": date_str}
        resp: Final = await self._http.client.request(
            method="GET",
            url=self._upload_url_endpoint,
            headers=self._auth_headers,
            params=params,
            timeout=30.0,
        )
        if resp.status_code >= 400:
            raise RuntimeError(
                f"Mavvrik FOCUS destination: failed to get signed URL ({resp.status_code}): {resp.text[:200]}"
            )
        signed_url: Final = resp.json().get("url")
        if not signed_url:
            raise RuntimeError(f"Mavvrik FOCUS destination: response missing 'url' field: {resp.json()}")
        _validate_gcs_url(signed_url, "signed URL")
        verbose_logger.debug("Mavvrik FOCUS destination: got signed URL for date %s", date_str)
        return signed_url

    async def _upload_to_gcs(self, signed_url: str, content: bytes) -> None:
        """Upload gzip-compressed CSV to GCS via chunked resumable upload.

        The full CSV is gzip-compressed first, then uploaded in _GCS_CHUNK_SIZE
        chunks using the GCS resumable upload protocol. GCS assembles the chunks
        server-side into a single complete object — the bucket receives one file
        regardless of how many chunks were sent.

        Intermediate chunks: Content-Range: bytes X-Y/*   → expect 308
        Final chunk:         Content-Range: bytes X-Y/T   → expect 200/201

        This handles exports larger than available memory for a single PUT while
        keeping the destination code self-contained (no changes to the FOCUS
        pipeline upstream).

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Read the embedded body in the message to see what fields were actually returned.
  2. Upgrade LiteLLM to the latest version — response-shape fixes are usually shipped quickly after API changes.
  3. If the field was renamed on Mavvrik's side, report it / pin a compatible Mavvrik API version if available.
  4. Retry once to rule out a transient gateway glitch.
Defensive patterns

Strategy: try-catch

Try / catch

try:
    await mavvrik_dest.deliver(content=csv, time_window=tw, filename="f.csv")
except RuntimeError as e:
    if "missing 'url' field" in str(e):
        log.error("Mavvrik API contract drift: %s", e)
        # one retry in case of gateway glitch, then open a support ticket
    raise

Prevention

When it happens

Trigger: Calling _get_signed_url when the Mavvrik API responds 200 with a payload like {"signedUrl": ...} (renamed field), an empty object, or a JSON error envelope without 'url'.

Common situations: Mavvrik API version change renaming the field; a gateway returning 200 with an error body; proxy health-check intercepting the path; older/newer LiteLLM expecting a different response shape.

Related errors


AI-assisted analysis of BerriAI/litellm@6c2dcb801b (2026-08-15). Data as JSON: /api/errors/0f0a8c6c430731f3. Report an issue: GitHub.