vxcontrol/pentagi · error · FatalError

failed to unmarshal response

Error message

failed to unmarshal response: %w

What it means

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.

Solutions

  1. Log the raw response body (string(body)) at the failure point to see what was actually returned
  2. Verify no corporate proxy/MITM is rewriting api.perplexity.ai responses (check HTTPS_PROXY env, TLS interception certs)
  3. Check Perplexity API changelog for response schema changes to model/choices/citations fields and update the CompletionResponse struct
  4. Confirm the API key is valid — some invalid-credential paths return HTML instead of JSON
  5. Retry the query; if transient truncation, the search orchestrator's fallback chain will try another engine

Example fix

// before
var response CompletionResponse
if err := json.Unmarshal(body, &response); err != nil {
    return "", Fatal(fmt.Errorf("failed to unmarshal response: %w", err))
}
// after
var response CompletionResponse
if err := json.Unmarshal(body, &response); err != nil {
    return "", Fatal(fmt.Errorf("failed to unmarshal response (status %d, body %q): %w", resp.StatusCode, string(body[:min(len(body), 512)]), err))
}
Defensive patterns

Strategy: validation

Validate before calling

// check Content-Type and empty body before unmarshal
if ct := resp.Header.Get("Content-Type"); !strings.Contains(ct, "application/json") {
    return fmt.Errorf("perplexity returned non-JSON content type %q", ct)
}
if len(body) == 0 {
    return fmt.Errorf("perplexity returned empty body")
}
var response CompletionResponse
if err := json.Unmarshal(body, &response); err != nil {
    return fmt.Errorf("failed to unmarshal response (body %q): %w", string(body[:min(len(body),512)]), err)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.

Related errors


AI-assisted analysis of vxcontrol/pentagi@ea665308ba (2026-09-01). Data as JSON: /api/errors/7bd0b4f6599752b1. Report an issue: GitHub.

Appendix: source

Thrown at backend/pkg/tools/searchers/perplexity.go:207

	// retryable/fatal classification is decided here from the status code.
	if resp.StatusCode != http.StatusOK {
		baseErr := p.handleErrorResponse(resp.StatusCode)
		if resp.StatusCode == http.StatusTooManyRequests || resp.StatusCode >= 500 {
			return "", Retryable(baseErr, 0)
		}
		return "", Fatal(baseErr)
	}

	// Reading the response body
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", Retryable(fmt.Errorf("failed to read response body: %w", err), 0)
	}

	// Deserializing the response
	var response CompletionResponse
	if err := json.Unmarshal(body, &response); err != nil {
		return "", Fatal(fmt.Errorf("failed to unmarshal response: %w", err))
	}

	// Forming the result
	result := p.formatResponse(ctx, &response, query)
	return result, nil
}

// handleErrorResponse handles erroneous HTTP statuses
func (p *perplexity) handleErrorResponse(statusCode int) error {
	switch statusCode {
	case http.StatusBadRequest:
		return errors.New("request is invalid")
	case http.StatusUnauthorized:
		return errors.New("API key is wrong")
	case http.StatusForbidden:
		return errors.New("the endpoint requested is hidden for administrators only")
	case http.StatusNotFound:
		return errors.New("the specified endpoint could not be found")

View on GitHub (pinned to ea665308ba)