package provider import ( "bytes" "context" "fmt" "io" "net/http" "time" ) // cloudTimeout ist das Budget für einen einzigen Synthese-Call eines // Cloud-Providers (Aufbau + Anfrage + vollständige Antwort). Ansagen sind // kurz; nach Ablauf greift der Fallback auf die nächste Ketten-Stufe. const cloudTimeout = 45 * time.Second // cloudClient ist der geteilte HTTP-Client der REST-Provider (mit Timeout, // im Gegensatz zu http.DefaultClient). Bei Ausbleiben einer Antwort greift // so der Ketten-Fallback statt einer Endlos-Blockade. var cloudClient = &http.Client{Timeout: cloudTimeout} // maxErrBody begrenzt, wie viel einer Fehler-Antwort in die Fehlermeldung // übernommen wird (Diagnose ja, Spam nein). const maxErrBody = 200 // postForBytes macht den gemeinsamen Kern der REST-Provider: POST von JSON // an url mit Headern, erwartet 2xx und liefert die rohen Antwort-Bytes // (Audio). Alles andere — non-2xx, Netz-Fehler, Timeouts, leere Antworten — // ist ein klarer Fehler, an dem die Kette in die nächste Stufe fällt. func postForBytes(ctx context.Context, client *http.Client, providerName, url string, headers map[string]string, body []byte) ([]byte, error) { if client == nil { client = cloudClient } // Eigenes Zeit-Budget setzen, falls der Aufrufer keins mitgibt — die // Kette soll auch ohne fremden Deadline-Kontext weiterfallen können. 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: Anfrage an %s bauen: %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: Anfrage an %s fehlgeschlagen (Netz/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 von %s: %s", providerName, resp.StatusCode, url, bytes.TrimSpace(snippet)) } data, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("%s: Antwort von %s lesen: %w", providerName, url, err) } if len(data) == 0 { return nil, fmt.Errorf("%s: leere Antwort von %s (0 Bytes Audio)", providerName, url) } return data, nil }