chinabugotech/hutool · error · AIException
Download failed with status:
Error message
Download failed with status:
What it means
Thrown by GeminiServiceImpl.downLoadVideo after the async GET of videoUri returns a non-2xx status (response.isOk() is false). The message includes the HTTP status code. Unlike the transport exceptions, this is an application-level failure: the URI was reachable but the server refused (auth, not-found, expired link, etc.). Auth uses the x-goog-api-key header.
Source
Thrown at hutool-ai/src/main/java/cn/hutool/ai/model/gemini/GeminiServiceImpl.java:145
public String getVideoOperation(String operationName) {
String endPoint = "/" + operationName;
final HttpResponse response = sendGet(endPoint);
return response.body();
}
@Override
public void downLoadVideo(String videoUri, String filePath) {
if (StrUtil.isBlank(videoUri)) {
throw new AIException("Video URI is empty");
}
final HttpResponse response = HttpRequest.get(videoUri)
.header("x-goog-api-key", config.getApiKey())
.setFollowRedirects(true)
.executeAsync();
if (response.isOk()) {
response.writeBody(FileUtil.file(filePath));
} else {
throw new AIException("Download failed with status: " + response.getStatus());
}
}
@Override
public String textToSpeech(String prompt) {
final Map<String, Object> paramMap = buildTextToSpeechRequestMap(prompt);
final HttpResponse response = sendPost(getEndpoint(false), JSONUtil.toJsonStr(paramMap));
return response.body();
}
@Override
public String textToSpeech(String prompt, String voice) {
final Map<String, Object> voiceConfig = MapUtil.of("prebuilt_voice_config", MapUtil.of("voice_name", voice));
config.putAdditionalConfigByKey("speech_config", MapUtil.of("voice_config", voiceConfig));
return this.textToSpeech(prompt);
}
@OverrideView on GitHub (pinned to 8870454b2a)
Solutions
- Download immediately after the operation completes, before the URI expires.
- Verify the API key (x-goog-api-key) is the same and still valid.
- Inspect response.getStatus() in the message: 403 -> key/permission, 404 -> wrong/expired URI.
- Re-run predictVideo + getVideoOperation to obtain a fresh URI if it expired.
Example fix
// before
gemini.downLoadVideo(staleOrSharedUri, "/tmp/out.mp4");
// -> Download failed with status: 403
// after
// regenerate to get a fresh, same-key URI
String op = gemini.predictVideo("a cat");
String uri = pollUntilDoneAndGetUri(gemini, op);
gemini.downLoadVideo(uri, "/tmp/out.mp4"); Defensive patterns
Strategy: try-catch
Validate before calling
// Same-key, fresh-URI check before download
if (StrUtil.isBlank(config.getApiKey())) throw new IllegalStateException("key missing");
if (uriOlderThanMinutes(uri, 5)) throw new IllegalStateException("uri likely expired"); Try / catch
try {
gemini.downLoadVideo(uri, path);
} catch (AIException e) {
String m = e.getMessage();
if (m.startsWith("Download failed with status")) {
int status = Integer.parseInt(m.substring(m.lastIndexOf(' ')).trim());
if (status == 403 || status == 401) { /* fix key */ }
else if (status == 404) { /* uri expired/wrong -> regenerate */ }
}
throw e;
} Prevention
- Download immediately after the operation completes; URIs expire.
- Use the same API key for generate and download.
- Confirm the key has Generative Language API access.
When it happens
Trigger: Invalid or unauthorized Gemini API key (x-goog-api-key rejected -> 403); the video URI has expired or was already consumed; the URI is malformed leading to a 404; the resource belongs to a different project/key.
Common situations: Downloadable Gemini video URIs are short-lived -- delaying the download past expiry yields 4xx; rotating the API key between generate and download causes a mismatch; key without Generative Language API access.
Related errors
- Video URI is empty
- File not found!
- Upload failed
- Failed to download media from URL:
- Failed to get remote file MIME type
AI-assisted analysis of chinabugotech/hutool@8870454b2a (2026-08-14).
Data as JSON: /api/errors/236f5c589e09345c.
Report an issue: GitHub.