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
- Inspect the body embedded in the error — HTML means the endpoint is wrong or intercepted; 'unexpected end of JSON input' means truncation
- Verify the endpoint is exactly https://api.linear.to/graphql
- If truncation: shrink the query with pagination so the body fits under MaxResponseSize
- 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
- Point the client strictly at https://api.linear.to/graphql, never a UI route
- Keep queries paginated so bodies fit within MaxResponseSize (avoid truncation mid-JSON)
- Check proxies/WAFs/captive portals that substitute HTML for JSON
- Log the embedded body from the error to diagnose format issues quickly
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
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- parsing JSON: %w
- failed to parse gh output: %w
- parse gh output: %w
- failed to parse JSONL line: %w
- failed to parse memory record: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/5fbd70d61d10f1ee.
Report an issue: GitHub.