{"record":{"id":"7bd0b4f6599752b1","repo":"vxcontrol/pentagi","slug":"failed-to-unmarshal-response-w","errorCode":null,"errorMessage":"failed to unmarshal response: %w","messagePattern":"failed to unmarshal response: %w","errorType":"exception","errorClass":"FatalError","httpStatus":null,"severity":"error","filePath":"backend/pkg/tools/searchers/perplexity.go","lineNumber":207,"sourceCode":"\t// retryable/fatal classification is decided here from the status code.\n\tif resp.StatusCode != http.StatusOK {\n\t\tbaseErr := p.handleErrorResponse(resp.StatusCode)\n\t\tif resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= 500 {\n\t\t\treturn \"\", Retryable(baseErr, 0)\n\t\t}\n\t\treturn \"\", Fatal(baseErr)\n\t}\n\n\t// Reading the response body\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn \"\", Retryable(fmt.Errorf(\"failed to read response body: %w\", err), 0)\n\t}\n\n\t// Deserializing the response\n\tvar response CompletionResponse\n\tif err := json.Unmarshal(body, &response); err != nil {\n\t\treturn \"\", Fatal(fmt.Errorf(\"failed to unmarshal response: %w\", err))\n\t}\n\n\t// Forming the result\n\tresult := p.formatResponse(ctx, &response, query)\n\treturn result, nil\n}\n\n// handleErrorResponse handles erroneous HTTP statuses\nfunc (p *perplexity) handleErrorResponse(statusCode int) error {\n\tswitch statusCode {\n\tcase http.StatusBadRequest:\n\t\treturn errors.New(\"request is invalid\")\n\tcase http.StatusUnauthorized:\n\t\treturn errors.New(\"API key is wrong\")\n\tcase http.StatusForbidden:\n\t\treturn errors.New(\"the endpoint requested is hidden for administrators only\")\n\tcase http.StatusNotFound:\n\t\treturn errors.New(\"the specified endpoint could not be found\")","sourceCodeStart":189,"sourceCodeEnd":225,"githubUrl":"https://github.com/vxcontrol/pentagi/blob/ea665308baaff015b226f308438a68d929d0f29b/backend/pkg/tools/searchers/perplexity.go#L189-L225","documentation":"This error is thrown by the Perplexity searcher when the HTTP response body returned from https://api.perplexity.ai/chat/completions cannot be deserialized into the CompletionResponse struct via json.Unmarshal. It is classified as Fatal (not retried) because a body that fails JSON parsing on a 200 response is unlikely to change on retry. Commonly it means the server returned non-JSON content (HTML error page, proxy login page, empty body) despite a 200 status.","triggerScenarios":"A POST to the Perplexity chat/completions endpoint returns HTTP 200 but the body is not valid JSON matching the expected schema — e.g. an HTML page injected by a corporate MITM proxy, a truncated response, or Perplexity returning a JSON error object that fails strict unmarshal into the struct fields.","commonSituations":"Corporate proxy/firewall or captive portal intercepting api.perplexity.ai traffic; Perplexity API deprecating or changing response fields so types no longer match; network middleboxes truncating large sonar-pro responses; using a wrong server URL override that points to an HTML page.","solutions":["Log the raw response body (string(body)) at the failure point to see what was actually returned","Verify no corporate proxy/MITM is rewriting api.perplexity.ai responses (check HTTPS_PROXY env, TLS interception certs)","Check Perplexity API changelog for response schema changes to model/choices/citations fields and update the CompletionResponse struct","Confirm the API key is valid — some invalid-credential paths return HTML instead of JSON","Retry the query; if transient truncation, the search orchestrator's fallback chain will try another engine"],"exampleFix":"// before\nvar response CompletionResponse\nif err := json.Unmarshal(body, &response); err != nil {\n    return \"\", Fatal(fmt.Errorf(\"failed to unmarshal response: %w\", err))\n}\n// after\nvar response CompletionResponse\nif err := json.Unmarshal(body, &response); err != nil {\n    return \"\", Fatal(fmt.Errorf(\"failed to unmarshal response (status %d, body %q): %w\", resp.StatusCode, string(body[:min(len(body), 512)]), err))\n}","handlingStrategy":"validation","validationCode":"// check Content-Type and empty body before unmarshal\nif ct := resp.Header.Get(\"Content-Type\"); !strings.Contains(ct, \"application/json\") {\n    return fmt.Errorf(\"perplexity returned non-JSON content type %q\", ct)\n}\nif len(body) == 0 {\n    return fmt.Errorf(\"perplexity returned empty body\")\n}\nvar response CompletionResponse\nif err := json.Unmarshal(body, &response); err != nil {\n    return fmt.Errorf(\"failed to unmarshal response (body %q): %w\", string(body[:min(len(body),512)]), err)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always log the raw response body on unmarshal failure to diagnose proxy injection","Verify API key validity before relying on the engine (IsAvailable only checks non-empty)","Test through corporate proxies; check for TLS interception affecting api.perplexity.ai","Pin and review Perplexity API changelog for response schema changes"],"tags":["json","http-response","parsing","perplexity"],"backgroundTag":"invalid-json-response","analyzedSha":"ea665308baaff015b226f308438a68d929d0f29b","analyzedAt":"2026-09-01T14:16:31.421Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}