Skip to content

fix: remove timeout handling for streaming requests.#648

Open
ainilili wants to merge 2 commits intogoogleapis:mainfrom
ainilili:main
Open

fix: remove timeout handling for streaming requests.#648
ainilili wants to merge 2 commits intogoogleapis:mainfrom
ainilili:main

Conversation

@ainilili
Copy link
Copy Markdown

@ainilili ainilili commented Dec 11, 2025

fix #649

Problem

Currently, streaming requests apply timeout constraints from HTTPOptions.Timeout and ClientConfig.HTTPClient.Timeout, which is unnecessary and potentially problematic. Unlike unary requests that wait for a complete server response, streaming requests return immediately after establishing the connection and start receiving data chunks incrementally. The timeout mechanism designed for blocking operations doesn't apply to streaming scenarios.

Solution

Remove timeout handling from sendStreamRequest function. Streaming requests should not be subject to client-side timeout constraints since:

  1. The connection is established immediately and returns without waiting for server response
  2. Data is received incrementally through the stream
  3. The timeout would incorrectly interrupt an active stream that is still receiving data

@google-cla
Copy link
Copy Markdown

google-cla bot commented Dec 11, 2025

Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA).

View this failed invocation of the CLA check for more information.

For the most up to date status, view the checks section at the bottom of the pull request.

@ainilili ainilili changed the title fix: remove timeout handling for streaming requests. fix: remove timeout handling for streaming requests. #649 Dec 11, 2025
@ainilili ainilili changed the title fix: remove timeout handling for streaming requests. #649 fix: remove timeout handling for streaming requests. Dec 11, 2025
@Sivasankaran25 Sivasankaran25 self-assigned this Dec 12, 2025
@JonXSnow
Copy link
Copy Markdown

This fix directly removes the timeout processing logic, resulting in the timeout parameter not taking effect in the stream scenario. A more appropriate solution would be to generate the ctx of the timeout without the need for defer cancel

@ainilili
Copy link
Copy Markdown
Author

This fix directly removes the timeout processing logic, resulting in the timeout parameter not taking effect in the stream scenario. A more appropriate solution would be to generate the ctx of the timeout without the need for defer cancel

In reality, the timeout parameter still functions in streaming scenarios, but it relies on the server-side timeout check. When you configure HTTPOptions.timeout, an x-server-timeout parameter is added to the request header. If a timeout occurs, the server will return a DEADLINE_EXCEEDED error.

@BenjaminKazemi
Copy link
Copy Markdown
Collaborator

Thanks for your contribution. Could you add a sample code to replicate the issue? We can discuss and review your commit afterwards if you will. Thanks!

@ainilili
Copy link
Copy Markdown
Author

Hi @BenjaminKazemi

Here is a minimal reproducible example that demonstrates the issue described in #649.
The program simply opens a streaming GenerateContent request with a 30-second timeout, consumes a few chunks, and then always gets context canceled even though the stream is still healthy and the server keeps sending data.

Reproduction code

package main

import (
	"cloud.google.com/go/auth"
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"google.golang.org/genai"
	"log"
	"time"
)

const (
	serviceAccountKey = "service-account-json-base64" // TODO: replace with your service account JSON in base64 format
	project           = "your-gcp-project-id"         // TODO: replace with your GCP project ID
	location          = "global"
)

func main() {
	ctx := context.Background()

	// Create credentials from service account key
	credentials, err := createCredential(serviceAccountKey)
	if err != nil {
		log.Fatalf("failed to create credentials: %v", err)
	}

	// Create GenAI client
	cli, err := genai.NewClient(ctx, &genai.ClientConfig{
		Project:     project,
		Location:    location,
		Backend:     genai.BackendVertexAI,
		Credentials: credentials,
	})
	if err != nil {
		log.Fatalf("failed to create genai client: %v", err)
	}

	// Generate content stream
	timeout := time.Duration(30) * time.Second
	cfg := &genai.GenerateContentConfig{
		HTTPOptions: &genai.HTTPOptions{
			// Set timeout for the request
			// When the timeout is set, the stream will be canceled even if there is no timeout
			Timeout: &timeout,
		},
	}

	// Prepare input contents
	contents := []*genai.Content{
		{
			Role: "user",
			Parts: []*genai.Part{
				genai.NewPartFromText("Write a article about the benefits of AI in healthcare at least 100 words."),
			},
		},
	}

	// Call GenerateContentStream
	iter := cli.Models.GenerateContentStream(ctx, "gemini-2.5-flash", contents, cfg)

	// Process the streaming responses
	iter(func(resp *genai.GenerateContentResponse, err error) bool {
		if err != nil {
			log.Printf("gemini: generate content error: %v", err)
			return false
		}
		if len(resp.Candidates) > 0 {
			parts := resp.Candidates[0].Content.Parts
			for _, p := range parts {
				if t := p.Text; t != "" {
					delta := t
					log.Printf("gemini: received delta: %s", delta)
				}
			}
		}
		return true
	})
}

// createCredential creates Google Cloud credentials from a base64-encoded service account JSON key.
func createCredential(key string) (*auth.Credentials, error) {
	bs, err := base64.StdEncoding.DecodeString(key)
	if err != nil {
		return nil, err
	}

	var sa struct {
		ClientEmail string `json:"client_email"`
		PrivateKey  string `json:"private_key"`
		TokenURI    string `json:"token_uri"`
		ProjectID   string `json:"project_id"`
	}
	if err = json.Unmarshal(bs, &sa); err != nil {
		return nil, fmt.Errorf("invalid service-account JSON: %w", err)
	}

	tp, err := auth.New2LOTokenProvider(&auth.Options2LO{
		Email:      sa.ClientEmail,
		PrivateKey: []byte(sa.PrivateKey),
		TokenURL:   sa.TokenURI,
		Scopes:     []string{"https://www.googleapis.com/auth/cloud-platform "},
	})
	if err != nil {
		return nil, fmt.Errorf("failed to create 2LO token provider: %w", err)
	}

	return auth.NewCredentials(&auth.CredentialsOptions{
		TokenProvider: tp,
		JSON:          bs,
	}), nil
}

Actual output

nico@mbp issue649 % go run main.go
2025/12/13 09:58:48 gemini: received delta: ## AI: Revolutionizing Healthcare for a Healthier Future
...
2025/12/13 09:58:48 Error context canceled

The stream is interrupted immediately after the first chunks, although the server is still sending data.

Root cause

sendStreamRequest (simplified excerpt below) applies HTTPOptions.Timeout to the request context even for streaming calls:

func sendStreamRequest[T responseStream[R], R any](ctx context.Context, ac *apiClient, path string, method string, body map[string]any, httpOptions *HTTPOptions, output *responseStream[R]) error {
	req, httpOptions, err := buildRequest(ctx, ac, path, body, method, httpOptions)
	if err != nil {
		return err
	}

	// Handle context timeout.
	requestContext := ctx
	timeout := httpOptions.Timeout
	var cancel context.CancelFunc
	if timeout != nil && *timeout > 0*time.Second && isTimeoutBeforeDeadline(ctx, *timeout) {
		requestContext, cancel = context.WithTimeout(ctx, *timeout)
		defer cancel()
	}
	req = req.WithContext(requestContext)

	resp, err := doRequest(ac, req)
	if err != nil {
		return err
	}
	// resp.Body will be closed by the iterator
	return deserializeStreamResponse(resp, output)
}

Streaming requests return instantly after the connection is established; the timeout is therefore triggered while the iterator is still reading chunks, causing the premature context canceled.

Let me know if you need anything else.

@Sivasankaran25 Sivasankaran25 added the api:gemini-api Issues related to Gemini API label Jan 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

api:gemini-api Issues related to Gemini API

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Streaming requests aborted prematurely when HTTP timeout is set

4 participants