vxcontrol/pentagi · error · FatalError

failed to unmarshal response: %w

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.

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)

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.