chinabugotech/hutool · error · AIException
Failed to download media from URL:
Error message
Failed to download media from URL:
What it means
Thrown inside GeminiServiceImpl.buildMultimodalRequestMap when HttpUtil.downloadBytes(media) fails for an http(s) media URL. IMPORTANT library bug: it is constructed as new AIException("..." + media, e.getMessage()). Because e.getMessage() is a String (an Object, not a Throwable), Java resolves this to the (String messageTemplate, Object... params) overload, NOT (String, Throwable). The original exception e is therefore NOT attached as a cause and the root stack trace is lost, which makes debugging hard.
Source
Thrown at hutool-ai/src/main/java/cn/hutool/ai/model/gemini/GeminiServiceImpl.java:332
));
} else if (media.startsWith("http")) {
//普通网络图片 (下载并转 Base64)
try {
final byte[] bytes = HttpUtil.downloadBytes(media);
//尝试识别下载文件的 MIME,无法识别则不强加后缀逻辑,通过流内容自适应
String mime = FileUtil.getMimeType(media);
if (StrUtil.isBlank(mime)) {
// 基础兜底
mime = "image/jpeg";
}
parts.add(MapUtil.ofEntries(
MapUtil.entry("inline_data", MapUtil.ofEntries(
MapUtil.entry("mime_type", mime),
MapUtil.entry("data", Base64.encode(bytes))
))
));
} catch (Exception e) {
throw new AIException("Failed to download media from URL: " + media, e.getMessage());
}
} else {
//Base64 数据
parts.add(MapUtil.ofEntries(
MapUtil.entry("inline_data", MapUtil.ofEntries(
MapUtil.entry("mime_type", "image/jpeg"),
MapUtil.entry("data", media)
))
));
}
}
}
final Map<String, Object> paramMap = new HashMap<>();
paramMap.put("contents", Collections.singletonList(MapUtil.ofEntries(
MapUtil.entry("role", "user"),
MapUtil.entry("parts", parts)
)));View on GitHub (pinned to 8870454b2a)
Solutions
- Pre-download and validate the media yourself before the multimodal call (e.g. HttpUtil.downloadBytes then check length).
- Ensure the URL is reachable from the JVM host and returns binary content.
- For authed/private media, download with proper headers and pass the bytes as base64 inline data instead.
- Since the cause is lost (library bug), reproduce the download in isolation to see the real IOException.
Example fix
// before
String r = gemini.chat("describe this", List.of("https://broken.example/img.png"));
// -> Failed to download media from URL: ... (cause lost)
// after -- validate first, pass base64 on failure
byte[] bytes;
try { bytes = HttpUtil.downloadBytes(url); }
catch (Exception ex) { throw new IOException("cannot fetch " + url, ex); }
String b64 = media.startsWith("http") ? Base64.encode(bytes) : media;
String r = gemini.chat("describe this", List.of(b64)); Defensive patterns
Strategy: validation
Validate before calling
// Pre-download and validate media; pass base64 on success
byte[] bytes;
try {
bytes = HttpUtil.downloadBytes(mediaUrl);
} catch (Exception ex) {
throw new IllegalStateException("cannot fetch media " + mediaUrl, ex);
}
if (bytes == null || bytes.length == 0) throw new IllegalStateException("empty media");
String b64 = cn.hutool.core.codec.Base64.encode(bytes);
gemini.chat(prompt, List.of(b64)); Type guard
static boolean isHttpMedia(String m) {
return m != null && (m.startsWith("http://") || m.startsWith("https://"));
} Try / catch
try {
return gemini.chat(prompt, mediaList);
} catch (AIException e) {
if (e.getMessage().startsWith("Failed to download media from URL")) {
// NOTE: cause is LOST (varargs-overload bug) -- reproduce the download
// separately to see the real IOException.
}
throw e;
} Prevention
- Download media yourself with proper headers/error handling before the call.
- For authed media, pass downloaded bytes as base64 inline_data.
- Do not rely on the exception cause here -- it is dropped by the library.
When it happens
Trigger: Passing an http(s) image/media URL (not a Gemini files/ URI, not raw base64) to a multimodal call where the URL is unreachable, returns 404, has an invalid certificate, requires authentication, redirects too many times, or times out during download.
Common situations: Image host behind auth/CDN that needs headers hutool does not send; intranet URL not reachable from the server; typo in the URL; the media host returns HTML instead of binary; large image exceeding download timeout.
Related errors
- Failed to get remote file MIME type
- Upload failed
- Failed to send GET request:
- Failed to send POST request:
- TTS处理失败:
AI-assisted analysis of chinabugotech/hutool@8870454b2a (2026-08-14).
Data as JSON: /api/errors/a02143543b0edd3a.
Report an issue: GitHub.