Skip to content

Video downloads fail with 400 INVALID_ARGUMENT: media moved to flow-content.google #4

Description

@davidwang2026

Summary

As of 2026-08-11, every video download fails. POST /v1/videos/generations returns:

HTTP 500 {"detail":"Failed to complete video generations or downloads."}

The generation itself succeeds and credits are spent — only the retrieval fails. Because callers retry, each failure regenerates the clip, so this silently burns quota. In my case 65 generations completed with 0 successful downloads before I caught it.

Version: 2.0.3 (installed via uv tool install from source).

Root cause

Two changes on Google's side, both of which download_video() is blind to:

  1. Generated media moved off storage.googleapis.com onto flow-content.google.
  2. GET /v1/media/{media_id} no longer returns video bytes. It now answers:
{
  "status": 400,
  "data": {"error": {
    "code": 400,
    "message": "Request contains an invalid argument.",
    "status": "INVALID_ARGUMENT"
  }}
}

poll_status still reports MEDIA_GENERATION_STATUS_SUCCESSFUL correctly, and the media[0].video object it returns carries only metadata (generatedVideo, dimensions, mediaBlobSize) — no URL and no encodedVideo. So the video is ready and its size is known, but the old fetch path can't reach the bytes.

Adding projectId as a query parameter does not help — the endpoint rejects it explicitly (Unknown name "projectId": Cannot bind query parameter), which confirms the argument isn't what's missing.

What the web app does instead

GET https://labs.google/fx/api/trpc/media.getMediaUrlRedirect?name={media_id}
→ 302
Location: https://flow-content.google/video/{media_id}?Expires=…&KeyName=labs-flow-prod-cdn-key&Signature=…

Omitting mediaUrlType yields the full video; mediaUrlType=MEDIA_URL_TYPE_THUMBNAIL yields the poster image. The signed link is short-lived (roughly a few hours).

Note this is the same shape the image path already uses — t2i.download_image() fetches a signed URL directly with a proxy-free opener. Video was the only generator still expecting base64 through the API.

Why it looked like a connectivity problem

download_video() logs the same line for every failure mode:

if not video_b64:
    log.error("No video data in response")
    return False

A timeout, a 401, and an outright rejection are indistinguishable in the log, so the symptom reads as "the extension isn't connected" when the bridge is in fact perfectly healthy (/v1/credits succeeds throughout). Including the raw response in that branch is what made the root cause visible within minutes.

The extension side has the same class of problem: injected.js gates its tRPC interception on a hardcoded text.includes('storage.googleapis.com/ai-sandbox-videofx/'), which is now never true — the capture path fails silently rather than erroring.

Fix I'm running

Backend (happy to open a PR if useful — I only have read access here):

  • bridge.py — accept a media_urls_refresh message from the extension and cache signed URLs; add request_media_url() that asks the extension to follow the redirect endpoint and returns the resulting URL; log unrecognised extension messages instead of dropping them
  • generators/common.py — try the signed URL first (proxy-free opener, mirroring download_image), keep the media API as a fallback; include the raw response when no video data is found
  • routes/media.py — add GET /v1/media/{media_id}/url, which resolves a media ID to a fresh signed URL so results whose download failed can be recovered rather than regenerated

Extension:

  • add https://flow-content.google/* to host_permissions (without it webRequest can't observe these requests at all)
  • add a get_media_url handler that fetches media.getMediaUrlRedirect, reads resp.url after the redirect, and cancels the body so the media isn't buffered
  • widen the hardcoded domain checks to match both hosts

Verification

A 4-second generation now lands a valid MP4 whose size matches the mediaBlobSize Google reports. I also recovered 51 previously undownloadable videos by resolving their media IDs to fresh signed URLs — worth noting for anyone hit by this, since the media survives on Google's side and doesn't need regenerating.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions