gastownhall/beads · error

failed to parse response: %w (body: %s)

Error message

failed to parse response: %w (body: %s)

What it means

The 2xx response body from Linear could not be unmarshaled into the expected GraphQL envelope {data, errors} (internal/linear/client.go:422). The request and transport succeeded, but the payload is not the JSON shape the client expects. The error includes the JSON parse error and the offending body for diagnosis. Not retried — returned immediately with the last status code.

Source

Thrown at internal/linear/client.go:422

			lastErr = fmt.Errorf("rate limited (attempt %d/%d), retrying after %v", attempt+1, MaxRetries+1, delay)
			select {
			case <-ctx.Done():
				return nil, lastStatus, ctx.Err()
			case <-time.After(delay):
				continue
			}
		}

		if resp.StatusCode < 200 || resp.StatusCode >= 300 {
			return nil, lastStatus, fmt.Errorf("API error: %s (status %d)", string(respBody), resp.StatusCode)
		}

		var gqlResp struct {
			Data   json.RawMessage `json:"data"`
			Errors []GraphQLError  `json:"errors,omitempty"`
		}
		if err := json.Unmarshal(respBody, &gqlResp); err != nil {
			return nil, lastStatus, fmt.Errorf("failed to parse response: %w (body: %s)", err, string(respBody))
		}

		if len(gqlResp.Errors) > 0 {
			errMsgs := make([]string, len(gqlResp.Errors))
			for i, e := range gqlResp.Errors {
				errMsgs[i] = e.Message
			}
			return nil, lastStatus, fmt.Errorf("GraphQL errors: %s", strings.Join(errMsgs, "; "))
		}

		return gqlResp.Data, lastStatus, nil
	}

	return nil, lastStatus, fmt.Errorf("max retries (%d) exceeded: %w", MaxRetries+1, lastErr)
}

// FetchIssues retrieves issues from Linear with optional filtering by state.
// state can be: "open" (unstarted/started), "closed" (completed/canceled), or "all".

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the body embedded in the error — HTML means the endpoint is wrong or intercepted; 'unexpected end of JSON input' means truncation
  2. Verify the endpoint is exactly https://api.linear.to/graphql
  3. If truncation: shrink the query with pagination so the body fits under MaxResponseSize
  4. Check proxy/WAF (HTTPS_PROXY, corporate gateways) for body rewriting and allowlist api.linear.to

Example fix

// before: endpoint pointing at the web UI
client := linear.NewClient(key)
client.Endpoint = "https://linear.app" // returns HTML 200 page
_, err := client.Execute(ctx, req) // failed to parse response: invalid character '<' ...
// after: correct GraphQL endpoint
client.Endpoint = "https://api.linear.to/graphql"
_, err = client.Execute(ctx, req)
Defensive patterns

Strategy: validation

Validate before calling

func checkEndpoint() error {
    u, err := url.Parse(client.Endpoint)
    if err != nil { return err }
    if u.Host != "api.linear.to" || u.Path != "/graphql" {
        return fmt.Errorf("unexpected Linear endpoint: %s", client.Endpoint)
    }
    return nil
}

Type guard

func isParseFailure(err error) bool {
    return strings.Contains(err.Error(), "failed to parse response:")
}
func bodyLooksLikeHTML(body string) bool {
    t := strings.TrimSpace(body)
    return strings.HasPrefix(t, "<") || strings.Contains(t, "<html")
}

Try / catch

_, err := client.Execute(ctx, req)
if err != nil {
    if strings.Contains(err.Error(), "failed to parse response:") {
        if bodyLooksLikeHTML(err.Error()) {
            return fmt.Errorf("linear endpoint returned HTML — check LINEAR_API_URL/proxy: %w", err)
        }
        return fmt.Errorf("linear returned non-GraphQL body: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: json.Unmarshal fails on a 2xx body: an HTML login/interstitial page, a proxy or captive portal replacing the JSON, a truncated body cut off by MaxResponseSize mid-JSON, or a gateway returning 200 with an error page.

Common situations: Corporate proxies injecting auth pages, misconfigured LINEAR_API_URL pointing at a UI route instead of the GraphQL endpoint, response size caps truncating large payloads, or CDN/WAF interference.

Understand the failure class

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/5fbd70d61d10f1ee. Report an issue: GitHub.