vxcontrol/pentagi · error · FatalError

failed to decode response body: %v

Error message

failed to decode response body: %v

What it means

On HTTP 200, the response body is JSON-decoded into firecrawlSearchResult; a decode failure is Fatal because the engine answered but with an unreadable body — retrying immediately would likely reproduce it.

Source

Thrown at backend/pkg/tools/searchers/firecrawl.go:179

	req = req.WithContext(ctx)
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Authorization", "Bearer "+f.apiKey())

	resp, err := client.Do(req)
	if err != nil {
		return "", Retryable(fmt.Errorf("failed to do request: %v", err), 0)
	}
	defer resp.Body.Close()

	return f.parseHTTPResponse(ctx, query, resp)
}

func (f *firecrawl) parseHTTPResponse(ctx context.Context, query string, resp *http.Response) (string, error) {
	switch resp.StatusCode {
	case http.StatusOK:
		var respBody firecrawlSearchResult
		if err := json.NewDecoder(resp.Body).Decode(&respBody); err != nil {
			return "", Fatal(fmt.Errorf("failed to decode response body: %v", err))
		}
		if !respBody.Success {
			if respBody.Error != "" {
				return "", Fatal(fmt.Errorf("request failed: %s", respBody.Error))
			}
			return "", Fatal(fmt.Errorf("request failed"))
		}
		return f.buildFirecrawlResult(ctx, query, &respBody), nil
	case http.StatusBadRequest:
		return "", Fatal(fmt.Errorf("request is invalid"))
	case http.StatusUnauthorized:
		return "", Fatal(fmt.Errorf("API key is wrong"))
	case http.StatusPaymentRequired:
		return "", Fatal(fmt.Errorf("insufficient credits to perform this request"))
	case http.StatusForbidden:
		return "", Fatal(fmt.Errorf("the endpoint requested is hidden for administrators only"))
	case http.StatusNotFound:
		return "", Fatal(fmt.Errorf("the specified endpoint could not be found"))

View on GitHub (pinned to ea665308ba)

Solutions

  1. Log the raw body (first N bytes) at the failure point to see what was returned.
  2. Verify the request actually reached Firecrawl, not an auth wall of a proxy.
  3. Pin/upgrade to the Firecrawl API version matching the struct (v2).
  4. Retry once with a plain http.Client (no proxy) to rule out transport rewriting.

Example fix

// before
timeout int `json:"timeout"`
// after (if API now returns string)
timeout json.Number `json:"timeout"`
Defensive patterns

Strategy: validation

Validate before calling

body, _ := io.ReadAll(resp.Body)
if !json.Valid(body) {
  return fmt.Errorf("non-JSON response: %.200s", body)
}

Prevention

When it happens

Trigger: Engine returns 200 with non-JSON body (HTML error/login page from a proxy), truncated body, or an API schema change breaking field types (string vs object).

Common situations: Captive portal/proxy intercepting traffic, Firecrawl API version change altering the /v2/search response shape, gzip/charset handling issue via custom transport.

Understand the failure class

Related errors


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