package provider import ( "bytes" "context" "fmt" "io" "net/http" "time" ) // cloudTimeout is the budget for a single synthesis call of a cloud // provider (connect + request + complete response). Announcements are // short; once it expires the chain falls back to the next tier. const cloudTimeout = 45 * time.Second // cloudClient is the shared HTTP client of the REST providers (with a // timeout, unlike http.DefaultClient). If a response never arrives, the // chain falls back instead of blocking forever. var cloudClient = &http.Client{Timeout: cloudTimeout} // maxErrBody limits how much of an error response is copied into the error // message (diagnosis yes, spam no). const maxErrBody = 200 // postForBytes is the shared core of the REST providers: POST JSON to url // with headers, expect 2xx and return the raw response bytes (audio). // Everything else (non-2xx, network errors, timeouts, empty responses) is a // clear error on which the chain falls back to the next tier. func postForBytes(ctx context.Context, client *http.Client, providerName, url string, headers map[string]string, body []byte) ([]byte, error) { if client == nil { client = cloudClient } // Apply our own time budget if the caller does not bring one; the chain // must be able to fall through even without an external deadline. if _, hasDeadline := ctx.Deadline(); !hasDeadline { var cancel context.CancelFunc ctx, cancel = context.WithTimeout(ctx, cloudTimeout) defer cancel() } req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(body)) if err != nil { return nil, fmt.Errorf("%s: building request to %s: %w", providerName, url, err) } req.Header.Set("Content-Type", "application/json") for k, v := range headers { req.Header.Set(k, v) } resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("%s: request to %s failed (network/timeout?): %w", providerName, url, err) } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode > 299 { snippet, _ := io.ReadAll(io.LimitReader(resp.Body, maxErrBody)) return nil, fmt.Errorf("%s: HTTP %d from %s: %s", providerName, resp.StatusCode, url, bytes.TrimSpace(snippet)) } data, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("%s: reading response from %s: %w", providerName, url, err) } if len(data) == 0 { return nil, fmt.Errorf("%s: empty response from %s (0 bytes of audio)", providerName, url) } return data, nil }