chinabugotech/hutool · error · AIException

Upload failed

Error message

Upload failed

What it means

Thrown by GeminiServiceImpl.uploadFile wrapping any exception during the two-step resumable upload (start request for the X-Goog-Upload-URL session URL, then PUT of the file bytes). The cause is preserved. Common underlying causes: the start request returned no X-Goog-Upload-URL header (auth failure or non-2xx), network failure on the PUT, file read error, or the upload base URL was computed incorrectly by getUploadBaseUrl.

Solutions

  1. Inspect ex.getCause() for the real failure (IOException, IORuntimeException, or NPE from a null session URL).
  2. Verify the API key is valid and has file-upload scope.
  3. Confirm getUploadBaseUrl() returns the correct upload host for a custom/reverse-proxy apiUrl.
  4. Check the start response actually contains the X-Goog-Upload-URL header before issuing the PUT.
  5. Ensure the file is within Gemini's size limits (currently ~2GB per file) and the project quota is not exhausted.

Example fix

// before
gemini.uploadFile(hugeOrUnreadableFile);
// -> Upload failed (cause hidden behind generic message)

// after
try {
    gemini.uploadFile(file);
} catch (AIException e) {
    Throwable root = e.getCause() != null ? e.getCause() : e;
    log.error("upload root cause: {}", root.toString());
    // e.g. fix apiUrl via setApiUrl so getUploadBaseUrl() resolves correctly
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-check key + size + session header presence before the PUT
if (StrUtil.isBlank(config.getApiKey())) throw new IllegalStateException("key missing");
if (file.length() > 2L * 1024 * 1024 * 1024) throw new IllegalStateException("file > 2GB");

Try / catch

try {
    return gemini.uploadFile(file);
} catch (AIException e) {
    Throwable root = e.getCause() != null ? e.getCause() : e;
    // root often: IORuntimeException(start rejected), NPE(null X-Goog-Upload-URL),
    //            SocketException(PUT failed)
    log.error("upload root: {}", root.toString(), root);
    throw e;
}

Prevention

When it happens

Trigger: Invalid x-goog-api-key so the start request is rejected; the start response lacks X-Goog-Upload-URL (then HttpRequest.put(nullUrl) -> malformed URL / NPE); network error while streaming the bytes; FileUtil.readBytes fails (permissions); file exceeds the Gemini file size/quota limit; a custom reverse-proxy apiUrl that breaks getUploadBaseUrl derivation.

Common situations: Key rotated between sessions; very large file hitting quota; reverse-proxy that strips the X-Goog-Upload-URL response header; running against a Vertex-compatible endpoint where the upload host differs.

Related errors


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

Appendix: source

Thrown at hutool-ai/src/main/java/cn/hutool/ai/model/gemini/GeminiServiceImpl.java:198

				.header("x-goog-api-key", config.getApiKey())
				.header("X-Goog-Upload-Protocol", "resumable")
				.header("X-Goog-Upload-Command", "start")
				.header("X-Goog-Upload-Header-Content-Length", String.valueOf(file.length()))
				.header("X-Goog-Upload-Header-Content-Type", mimeType)
				.body(metadata).execute();

			String sessionUrl = res.header("X-Goog-Upload-URL");

			//上传二进制流
			final HttpResponse uploadRes = HttpRequest.put(sessionUrl)
				.header("X-Goog-Upload-Command", "upload, finalize")
				.header("X-Goog-Upload-Offset", "0")
				.body(FileUtil.readBytes(file)).execute();

			//返回 JSON,调用者可以从中解析出 file.uri
			return uploadRes.body();
		} catch (Exception e) {
			throw new AIException("Upload failed", e);
		}
	}


	@Override
	public byte[] addWavHeader(final byte[] pcmData) {
		final int totalDataLen = pcmData.length;
		final int totalAudioLen = totalDataLen + 36;
		// Gemini TTS 默认通常是 24k 或 16k
		final int sampleRate = 24000;
		// 单声道
		final int channels = 1;
		// 16bit
		final int byteRate = sampleRate * channels * 2;

		final byte[] header = new byte[44];
		header[0] = 'R'; header[1] = 'I'; header[2] = 'F'; header[3] = 'F';
		header[4] = (byte) (totalAudioLen & 0xff);

View on GitHub (pinned to 8870454b2a)