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:
- Generated media moved off
storage.googleapis.com onto flow-content.google.
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.
Summary
As of 2026-08-11, every video download fails.
POST /v1/videos/generationsreturns: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 installfrom source).Root cause
Two changes on Google's side, both of which
download_video()is blind to:storage.googleapis.comontoflow-content.google.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_statusstill reportsMEDIA_GENERATION_STATUS_SUCCESSFULcorrectly, and themedia[0].videoobject it returns carries only metadata (generatedVideo,dimensions,mediaBlobSize) — no URL and noencodedVideo. So the video is ready and its size is known, but the old fetch path can't reach the bytes.Adding
projectIdas 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
Omitting
mediaUrlTypeyields the full video;mediaUrlType=MEDIA_URL_TYPE_THUMBNAILyields 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: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/creditssucceeds 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.jsgates its tRPC interception on a hardcodedtext.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 amedia_urls_refreshmessage from the extension and cache signed URLs; addrequest_media_url()that asks the extension to follow the redirect endpoint and returns the resulting URL; log unrecognised extension messages instead of dropping themgenerators/common.py— try the signed URL first (proxy-free opener, mirroringdownload_image), keep the media API as a fallback; include the raw response when no video data is foundroutes/media.py— addGET /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 regeneratedExtension:
https://flow-content.google/*tohost_permissions(without itwebRequestcan't observe these requests at all)get_media_urlhandler that fetchesmedia.getMediaUrlRedirect, readsresp.urlafter the redirect, and cancels the body so the media isn't bufferedVerification
A 4-second generation now lands a valid MP4 whose size matches the
mediaBlobSizeGoogle 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.