From 735b28850493ae82297f3502e6ba089b6bf20147 Mon Sep 17 00:00:00 2001 From: Oliver Jakoubek Date: Mon, 26 Jan 2026 20:35:21 +0100 Subject: [PATCH] fix: add HTML response body validation before JSON parsing Adds a backup check that validates the response body starts with '{' before attempting JSON unmarshal. This catches cases where: - Server returns HTML with incorrect Content-Type header - Reverse proxy/load balancer modifies headers but not body - PHP error pages with HTTP 200 status Includes a preview of the HTML content (up to 200 chars) for debugging. Co-Authored-By: Claude Opus 4.5 --- jsonrpc.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/jsonrpc.go b/jsonrpc.go index f1cedb2..6b0e026 100644 --- a/jsonrpc.go +++ b/jsonrpc.go @@ -4,9 +4,11 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net/http" + "strings" "sync/atomic" ) @@ -77,6 +79,10 @@ func (c *Client) call(ctx context.Context, method string, params interface{}, re resp, err := c.httpClient.Do(httpReq) if err != nil { + // Preserve specific errors that shouldn't be wrapped as connection failures + if errors.Is(err, ErrTooManyRedirects) { + return ErrTooManyRedirects + } return fmt.Errorf("%w: %v", ErrConnectionFailed, err) } defer resp.Body.Close() @@ -91,6 +97,12 @@ func (c *Client) call(ctx context.Context, method string, params interface{}, re return fmt.Errorf("unexpected HTTP status: %d", resp.StatusCode) } + // Check for HTML response (indicates redirect to login page or misconfiguration) + contentType := resp.Header.Get("Content-Type") + if strings.Contains(contentType, "text/html") { + return fmt.Errorf("%w: received HTML instead of JSON (possible redirect to login page)", ErrUnauthorized) + } + respBody, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read response body: %w", err) @@ -100,6 +112,19 @@ func (c *Client) call(ctx context.Context, method string, params interface{}, re c.logger.Debug("JSON-RPC response", "method", method, "body", string(respBody)) } + // Check if response body is HTML (backup check if Content-Type header is wrong/missing) + if len(respBody) > 0 { + trimmed := bytes.TrimLeft(respBody, " \t\n\r") + if len(trimmed) > 0 && trimmed[0] == '<' { + // Response is HTML, not JSON - extract a preview for debugging + preview := string(respBody) + if len(preview) > 200 { + preview = preview[:200] + "..." + } + return fmt.Errorf("server returned HTML instead of JSON (possible auth error or server error): %s", preview) + } + } + var rpcResp JSONRPCResponse if err := json.Unmarshal(respBody, &rpcResp); err != nil { return fmt.Errorf("failed to unmarshal response: %w", err)