sipeed/picoclaw · error

failed to create request: %w

Error message

failed to create request: %w

What it means

Returned by OpenAITTSProvider.doSpeechRequest when http.NewRequestWithContext rejects the POST URL. The constructor normalizes apiBase (default https://api.openai.com/v1/audio/speech, forcing the /audio/speech suffix), so failure means the configured base was still not a parseable absolute URL — missing scheme/host after normalization, or characters that break url.Parse.

Source

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

	responseFormat string,
) (io.ReadCloser, error) {
	reqBody := map[string]any{
		"model": t.model,
		"input": text,
		"voice": t.voice,
	}
	if responseFormat != "" {
		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,

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Set api_base to a full URL with scheme and host, e.g. https://api.openai.com/v1/audio/speech
  2. url.Parse the configured base at startup and reject invalid values early
  3. Log the final apiBase after NewOpenAITTSProvider to verify normalization

Example fix

// before
apiBase := `api.openai.com/v1` // normalization cannot add a scheme
// after
apiBase := `https://api.openai.com/v1`
if u, err := url.Parse(apiBase); err != nil || u.Scheme == `` || u.Host == `` {
    return fmt.Errorf(`invalid openai tts api_base: %s`, apiBase)
}
Defensive patterns

Strategy: validation

Validate before calling

u, err := url.Parse(strings.TrimSpace(apiBase))
if err != nil || u.Scheme == `` || u.Host == `` {
    return fmt.Errorf(`invalid openai tts api_base %q: must be an absolute URL`, apiBase)
}

Type guard

func isInvalidURL(err error) bool {
    return err != nil && strings.Contains(err.Error(), `invalid control character`) ||
        err != nil && strings.Contains(err.Error(), `missing protocol scheme`)
}

Try / catch

if err != nil {
    // configuration bug: fix api_base (full URL with scheme) before retrying
}

Prevention

When it happens

Trigger: api_base set to 'api.openai.com/v1' without https://; a base with control characters or invalid percent-escapes that falls through the string-based normalization fallback path.

Common situations: Environment/config mistakes where the base URL variable lacks the scheme; typos in local proxy URLs.

Related errors


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