{"record":{"id":"762de5dceb3c828e","repo":"sipeed/picoclaw","slug":"failed-to-create-request-w-762de5","errorCode":null,"errorMessage":"failed to create request: %w","messagePattern":"failed to create request: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/audio/tts/openai_tts.go","lineNumber":186,"sourceCode":"\tresponseFormat string,\n) (io.ReadCloser, error) {\n\treqBody := map[string]any{\n\t\t\"model\": t.model,\n\t\t\"input\": text,\n\t\t\"voice\": t.voice,\n\t}\n\tif responseFormat != \"\" {\n\t\treqBody[\"response_format\"] = responseFormat\n\t}\n\n\tjsonData, err := json.Marshal(reqBody)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal request: %w\", err)\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, \"POST\", t.apiBase, bytes.NewReader(jsonData))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+t.apiKey)\n\n\tresp, err := t.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to send request: %w\", err)\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tdefer resp.Body.Close()\n\t\tbody, readErr := io.ReadAll(resp.Body)\n\t\tif readErr != nil {\n\t\t\tbody = []byte(fmt.Sprintf(\"(failed to read error body: %v)\", readErr))\n\t\t}\n\t\treturn nil, &openAITTSAPIError{\n\t\t\tstatusCode: resp.StatusCode,","sourceCodeStart":168,"sourceCodeEnd":204,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/audio/tts/openai_tts.go#L168-L204","documentation":"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.","triggerScenarios":"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.","commonSituations":"Environment/config mistakes where the base URL variable lacks the scheme; typos in local proxy URLs.","solutions":["Set api_base to a full URL with scheme and host, e.g. https://api.openai.com/v1/audio/speech","url.Parse the configured base at startup and reject invalid values early","Log the final apiBase after NewOpenAITTSProvider to verify normalization"],"exampleFix":"// before\napiBase := `api.openai.com/v1` // normalization cannot add a scheme\n// after\napiBase := `https://api.openai.com/v1`\nif u, err := url.Parse(apiBase); err != nil || u.Scheme == `` || u.Host == `` {\n    return fmt.Errorf(`invalid openai tts api_base: %s`, apiBase)\n}","handlingStrategy":"validation","validationCode":"u, err := url.Parse(strings.TrimSpace(apiBase))\nif err != nil || u.Scheme == `` || u.Host == `` {\n    return fmt.Errorf(`invalid openai tts api_base %q: must be an absolute URL`, apiBase)\n}","typeGuard":"func isInvalidURL(err error) bool {\n    return err != nil && strings.Contains(err.Error(), `invalid control character`) ||\n        err != nil && strings.Contains(err.Error(), `missing protocol scheme`)\n}","tryCatchPattern":"if err != nil {\n    // configuration bug: fix api_base (full URL with scheme) before retrying\n}","preventionTips":["Configure api_base as a complete URL including https:// and /v1","url.Parse the base at startup and fail fast","Log the normalized apiBase after provider construction to confirm the endpoint"],"tags":["config","url","tts","openai","http"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}