sipeed/picoclaw · error

failed to send request: %w

Error message

failed to send request: %w

What it means

Returned by OpenAITTSProvider.doSpeechRequest when httpClient.Do fails before a response: DNS, refused connections, TLS errors, the 60s client timeout, proxy problems (the client is built via common.NewHTTPClient(proxyURL)), or context cancellation. Transport errors are not retried by the provider — only the response_format rejection path retries.

Source

Thrown at pkg/audio/tts/openai_tts.go:194

		reqBody["response_format"] = responseFormat
	}

	jsonData, err := json.Marshal(reqBody)
	if err != nil {
		return nil, fmt.Errorf("failed to marshal request: %w", err)
	}

	req, err := http.NewRequestWithContext(ctx, "POST", t.apiBase, bytes.NewReader(jsonData))
	if err != nil {
		return nil, fmt.Errorf("failed to create request: %w", err)
	}

	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+t.apiKey)

	resp, err := t.httpClient.Do(req)
	if err != nil {
		return nil, fmt.Errorf("failed to send request: %w", err)
	}

	if resp.StatusCode != http.StatusOK {
		defer resp.Body.Close()
		body, readErr := io.ReadAll(resp.Body)
		if readErr != nil {
			body = []byte(fmt.Sprintf("(failed to read error body: %v)", readErr))
		}
		return nil, &openAITTSAPIError{
			statusCode: resp.StatusCode,
			body:       string(body),
		}
	}

	return resp.Body, nil
}

func shouldRetryWithoutResponseFormat(body string) bool {

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Verify connectivity/proxy reachability to the apiBase host
  2. Split very long text into chunks or raise the client timeout at construction
  3. Retry transient transport failures with backoff
  4. Ensure the caller's context deadline exceeds expected synthesis time

Example fix

// before
stream, err := openaiTTS.Synthesize(ctx, bookChapter)
// after
var stream io.ReadCloser
for _, chunk := range chunkText(bookChapter, 4000) {
    s, err := openaiTTS.Synthesize(ctx, chunk)
    if err != nil {
        return err
    }
    stream = s
    break
}
Defensive patterns

Strategy: retry

Validate before calling

u, err := url.Parse(apiBase)
if err != nil {
    return fmt.Errorf(`invalid openai tts api_base: %w`, err)
}
host := u.Host
if !strings.Contains(host, `:`) {
    host += `:443`
}
conn, err := net.DialTimeout(`tcp`, host, 3*time.Second)
if err != nil {
    return fmt.Errorf(`tts endpoint unreachable: %w`, err)
}
_ = conn.Close()

Type guard

func isTransientTransport(err error) bool {
    var ue *url.Error
    return errors.As(err, &ue) && !errors.Is(err, context.Canceled)
}

Try / catch

err := retryN(3, 2*time.Second, func() error {
    var e error
    stream, e = provider.Synthesize(ctx, text)
    if e != nil && isTransientTransport(e) {
        return e
    }
    return nil
})

Prevention

When it happens

Trigger: api.openai.com (or the configured proxy) unreachable; synthesis of very long input exceeding the fixed 60s timeout; the caller's context canceled or its deadline shorter than synthesis time.

Common situations: Firewalled regions requiring a proxy; long texts (thousands of characters) timing out; short caller deadlines.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/285d6ede86934133. Report an issue: GitHub.