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);
	}

	@Override

View on GitHub (pinned to 8870454b2a)

Solutions

  1. Download immediately after the operation completes, before the URI expires.
  2. Verify the API key (x-goog-api-key) is the same and still valid.
  3. Inspect response.getStatus() in the message: 403 -> key/permission, 404 -> wrong/expired URI.
  4. 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

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


AI-assisted analysis of chinabugotech/hutool@8870454b2a (2026-08-14). Data as JSON: /api/errors/236f5c589e09345c. Report an issue: GitHub.