{"record":{"id":"7d4c231307c0837f","repo":"chinabugotech/hutool","slug":"upload-failed","errorCode":null,"errorMessage":"Upload failed","messagePattern":"Upload failed","errorType":"exception","errorClass":"AIException","httpStatus":null,"severity":"error","filePath":"hutool-ai/src/main/java/cn/hutool/ai/model/gemini/GeminiServiceImpl.java","lineNumber":198,"sourceCode":"\t\t\t\t.header(\"x-goog-api-key\", config.getApiKey())\n\t\t\t\t.header(\"X-Goog-Upload-Protocol\", \"resumable\")\n\t\t\t\t.header(\"X-Goog-Upload-Command\", \"start\")\n\t\t\t\t.header(\"X-Goog-Upload-Header-Content-Length\", String.valueOf(file.length()))\n\t\t\t\t.header(\"X-Goog-Upload-Header-Content-Type\", mimeType)\n\t\t\t\t.body(metadata).execute();\n\n\t\t\tString sessionUrl = res.header(\"X-Goog-Upload-URL\");\n\n\t\t\t//上传二进制流\n\t\t\tfinal HttpResponse uploadRes = HttpRequest.put(sessionUrl)\n\t\t\t\t.header(\"X-Goog-Upload-Command\", \"upload, finalize\")\n\t\t\t\t.header(\"X-Goog-Upload-Offset\", \"0\")\n\t\t\t\t.body(FileUtil.readBytes(file)).execute();\n\n\t\t\t//返回 JSON，调用者可以从中解析出 file.uri\n\t\t\treturn uploadRes.body();\n\t\t} catch (Exception e) {\n\t\t\tthrow new AIException(\"Upload failed\", e);\n\t\t}\n\t}\n\n\n\t@Override\n\tpublic byte[] addWavHeader(final byte[] pcmData) {\n\t\tfinal int totalDataLen = pcmData.length;\n\t\tfinal int totalAudioLen = totalDataLen + 36;\n\t\t// Gemini TTS 默认通常是 24k 或 16k\n\t\tfinal int sampleRate = 24000;\n\t\t// 单声道\n\t\tfinal int channels = 1;\n\t\t// 16bit\n\t\tfinal int byteRate = sampleRate * channels * 2;\n\n\t\tfinal byte[] header = new byte[44];\n\t\theader[0] = 'R'; header[1] = 'I'; header[2] = 'F'; header[3] = 'F';\n\t\theader[4] = (byte) (totalAudioLen & 0xff);","sourceCodeStart":180,"sourceCodeEnd":216,"githubUrl":"https://github.com/chinabugotech/hutool/blob/8870454b2a0c29cc6ffd31dcf5667c8ceb2fc442/hutool-ai/src/main/java/cn/hutool/ai/model/gemini/GeminiServiceImpl.java#L180-L216","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect ex.getCause() for the real failure (IOException, IORuntimeException, or NPE from a null session URL).","Verify the API key is valid and has file-upload scope.","Confirm getUploadBaseUrl() returns the correct upload host for a custom/reverse-proxy apiUrl.","Check the start response actually contains the X-Goog-Upload-URL header before issuing the PUT.","Ensure the file is within Gemini's size limits (currently ~2GB per file) and the project quota is not exhausted."],"exampleFix":"// before\ngemini.uploadFile(hugeOrUnreadableFile);\n// -> Upload failed (cause hidden behind generic message)\n\n// after\ntry {\n    gemini.uploadFile(file);\n} catch (AIException e) {\n    Throwable root = e.getCause() != null ? e.getCause() : e;\n    log.error(\"upload root cause: {}\", root.toString());\n    // e.g. fix apiUrl via setApiUrl so getUploadBaseUrl() resolves correctly\n}","handlingStrategy":"try-catch","validationCode":"// Pre-check key + size + session header presence before the PUT\nif (StrUtil.isBlank(config.getApiKey())) throw new IllegalStateException(\"key missing\");\nif (file.length() > 2L * 1024 * 1024 * 1024) throw new IllegalStateException(\"file > 2GB\");","typeGuard":null,"tryCatchPattern":"try {\n    return gemini.uploadFile(file);\n} catch (AIException e) {\n    Throwable root = e.getCause() != null ? e.getCause() : e;\n    // root often: IORuntimeException(start rejected), NPE(null X-Goog-Upload-URL),\n    //            SocketException(PUT failed)\n    log.error(\"upload root: {}\", root.toString(), root);\n    throw e;\n}","preventionTips":["Inspect getCause() -- the message is generic.","Verify the start response includes X-Goog-Upload-URL before the PUT.","Confirm getUploadBaseUrl() is correct for custom/reverse-proxy apiUrl."],"tags":["gemini","upload","network","error-masking"],"backgroundTag":null,"analyzedSha":"8870454b2a0c29cc6ffd31dcf5667c8ceb2fc442","analyzedAt":"2026-08-14T04:01:12.892Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}