BerriAI/litellm · error · RuntimeError

Mavvrik FOCUS destination: failed to get signed URL ({resp.s

Error message

Mavvrik FOCUS destination: failed to get signed URL ({resp.status_code}): {resp.text[:200]}

What it means

Before uploading, the destination GETs the Mavvrik upload-url endpoint (params name/type=metrics/datetime) to obtain a GCS signed URL. Any status >= 400 raises RuntimeError embedding the status and truncated body. Unlike registration, this is not cached — it happens per delivery date.

Source

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

        metrics_marker: Final = resp.json().get("metricsMarker", 0)
        verbose_logger.debug(
            "Mavvrik FOCUS destination: connector registered (metricsMarker=%s)",
            metrics_marker,
        )
        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

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Inspect the embedded status: 403 → grant upload permission to the key; 429 → back off and retry with jitter; 5xx → retry after a delay.
  2. Verify the date passed (time_window) is sane and the connection is registered first.
  3. Wrap delivery in a retry with exponential backoff for 429/5xx, since signed-URL fetch is idempotent.
  4. Contact Mavvrik support with the body snippet if 4xx persists.
Defensive patterns

Strategy: retry

Try / catch

import asyncio

for attempt in range(4):
    try:
        await mavvrik_dest.deliver(content=csv, time_window=tw, filename="f.csv")
        break
    except RuntimeError as e:
        if "failed to get signed URL" not in str(e) or attempt == 3:
            raise
        if any(s in str(e) for s in ("(401)", "(403)", "(404)")):
            raise
        await asyncio.sleep(2 ** attempt + 1)  # backoff for 429/5xx

Prevention

When it happens

Trigger: Calling deliver() when the API key lacks permission for the upload-url endpoint (403), the connection_id/date combination is unknown (404), Mavvrik rate-limits or errors (429/5xx), or the query params are rejected (400).

Common situations: Key valid for registration but scoped away from uploads; quota exhaustion on repeated exports; Mavvrik-side incidents; clock skew making the datetime param look out of range.

Related errors


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