BerriAI/litellm · error · RuntimeError

GCS upload failed: status={response.status_code} body={respo

Error message

GCS upload failed: status={response.status_code} body={response.text}

What it means

After FocusGCSDestination.deliver() POSTs the export bytes to the GCS JSON API upload endpoint (uploadType=media), any response other than HTTP 200 raises RuntimeError embedding the status code and response body. Because the message is an f-string, the actual error contains the concrete status/body — common causes are 401/403 auth failures, 404 wrong bucket, and 400 malformed object name.

Source

Thrown at litellm/integrations/focus/destinations/gcs_destination.py:53

    async def deliver(
        self,
        *,
        content: bytes,
        time_window: FocusTimeWindow,
        filename: str,
    ) -> None:
        object_name: Final = self._build_object_key(time_window=time_window, filename=filename)
        headers: Final = await self.construct_request_headers(service_account_json=self.path_service_account_json)
        headers["Content-Type"] = "application/octet-stream"
        encoded_name: Final = encode_gcs_object_name_for_url(object_name)
        url: Final = (
            f"https://storage.googleapis.com/upload/storage/v1/b/"
            f"{self.BUCKET_NAME}/o?uploadType=media&name={encoded_name}"
        )
        response: Final = await self.async_httpx_client.post(url=url, headers=headers, data=content)
        if response.status_code != 200:
            raise RuntimeError(f"GCS upload failed: status={response.status_code} body={response.text}")
        verbose_logger.debug(
            "Focus GCS: uploaded %d bytes to gs://%s/%s",
            len(content),
            self.BUCKET_NAME,
            object_name,
        )

    def _build_object_key(self, *, time_window: FocusTimeWindow, filename: str) -> str:
        start_utc: Final = time_window.start_time.astimezone(timezone.utc)
        date_component: Final = f"date={start_utc.strftime('%Y-%m-%d')}"
        parts: Final = [self.prefix, date_component]
        if time_window.frequency == "hourly":
            parts.append(f"hour={start_utc.strftime('%H')}")
        key_prefix: Final = "/".join(filter(None, parts))
        return f"{key_prefix}/{filename}" if key_prefix else filename

View on GitHub (pinned to 6c2dcb801b)

Solutions

  1. Read the embedded status/body: 401/403 means credentials — verify FOCUS_GCS_PATH_SERVICE_ACCOUNT or ambient ADC, and grant roles/storage.objectCreator on the bucket.
  2. 404 means bucket mismatch — confirm the bucket_name used at construction matches an existing bucket in that project.
  3. Test auth independently: gcloud auth application-default print-access-token and a manual upload to the bucket.
  4. For network/VPC-SC errors (7xx/403 with 'Request is prohibited'), check egress rules and service perimeter configuration.
  5. Retry once on transient 5xx — GCS occasionally returns 500/503.

Example fix

# before
await destination.deliver(content=csv_bytes, time_window=tw, filename="focus.csv")

# after
try:
    await destination.deliver(content=csv_bytes, time_window=tw, filename="focus.csv")
except RuntimeError as e:
    log.error("GCS export failed: %s", e)  # message includes status + body
    raise
Defensive patterns

Strategy: retry

Validate before calling

# Pre-flight: bucket exists and caller has objectCreator before first export
import subprocess
subprocess.run(["gcloud", "storage", "buckets", "describe", f"gs://{bucket_name}"], check=True)

Try / catch

import re

async def safe_deliver(dest, **kw):
    try:
        return await dest.deliver(**kw)
    except RuntimeError as e:
        msg = str(e)
        if re.search(r"status=(5\d\d|429)", msg):
            await asyncio.sleep(5)
            return await dest.deliver(**kw)  # one retry on transient
        raise  # 401/403/404 are config issues — surface them

Prevention

When it happens

Trigger: Calling deliver() on a FocusGCSDestination when the service-account/default credentials lack storage.objects.create on the bucket (403), the bucket name is wrong or deleted (404), the OAuth token fetch failed (401), or the URL-encoded object name is rejected (400).

Common situations: Workload Identity not bound to the service account; service account JSON expired or for a different project; bucket deleted or renamed after config; VPC-SC perimeter blocking storage.googleapis.com; regional bucket access restrictions.

Related errors


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