{"record":{"id":"27076ff79a1ef489","repo":"vxcontrol/pentagi","slug":"failed-to-decode-response-body-v-27076f","errorCode":null,"errorMessage":"failed to decode response body: %v","messagePattern":"failed to decode response body: (.+?)","errorType":"exception","errorClass":"Fatal","httpStatus":null,"severity":"error","filePath":"backend/pkg/tools/searchers/tavily.go","lineNumber":146,"sourceCode":"\n\treq = req.WithContext(ctx)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", Retryable(fmt.Errorf(\"failed to do request: %v\", err), 0)\n\t}\n\tdefer resp.Body.Close()\n\n\treturn t.parseHTTPResponse(ctx, resp)\n}\n\nfunc (t *tavily) parseHTTPResponse(ctx context.Context, resp *http.Response) (string, error) {\n\tswitch resp.StatusCode {\n\tcase http.StatusOK:\n\t\tvar respBody tavilySearchResult\n\t\tif err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {\n\t\t\treturn \"\", Fatal(fmt.Errorf(\"failed to decode response body: %v\", err))\n\t\t}\n\t\treturn t.buildTavilyResult(ctx, &respBody), nil\n\tcase http.StatusBadRequest:\n\t\treturn \"\", Fatal(fmt.Errorf(\"request is invalid\"))\n\tcase http.StatusUnauthorized:\n\t\treturn \"\", Fatal(fmt.Errorf(\"API key is wrong\"))\n\tcase http.StatusForbidden:\n\t\treturn \"\", Fatal(fmt.Errorf(\"the endpoint requested is hidden for administrators only\"))\n\tcase http.StatusNotFound:\n\t\treturn \"\", Fatal(fmt.Errorf(\"the specified endpoint could not be found\"))\n\tcase http.StatusMethodNotAllowed:\n\t\treturn \"\", Fatal(fmt.Errorf(\"there need to try to access an endpoint with an invalid method\"))\n\tcase http.StatusTooManyRequests:\n\t\treturn \"\", Retryable(fmt.Errorf(\"there are requesting too many results\"), 0)\n\tcase http.StatusInternalServerError:\n\t\treturn \"\", Retryable(fmt.Errorf(\"there had a problem with our server. try again later\"), 0)\n\tcase http.StatusBadGateway:\n\t\treturn \"\", Retryable(fmt.Errorf(\"there was a problem with the server. Please try again later\"), 0)","sourceCodeStart":128,"sourceCodeEnd":164,"githubUrl":"https://github.com/vxcontrol/pentagi/blob/ea665308baaff015b226f308438a68d929d0f29b/backend/pkg/tools/searchers/tavily.go#L128-L164","documentation":"Thrown in tavily.parseHTTPResponse when Tavily answered HTTP 200 but the body could not be JSON-decoded into tavilySearchResult (json.NewDecoder(resp.Body).Decode failed). It is classified Fatal, so no retry or engine fallback from the error alone — the response is considered unusable. Usually means the body is HTML (error page/proxy block page), empty, or truncated.","triggerScenarios":"Tavily returns 200 with a non-JSON body: an intercepting proxy or captive portal HTML page, an empty body, a truncated response (connection cut mid-body), or an unexpected schema change causing json.UnmarshalTypeError.","commonSituations":"Corporate proxy injecting an HTML error/auth page with status 200; gzip/encoding issues when a custom transport strips Content-Encoding handling; Tavily API response schema drift after an API update; container memory/conn limits truncating the body.","solutions":["Log/inspect the raw response body on decode failure (read a bounded prefix with io.LimitReader) to see what actually arrived.","Check proxy/MITM appliances in the path — an HTML login or block page with status 200 is the classic cause.","Re-run the same query with curl against api.tavily.com/search to compare the real body shape.","Check Tavily API changelog for schema changes to the /search response and update the tavilySearchResult/tavilyResult structs.","Consider reclassifying as Retryable for truncated-body cases (io.ErrUnexpectedEOF) so the orchestrator can fall back to another engine."],"exampleFix":"// before\nif err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {\n    return \"\", Fatal(fmt.Errorf(\"failed to decode response body: %v\", err))\n}\n// after\nraw, _ := io.ReadAll(io.LimitReader(resp.Body, 4096))\nif err := json.Unmarshal(raw, &respBody); err != nil {\n    return \"\", Fatal(fmt.Errorf(\"failed to decode response body: %v (body prefix: %q)\", err, string(raw[:min(len(raw), 256)])))\n}","handlingStrategy":"type-guard","validationCode":"// verify the body looks like JSON before decoding\nbody, _ := io.ReadAll(io.LimitReader(resp.Body, 1<<20))\nif !json.Valid(body) {\n    return \"\", Fatal(fmt.Errorf(\"non-JSON response body\"))\n}","typeGuard":"func isValidTavilyResult(v any) bool {\n    r, ok := v.(*tavilySearchResult)\n    return ok && r != nil // schema fields are all optional per struct; presence of a parseable object is the gate\n}","tryCatchPattern":"if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {\n    var serr *json.SyntaxError\n    if errors.As(err, &serr) {\n        // log body prefix + offset for diagnosis; fall back to another engine\n    }\n    return \"\", Fatal(err)\n}","preventionTips":["Inspect raw bodies through proxies — 200-with-HTML is the signature of a MITM/captive portal.","Log a bounded body prefix on every decode failure to speed up diagnosis.","Pin and review Tavily API changelogs when upgrading.","Avoid custom HTTP transports that mishandle Content-Encoding."],"tags":["json","http-response","searchers","fatal","go"],"backgroundTag":"json-decode-failed","analyzedSha":"ea665308baaff015b226f308438a68d929d0f29b","analyzedAt":"2026-09-01T14:16:31.421Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}