70 lines
2.5 KiB
Go
70 lines
2.5 KiB
Go
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
|
||
|
|
}
|