{"record":{"id":"ec54a3256a4695d4","repo":"Tencent/WeKnora","slug":"failed-to-read-exa-response-w","errorCode":null,"errorMessage":"failed to read Exa response: %w","messagePattern":"failed to read Exa response: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/infrastructure/web_search/exa.go","lineNumber":102,"sourceCode":"\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal Exa request: %w\", err)\n\t}\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, p.baseURL, bytes.NewReader(bodyBytes))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create Exa request: %w\", err)\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"x-api-key\", p.apiKey)\n\n\tlogger.Infof(ctx, \"[WebSearch][Exa] query=%q maxResults=%d url=%s\", query, maxResults, p.baseURL)\n\tresp, err := p.client.Do(req)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to execute Exa request: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\tbody, err := io.ReadAll(io.LimitReader(resp.Body, maxExaResponseBytes))\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to read Exa response: %w\", err)\n\t}\n\tif resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {\n\t\tlogger.Warnf(ctx, \"[WebSearch][Exa] API returned status %d: %s\", resp.StatusCode, string(body))\n\t\treturn nil, fmt.Errorf(\"exa API returned status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar data exaSearchResponse\n\tif err := json.Unmarshal(body, &data); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to unmarshal Exa response: %w\", err)\n\t}\n\tif data.Error != \"\" {\n\t\treturn nil, fmt.Errorf(\"exa API error: %s\", data.Error)\n\t}\n\n\tresults := make([]*types.WebSearchResult, 0, len(data.Results))\n\tfor _, item := range data.Results {\n\t\tif len(results) >= maxResults {\n\t\t\tbreak","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/infrastructure/web_search/exa.go#L84-L120","documentation":"Wraps errors from io.ReadAll when reading the Exa response body (bounded by maxExaResponseBytes via io.LimitReader). Fires on mid-stream connection resets, premature EOF, or body read failures after headers were received.","triggerScenarios":"Calling Search when the connection to Exa drops while the body is being read, a proxy terminates the stream, or the server closes the connection prematurely.","commonSituations":"Flaky networks or load balancers cutting long responses, idle-timeout at a proxy smaller than Exa's response time, very large result sets hitting connection limits, HTTP/2 stream resets.","solutions":["Retry the request — this is typically transient","Check proxy/LB idle and response timeouts versus Exa latency","Reduce maxResults to shrink response size","Verify maxExaResponseBytes is not causing interaction issues with compressed responses","Log the unwrapped error (unexpected EOF vs connection reset) to target the fix"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"// cannot be pre-validated; mitigate by bounding request size\nif maxResults > 20 {\n    maxResults = 20 // smaller responses reduce mid-body disconnections\n}","typeGuard":null,"tryCatchPattern":"results, err := provider.Search(ctx, query, 10, false)\nif err != nil {\n    if strings.Contains(err.Error(), \"failed to read Exa response\") ||\n       errors.Is(err, io.ErrUnexpectedEOF) {\n        return retryWithBackoff(ctx, query)\n    }\n    return nil, err\n}","preventionTips":["Bound maxResults to keep responses small","Align proxy/LB read timeouts with the HTTP client timeout","Prefer uncompressed or properly negotiated compression with LimitReader","Treat body-read failures as transient and retry idempotent searches"],"tags":["network","io","http-body","go"],"backgroundTag":"response-body-read-failed","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}