{"record":{"id":"38ad4e9ead223144","repo":"sipeed/picoclaw","slug":"http-d-s","errorCode":null,"errorMessage":"HTTP %d: %s","messagePattern":"HTTP (.+?): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/picoclaw/internal/model/online.go","lineNumber":52,"sourceCode":"\tclient := &http.Client{Timeout: 15 * time.Second}\n\treq, err := http.NewRequest(http.MethodGet, url, nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"build request: %w\", err)\n\t}\n\treq.Header.Set(\"Authorization\", \"Bearer \"+apiKey)\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"request failed: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, readErr := io.ReadAll(io.LimitReader(resp.Body, 512))\n\t\tif readErr != nil {\n\t\t\treturn nil, fmt.Errorf(\"read error response: %w\", readErr)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"HTTP %d: %s\", resp.StatusCode, strings.TrimSpace(string(body)))\n\t}\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"read response: %w\", err)\n\t}\n\n\t// {\"data\": [...]} envelope. Distinguish \"envelope shape with empty list\"\n\t// from \"object without a data key\" via Data being non-nil after unmarshal:\n\t// json.Unmarshal sets Data to []modelEntry{} for `{\"data\":[]}` but leaves\n\t// it as nil when \"data\" is absent or null.\n\tvar envelope modelsAPIResponse\n\tif err := json.Unmarshal(body, &envelope); err == nil && envelope.Data != nil {\n\t\treturn envelope.Data, nil\n\t}\n\n\t// Bare-array shape, including `[]`.\n\tvar arr []modelEntry","sourceCodeStart":34,"sourceCodeEnd":70,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/cmd/picoclaw/internal/model/online.go#L34-L70","documentation":"The provider answered with a non-200 status; the message embeds the numeric code plus up to 512 bytes of the response body. This is the server rejecting the request — auth, path, or quota — not a transport failure, and the body preview names the provider's reason.","triggerScenarios":"Any resp.StatusCode != 200 at online.go:52: 401/403 for a bad or revoked key, 404 when the api base path is wrong (e.g. /v1 present or missing versus what the server routes), 429 when rate-limited, 5xx on provider errors.","commonSituations":"API key revoked or issued for a different provider, api_base set with/without /v1 while the server expects the opposite, free-tier quota exhausted, or pointing the client at an endpoint that does not serve /models at all.","solutions":["Map the status: 401/403 -> fix the API key; 404 -> adjust the api base path (add or drop /v1); 429 -> wait or raise limits; 5xx -> retry later.","Read the embedded body preview — providers usually state the exact problem in it.","Reproduce with curl -H \"Authorization: Bearer $KEY\" <baseURL>/models to confirm independently of picoclaw.","Confirm the key belongs to the same account/endpoint as the api base."],"exampleFix":"// before: api_base = \"https://provider.example.com\" (server routes /v1/models -> 404)\n// after:  api_base = \"https://provider.example.com/v1\"","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"if err != nil && strings.HasPrefix(err.Error(), \"HTTP \") {\n    code, _ := strconv.Atoi(strings.Fields(err.Error())[1])\n    switch {\n    case code == 429 || code >= 500:\n        retryWithBackoff()\n    case code == 401 || code == 403:\n        failFast(\"credential rejected\")\n    case code == 404:\n        failFast(\"api base path is wrong\")\n    }\n}","preventionTips":["Smoke-test credentials and the base URL with curl before wiring them into config.","Treat 429/5xx as retryable and other 4xx as terminal in automation.","Alert on 401s — they usually mean a rotated key was not updated."],"tags":["http","api","auth","rate-limit","go"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}