{"record":{"id":"a02143543b0edd3a","repo":"chinabugotech/hutool","slug":"failed-to-download-media-from-url","errorCode":null,"errorMessage":"Failed to download media from URL: ","messagePattern":"Failed to download media from URL: ","errorType":"exception","errorClass":"AIException","httpStatus":null,"severity":"error","filePath":"hutool-ai/src/main/java/cn/hutool/ai/model/gemini/GeminiServiceImpl.java","lineNumber":332,"sourceCode":"\t\t\t\t\t));\n\t\t\t\t} else if (media.startsWith(\"http\")) {\n\t\t\t\t\t//普通网络图片 (下载并转 Base64)\n\t\t\t\t\ttry {\n\t\t\t\t\t\tfinal byte[] bytes = HttpUtil.downloadBytes(media);\n\t\t\t\t\t\t//尝试识别下载文件的 MIME，无法识别则不强加后缀逻辑，通过流内容自适应\n\t\t\t\t\t\tString mime = FileUtil.getMimeType(media);\n\t\t\t\t\t\tif (StrUtil.isBlank(mime)) {\n\t\t\t\t\t\t\t// 基础兜底\n\t\t\t\t\t\t\tmime = \"image/jpeg\";\n\t\t\t\t\t\t}\n\t\t\t\t\t\tparts.add(MapUtil.ofEntries(\n\t\t\t\t\t\t\tMapUtil.entry(\"inline_data\", MapUtil.ofEntries(\n\t\t\t\t\t\t\t\tMapUtil.entry(\"mime_type\", mime),\n\t\t\t\t\t\t\t\tMapUtil.entry(\"data\", Base64.encode(bytes))\n\t\t\t\t\t\t\t))\n\t\t\t\t\t\t));\n\t\t\t\t\t} catch (Exception e) {\n\t\t\t\t\t\tthrow new AIException(\"Failed to download media from URL: \" + media, e.getMessage());\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\t//Base64 数据\n\t\t\t\t\tparts.add(MapUtil.ofEntries(\n\t\t\t\t\t\tMapUtil.entry(\"inline_data\", MapUtil.ofEntries(\n\t\t\t\t\t\t\tMapUtil.entry(\"mime_type\", \"image/jpeg\"),\n\t\t\t\t\t\t\tMapUtil.entry(\"data\", media)\n\t\t\t\t\t\t))\n\t\t\t\t\t));\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfinal Map<String, Object> paramMap = new HashMap<>();\n\t\tparamMap.put(\"contents\", Collections.singletonList(MapUtil.ofEntries(\n\t\t\tMapUtil.entry(\"role\", \"user\"),\n\t\t\tMapUtil.entry(\"parts\", parts)\n\t\t)));","sourceCodeStart":314,"sourceCodeEnd":350,"githubUrl":"https://github.com/chinabugotech/hutool/blob/8870454b2a0c29cc6ffd31dcf5667c8ceb2fc442/hutool-ai/src/main/java/cn/hutool/ai/model/gemini/GeminiServiceImpl.java#L314-L350","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nString r = gemini.chat(\"describe this\", List.of(\"https://broken.example/img.png\"));\n// -> Failed to download media from URL: ... (cause lost)\n\n// after -- validate first, pass base64 on failure\nbyte[] bytes;\ntry { bytes = HttpUtil.downloadBytes(url); }\ncatch (Exception ex) { throw new IOException(\"cannot fetch \" + url, ex); }\nString b64 = media.startsWith(\"http\") ? Base64.encode(bytes) : media;\nString r = gemini.chat(\"describe this\", List.of(b64));","handlingStrategy":"validation","validationCode":"// Pre-download and validate media; pass base64 on success\nbyte[] bytes;\ntry {\n    bytes = HttpUtil.downloadBytes(mediaUrl);\n} catch (Exception ex) {\n    throw new IllegalStateException(\"cannot fetch media \" + mediaUrl, ex);\n}\nif (bytes == null || bytes.length == 0) throw new IllegalStateException(\"empty media\");\nString b64 = cn.hutool.core.codec.Base64.encode(bytes);\ngemini.chat(prompt, List.of(b64));","typeGuard":"static boolean isHttpMedia(String m) {\n    return m != null && (m.startsWith(\"http://\") || m.startsWith(\"https://\"));\n}","tryCatchPattern":"try {\n    return gemini.chat(prompt, mediaList);\n} catch (AIException e) {\n    if (e.getMessage().startsWith(\"Failed to download media from URL\")) {\n        // NOTE: cause is LOST (varargs-overload bug) -- reproduce the download\n        // separately to see the real IOException.\n    }\n    throw e;\n}","preventionTips":["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."],"tags":["gemini","multimodal","network","error-masking","library-bug"],"backgroundTag":null,"analyzedSha":"8870454b2a0c29cc6ffd31dcf5667c8ceb2fc442","analyzedAt":"2026-08-14T04:01:12.892Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}