{"record":{"id":"c6912030828993d1","repo":"chinabugotech/hutool","slug":"video-uri-is-empty","errorCode":null,"errorMessage":"Video URI is empty","messagePattern":"Video URI is empty","errorType":"validation","errorClass":"AIException","httpStatus":null,"severity":"error","filePath":"hutool-ai/src/main/java/cn/hutool/ai/model/gemini/GeminiServiceImpl.java","lineNumber":136,"sourceCode":"\n\t@Override\n\tpublic String predictVideo(String prompt) {\n\t\tfinal Map<String, Object> paramMap = buildPredictVideoRequestMap(prompt);\n\t\tfinal HttpResponse response = sendPost(getPredictVideoEndpoint(), JSONUtil.toJsonStr(paramMap));\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic String getVideoOperation(String operationName) {\n\t\tString endPoint = \"/\" + operationName;\n\t\tfinal HttpResponse response = sendGet(endPoint);\n\t\treturn response.body();\n\t}\n\n\t@Override\n\tpublic void downLoadVideo(String videoUri, String filePath) {\n\t\tif (StrUtil.isBlank(videoUri)) {\n\t\t\tthrow new AIException(\"Video URI is empty\");\n\t\t}\n\t\tfinal HttpResponse response = HttpRequest.get(videoUri)\n\t\t\t.header(\"x-goog-api-key\", config.getApiKey())\n\t\t\t.setFollowRedirects(true)\n\t\t\t.executeAsync();\n\t\tif (response.isOk()) {\n\t\t\tresponse.writeBody(FileUtil.file(filePath));\n\t\t} else {\n\t\t\tthrow new AIException(\"Download failed with status: \" + response.getStatus());\n\t\t}\n\t}\n\n\t@Override\n\tpublic String textToSpeech(String prompt) {\n\t\tfinal Map<String, Object> paramMap = buildTextToSpeechRequestMap(prompt);\n\t\tfinal HttpResponse response = sendPost(getEndpoint(false), JSONUtil.toJsonStr(paramMap));\n\t\treturn response.body();\n\t}","sourceCodeStart":118,"sourceCodeEnd":154,"githubUrl":"https://github.com/chinabugotech/hutool/blob/8870454b2a0c29cc6ffd31dcf5667c8ceb2fc442/hutool-ai/src/main/java/cn/hutool/ai/model/gemini/GeminiServiceImpl.java#L118-L154","documentation":"A precondition violation thrown by GeminiServiceImpl.downLoadVideo(videoUri, filePath) when videoUri is null, empty, or whitespace-only (StrUtil.isBlank). It is a guard before attempting the HTTP download, so no network call is made. Indicates the caller did not obtain a valid video resource URI from a completed generate-video operation.","triggerScenarios":"Calling downLoadVideo before the asynchronous video generation operation has produced a result URI; passing the operation name instead of the video URI; parsing the operation JSON incorrectly and extracting null for the videoUri field.","commonSituations":"Gemini video generation is asynchronous: predictVideo returns an operation name, getVideoOperation polls it, and only a done operation contains response.generateVideoResponse.samples.uri. Calling downLoadVideo with a blank value means the workflow skipped polling or parsed the wrong field.","solutions":["Poll getVideoOperation(operationName) until the operation JSON shows done == true.","Extract the URI from response.generateVideoResponse.samples[].uri (or the signed URL field your Gemini version returns).","Validate the extracted URI with StrUtil.isNotBlank before calling downLoadVideo.","Do not pass the operation name or the prompt as the videoUri."],"exampleFix":"// before\nString op = gemini.predictVideo(\"a cat\");\ngemini.downLoadVideo(null, \"/tmp/out.mp4\"); // -> Video URI is empty\n\n// after\nString opJson = gemini.predictVideo(\"a cat\");\nString opName = JSONUtil.parseObj(opJson).getStr(\"name\");\nString opResult;\ndo {\n    opResult = gemini.getVideoOperation(opName);\n} while (!JSONUtil.parseObj(opResult).getBool(\"done\", false));\nString uri = JSONUtil.parseObj(opResult)\n    .getByPath(\"response.generateVideoResponse.samples[0].uri\", String.class);\ngemini.downLoadVideo(uri, \"/tmp/out.mp4\");","handlingStrategy":"validation","validationCode":"// Ensure a non-blank URI from a completed operation before download\nif (StrUtil.isBlank(videoUri)) {\n    throw new IllegalStateException(\"video not ready: operation not done or uri missing\");\n}\ngemini.downLoadVideo(videoUri, filePath);","typeGuard":"// Narrow a parsed operation result to a downloadable URI\nstatic String extractVideoUri(String operationJson) {\n    JSONObject op = JSONUtil.parseObj(operationJson);\n    if (!op.getBool(\"done\", false)) return null;\n    return op.getByPath(\"response.generateVideoResponse.samples[0].uri\", String.class);\n}","tryCatchPattern":"try {\n    gemini.downLoadVideo(uri, path);\n} catch (AIException e) {\n    if (\"Video URI is empty\".equals(e.getMessage())) {\n        // re-poll the operation until done, then re-extract uri\n    }\n    throw e;\n}","preventionTips":["Always poll getVideoOperation until done == true before downloading.","Extract the URI from the documented JSON path.","Validate with StrUtil.isNotBlank before calling downLoadVideo."],"tags":["validation","gemini","video","precondition"],"backgroundTag":null,"analyzedSha":"8870454b2a0c29cc6ffd31dcf5667c8ceb2fc442","analyzedAt":"2026-08-14T04:01:12.892Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}