Repository: langchain4j/langchain4j (2026-03-28T15:26:40Z)

Issue:
[BUG] GeminiStreamingResponseBuilder crashes with NPE/IOOBE when candidates is null or empty
## Describe the bug

`GeminiStreamingResponseBuilder.append()` accesses `partialResponse.candidates().get(0)` on line 62 without checking if `candidates()` is null or empty.

When the Gemini API returns a response with no candidates (e.g. due to safety filtering, quota errors, or malformed responses), this causes:
- `NullPointerException` if `candidates()` is null
- `IndexOutOfBoundsException` if `candidates()` is an empty list

## Log and Stack trace

```
java.lang.NullPointerException
    at dev.langchain4j.model.googleai.GeminiStreamingResponseBuilder.append(GeminiStreamingResponseBuilder.java:62)
```

or

```
java.lang.IndexOutOfBoundsException: Index 0 out of bounds for length 0
    at java.base/java.util.ImmutableCollections$ListN.get(ImmutableCollections.java:XXX)
    at dev.langchain4j.model.googleai.GeminiStreamingResponseBuilder.append(GeminiStreamingResponseBuilder.java:62)
```

## How To Reproduce

Send a request to the Gemini API via `GoogleAiGeminiStreamingChatModel` that triggers a safety filter or results in an empty candidates list. The streaming callback will crash instead of gracefully returning empty.

## Expected behavior

The `append()` method should check for null/empty candidates and return an empty `TextAndTools` result, consistent with how the Vertex AI Gemini module's `StreamingChatResponseBuilder` already handles this case (see `langchain4j-vertex-ai-gemini/.../StreamingChatResponseBuilder.java` line 30-31).

## Environment
- Langchain4j version: 1.0.0-beta1+
- Java version: 17+
- Spring Boot version: N/A



Discussion:

Patch:
diff --git a/langchain4j-google-ai-gemini/src/main/java/dev/langchain4j/model/googleai/GeminiStreamingResponseBuilder.java b/langchain4j-google-ai-gemini/src/main/java/dev/langchain4j/model/googleai/GeminiStreamingResponseBuilder.java
index 3bcf49b13ba..70ae8e55f0f 100644
--- a/langchain4j-google-ai-gemini/src/main/java/dev/langchain4j/model/googleai/GeminiStreamingResponseBuilder.java
+++ b/langchain4j-google-ai-gemini/src/main/java/dev/langchain4j/model/googleai/GeminiStreamingResponseBuilder.java
@@ -59,7 +59,12 @@ TextAndTools append(GeminiGenerateContentResponse partialResponse) {
             return new TextAndTools(Optional.empty(), Optional.empty(), List.of());
         }
 
-        GeminiCandidate firstCandidate = partialResponse.candidates().get(0);
+        List<GeminiCandidate> candidates = partialResponse.candidates();
+        if (candidates == null || candidates.isEmpty()) {
+            return new TextAndTools(Optional.empty(), Optional.empty(), List.of());
+        }
+
+        GeminiCandidate firstCandidate = candidates.get(0);
 
         updateId(partialResponse);
         updateModelName(partialResponse);


Test patch:
diff --git a/langchain4j-google-ai-gemini/src/test/java/dev/langchain4j/model/googleai/GeminiStreamingResponseBuilderTest.java b/langchain4j-google-ai-gemini/src/test/java/dev/langchain4j/model/googleai/GeminiStreamingResponseBuilderTest.java
new file mode 100644
index 00000000000..d5640ef603d
--- /dev/null
+++ b/langchain4j-google-ai-gemini/src/test/java/dev/langchain4j/model/googleai/GeminiStreamingResponseBuilderTest.java
@@ -0,0 +1,61 @@
+package dev.langchain4j.model.googleai;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import dev.langchain4j.model.googleai.GeminiGenerateContentResponse.GeminiCandidate;
+import dev.langchain4j.model.googleai.GeminiStreamingResponseBuilder.TextAndTools;
+import java.util.Collections;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class GeminiStreamingResponseBuilderTest {
+
+    private final GeminiStreamingResponseBuilder builder = new GeminiStreamingResponseBuilder(false, null);
+
+    @Test
+    void should_return_empty_when_partial_response_is_null() {
+        TextAndTools result = builder.append(null);
+
+        assertThat(result.maybeText()).isEmpty();
+        assertThat(result.maybeThought()).isEmpty();
+        assertThat(result.tools()).isEmpty();
+    }
+
+    @Test
+    void should_return_empty_when_candidates_is_null() {
+        GeminiGenerateContentResponse response = new GeminiGenerateContentResponse(null, null, null, null, null);
+
+        TextAndTools result = builder.append(response);
+
+        assertThat(result.maybeText()).isEmpty();
+        assertThat(result.maybeThought()).isEmpty();
+        assertThat(result.tools()).isEmpty();
+    }
+
+    @Test
+    void should_return_empty_when_candidates_is_empty() {
+        GeminiGenerateContentResponse response =
+                new GeminiGenerateContentResponse(null, null, Collections.emptyList(), null, null);
+
+        TextAndTools result = builder.append(response);
+
+        assertThat(result.maybeText()).isEmpty();
+        assertThat(result.maybeThought()).isEmpty();
+        assertThat(result.tools()).isEmpty();
+    }
+
+    @Test
+    void should_return_text_when_candidate_has_content() {
+        GeminiContent content = new GeminiContent(
+                List.of(new GeminiContent.GeminiPart("Hello", null, null, null, null, null, null, null, null, null)),
+                "model");
+        GeminiCandidate candidate = new GeminiCandidate(content, null, null, null);
+        GeminiGenerateContentResponse response =
+                new GeminiGenerateContentResponse("id-1", "gemini-pro", List.of(candidate), null, null);
+
+        TextAndTools result = builder.append(response);
+
+        assertThat(result.maybeText()).hasValue("Hello");
+        assertThat(result.tools()).isEmpty();
+    }
+}