{"record":{"id":"9f1a2184f728ab1f","repo":"Tencent/WeKnora","slug":"keenable-api-returned-status-d-s","errorCode":null,"errorMessage":"keenable API returned status %d: %s","messagePattern":"keenable API returned status (.+?): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/infrastructure/web_search/keenable.go","lineNumber":107,"sourceCode":"\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Accept\", \"application/json\")\n\treq.Header.Set(\"X-Keenable-Title\", keenableTitle)\n\tif p.apiKey != \"\" {\n\t\treq.Header.Set(\"X-API-Key\", p.apiKey)\n\t}\n\n\tresp, err := p.client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode != http.StatusOK {\n\t\trespBody, _ := io.ReadAll(resp.Body)\n\t\tlogger.Warnf(ctx, \"[WebSearch][Keenable] API returned status %d: %s\", resp.StatusCode, string(respBody))\n\t\treturn nil, fmt.Errorf(\"keenable API returned status %d: %s\", resp.StatusCode, string(respBody))\n\t}\n\n\trespBody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read response: %w\", err)\n\t}\n\n\tvar respData keenableSearchResponse\n\tif err := json.Unmarshal(respBody, &respData); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to unmarshal response: %w\", err)\n\t}\n\n\tresults := make([]*types.WebSearchResult, 0, len(respData.Results))\n\tfor _, item := range respData.Results {\n\t\tif maxResults > 0 && len(results) >= maxResults {\n\t\t\tbreak\n\t\t}\n\t\t// Keenable returns both a short \"description\" and a longer \"snippet\"","sourceCodeStart":89,"sourceCodeEnd":125,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/infrastructure/web_search/keenable.go#L89-L125","documentation":"This error is returned by KeenableProvider.Search when the Keenable web search API responds with an HTTP status other than 200 OK. The provider reads the response body and embeds both the status code and the raw body in the message, so the text after the status tells you exactly what the API complained about (e.g. 401 unauthorized, 429 rate limit, 500 server error). It is a wrapper around a remote-API rejection, not a local bug.","triggerScenarios":"KeenableProvider.Search (internal/infrastructure/web_search/keenable.go:107) receives an HTTP response whose StatusCode != http.StatusOK — e.g. expired/invalid API key producing 401, exceeded quota producing 429, or upstream 5xx outages.","commonSituations":"Misconfigured or rotated-out API key in provider parameters; exhausted Keenable quota or billing lapse; Keenable service incident returning 5xx; firewall/proxy intercepting the request and returning a non-200 HTML page.","solutions":["Read the status code and body embedded in the error message to identify the root cause (401/403 = credentials, 429 = quota, 5xx = server side).","Verify the Keenable API key configured for the provider is valid and active; regenerate it if it was rotated.","Check your Keenable account quota/billing if the status is 429, and back off before retrying.","If the status is 5xx, retry later or check Keenable's status page / inspect the response body for an upstream incident.","Inspect the raw response body in the error for a provider-specific error code or HTML from an intercepting proxy."],"exampleFix":"// before: no handling of non-200 detail\nresults, err := provider.Search(ctx, query, 10, false)\nif err != nil { return err }\n\n// after: log status and body, branch on status class\nresults, err := provider.Search(ctx, query, 10, false)\nif err != nil {\n    var statusErr string = err.Error()\n    if strings.Contains(statusErr, \"401\") || strings.Contains(statusErr, \"403\") {\n        return fmt.Errorf(\"keenable credentials invalid, refresh API key: %w\", err)\n    }\n    return fmt.Errorf(\"search failed: %w\", err)\n}","handlingStrategy":"try-catch","validationCode":"if strings.TrimSpace(cfg.KeenableAPIKey) == \"\" {\n    return fmt.Errorf(\"keenable API key must be configured before searching\")\n}","typeGuard":null,"tryCatchPattern":"results, err := provider.Search(ctx, query, 10, false)\nif err != nil {\n    if strings.Contains(err.Error(), \"returned status 4\") {\n        // client-side: bad key or quota — alert, don't retry blindly\n        return nil, fmt.Errorf(\"keenable rejected request: %w\", err)\n    }\n    if strings.Contains(err.Error(), \"returned status 5\") {\n        // server-side: safe to retry with backoff\n        return retryWithBackoff(ctx, query)\n    }\n    return nil, err\n}","preventionTips":["Rotate and verify API keys on a schedule; validate them at startup with a cheap test call.","Monitor quota usage and alert before hitting 429.","Subscribe to provider status updates so 5xx incidents are recognized quickly.","Log the status code and body separately for alerting rather than parsing error strings."],"tags":["http","api","web-search","network"],"backgroundTag":"upstream-api-non-200-response","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}