{"record":{"id":"1da26211112eb875","repo":"conductor-oss/conductor","slug":"empty-response-downloading-from-url","errorCode":null,"errorMessage":"Empty response downloading from {url}","messagePattern":"Empty response downloading from (.+?)","errorType":"exception","errorClass":"RuntimeException","httpStatus":null,"severity":"error","filePath":"ai/src/main/java/org/conductoross/conductor/ai/providers/gemini/GeminiVertex.java","lineNumber":193,"sourceCode":"                                    .data(Base64.getDecoder().decode(video.getB64Json()))\n                                    .mimeType(mimeType)\n                                    .build());\n                } else if (video.getUrl() != null) {\n                    byte[] bytes = downloadFromUrl(video.getUrl());\n                    mediaList.add(Media.builder().data(bytes).mimeType(mimeType).build());\n                }\n            }\n            builder.media(mediaList);\n        }\n\n        return builder.build();\n    }\n\n    private byte[] downloadFromUrl(String url) {\n        okhttp3.Request request = new okhttp3.Request.Builder().url(url).get().build();\n        try (okhttp3.Response response = httpClient.newCall(request).execute()) {\n            if (response.body() == null) {\n                throw new RuntimeException(\"Empty response downloading from \" + url);\n            }\n            return response.body().bytes();\n        } catch (RuntimeException e) {\n            throw e;\n        } catch (Exception e) {\n            throw new RuntimeException(\"Failed to download from \" + url, e);\n        }\n    }\n\n    @Override\n    public LLMResponse generateAudio(AudioGenRequest request) {\n        GeminiApi.SpeechConfig speechConfig =\n                new GeminiApi.SpeechConfig(\n                        new GeminiApi.VoiceConfig(\n                                new GeminiApi.PrebuiltVoiceConfig(request.getVoice())));\n        GeminiApi.GenerationConfig genConfig =\n                new GeminiApi.GenerationConfig(\n                        null,","sourceCodeStart":175,"sourceCodeEnd":211,"githubUrl":"https://github.com/conductor-oss/conductor/blob/cf7c3e4a8adfb158be778ab1ec525323c363cd3a/ai/src/main/java/org/conductoross/conductor/ai/providers/gemini/GeminiVertex.java#L175-L211","documentation":"GeminiVertex.downloadFromUrl() throws this when the OkHttp response body is null after downloading from a URL returned by the Gemini API for generated media (video, audio). A null body means the server responded but sent no content — typically because the signed URL has expired or the resource is no longer available. RuntimeException is then rethrown unchanged (the second catch block passes it through).","triggerScenarios":"The URL returned by Gemini for a generated video or audio resource returns an HTTP response with a null body. This happens when: the signed URL has expired (Google signed URLs have a limited TTL), the resource was garbage-collected after a delay between generation and download, or the CDN returned an empty response.","commonSituations":"A delay between video generation completion (checkVideoStatus returns COMPLETED) and the actual download of the media. The signed URL TTL expired during processing. The job was from a previous session and the URL is stale. Conductor restarted and is reprocessing old results.","solutions":["Download media immediately after the video/audio generation job completes — do not store the URL for later retrieval.","If the URL has expired, re-submit the generation job to get a fresh URL.","Cache the downloaded bytes (Conductor's checkVideoStatus already downloads and stores bytes), so repeated access doesn't re-fetch from the stale URL.","Increase the OkHttp timeout to ensure large media downloads complete before the URL expires."],"exampleFix":"// before — download later, URL may expire\nString videoUrl = response.getResults().get(0).getUrl();\n// ... store url, download hours later\nbyte[] bytes = downloadFromUrl(videoUrl); // null body\n\n// after — download immediately when status is COMPLETED\nif (\"COMPLETED\".equals(status)) {\n    byte[] bytes = downloadFromUrl(video.getUrl());\n    mediaList.add(Media.builder().data(bytes).mimeType(mime).build());\n}","handlingStrategy":"validation","validationCode":"// Validate the media URL before downloading\nvoid validateMediaUrl(String url) {\n    if (url == null || url.isBlank()) {\n        throw new IllegalArgumentException(\"Media URL is null or empty\");\n    }\n    // Check if this is likely a signed URL that may have expired\n    if (url.contains(\"Expires=\")) {\n        long expires = extractExpiry(url);\n        if (expires < System.currentTimeMillis() / 1000) {\n            throw new IllegalStateException(\n                \"Media URL has expired — re-generate to get a fresh URL\");\n        }\n    }\n}","typeGuard":null,"tryCatchPattern":"try {\n    byte[] media = vertex.downloadFromUrl(url);\n} catch (RuntimeException e) {\n    if (e.getMessage().startsWith(\"Empty response downloading from\")) {\n        throw new IllegalStateException(\n            \"Media URL returned empty response — likely expired. \"\n            + \"Re-generate the video/audio to get a fresh URL.\", e);\n    }\n    throw e;\n}","preventionTips":["Download media immediately after generation completes — do not store URLs for later retrieval.","Cache the downloaded bytes in Conductor's media storage (checkVideoStatus already does this) so subsequent access doesn't re-fetch.","If processing old results, re-submit the generation job to get a fresh signed URL rather than reusing a stale one.","Set generous OkHttp timeouts for large media files."],"tags":["gemini","download","empty-response","signed-url","vertex-ai"],"backgroundTag":null,"analyzedSha":"cf7c3e4a8adfb158be778ab1ec525323c363cd3a","analyzedAt":"2026-08-14T03:33:19.897Z","schemaVersion":2},"datasetVersion":"2026-08-14T05:17:29.042Z"}