gastownhall/beads · error

failed to parse GraphQL response: %w

Error message

failed to parse GraphQL response: %w

What it means

graphqlRequest received an HTTP response from /api/graphql but the body could not be unmarshaled into the envelope {data, errors}. Unlike the REST path, GraphQL typically always returns valid JSON, so this usually indicates the response is not actually from the GraphQL endpoint — an HTML page, a redirect, or an empty/garbled body.

Source

Thrown at internal/gitlab/client.go:521

	if len(variables) > 0 {
		body["variables"] = variables
	}

	// GraphQL endpoint is at /api/graphql (not under /api/v4/)
	urlStr := c.BaseURL + "/api/graphql"
	respBody, _, err := c.doRequest(ctx, http.MethodPost, urlStr, body)
	if err != nil {
		return nil, fmt.Errorf("GraphQL request failed: %w", err)
	}

	var result struct {
		Data   json.RawMessage `json:"data"`
		Errors []struct {
			Message string `json:"message"`
		} `json:"errors"`
	}
	if err := json.Unmarshal(respBody, &result); err != nil {
		return nil, fmt.Errorf("failed to parse GraphQL response: %w", err)
	}
	if len(result.Errors) > 0 {
		return nil, fmt.Errorf("GraphQL error: %s", result.Errors[0].Message)
	}
	return result.Data, nil
}

// WorkItem represents a GitLab work item from the GraphQL API.
type WorkItem struct {
	ID    string `json:"id"`  // Global ID (gid://gitlab/WorkItem/123)
	IID   string `json:"iid"` // Project-scoped ID
	Title string `json:"title"`
	Type  string `json:"type"` // Work item type name
}

// defaultTaskTypeID is the fallback GID for older GitLab instances where the
// workItemTypes GraphQL query is unavailable.
const defaultTaskTypeID = "gid://gitlab/WorkItems::Type/5"

View on GitHub (pinned to 71377f2769)

Solutions

  1. Log respBody on parse failure to identify what was actually returned.
  2. Confirm the URL is <BaseURL>/api/graphql and not being redirected (check for 3xx handling).
  3. Bypass proxies/SSO for the API host or configure token-based auth headers doRequest sends.
  4. Retry the request if the network is known-flaky; a truncated body will also fail to parse.

Example fix

null
Defensive patterns

Strategy: retry

Validate before calling

// verify endpoint returns JSON before making real calls
req, _ := http.NewRequest(http.MethodPost, client.BaseURL+"/api/graphql", nil)
resp, err := http.DefaultClient.Do(req)
if err == nil && !strings.Contains(resp.Header.Get("Content-Type"), "json") {
    return fmt.Errorf("/api/graphql returned %s, not JSON", resp.Header.Get("Content-Type"))
}

Try / catch

data, err := client.GetWorkItemGID(ctx, projectPath)
if err != nil {
    if strings.Contains(err.Error(), "failed to parse GraphQL response") {
        // non-JSON body: retry once after delay, then surface config issue
    }
    return err
}

Prevention

When it happens

Trigger: Calls to getTaskWorkItemTypeID, CreateTaskWorkItem, or GetWorkItemGID where the server returns 200 with an HTML login page, a proxy error page, an empty body, or a non-JSON content type on /api/graphql.

Common situations: SSO/redirect interposing on the API; a load balancer returning a 502 HTML page with status 200 suppressed; hitting the wrong port where another service responds; truncated responses from a flaky network via doRequest not validating status.

Understand the failure class

Related errors


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