Files
talk/internal/provider/httpclient.go
T

70 lines
2.5 KiB
Go
Raw Normal View History

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
}