fix: remove timeout handling for streaming requests.#648
fix: remove timeout handling for streaming requests.#648ainilili wants to merge 2 commits intogoogleapis:mainfrom
Conversation
|
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. |
|
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 |
|
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! |
|
Here is a minimal reproducible example that demonstrates the issue described in #649. Reproduction codepackage 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 outputnico@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 canceledThe stream is interrupted immediately after the first chunks, although the server is still sending data. Root cause
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 Let me know if you need anything else. |
fix #649
Problem
Currently, streaming requests apply timeout constraints from
HTTPOptions.TimeoutandClientConfig.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
sendStreamRequestfunction. Streaming requests should not be subject to client-side timeout constraints since: