gastownhall/beads · error

failed to parse issue response: %w

Error message

failed to parse issue response: %w

What it means

After FetchIssueByIID gets a successful HTTP response from /projects/:id/issues/:iid, the body is unmarshaled into the Issue struct. This error wraps the json.Unmarshal failure when the response body is not valid JSON or does not match the Issue struct shape. The HTTP call succeeded, so the problem is the payload content, not connectivity.

Source

Thrown at internal/gitlab/client.go:406

	var links []IssueLink
	if err := json.Unmarshal(respBody, &links); err != nil {
		return nil, fmt.Errorf("failed to parse issue links response: %w", err)
	}

	return links, nil
}

// FetchIssueByIID retrieves a single issue by its project-scoped IID.
func (c *Client) FetchIssueByIID(ctx context.Context, iid int) (*Issue, error) {
	urlStr := c.buildURL("/projects/"+c.projectPath()+"/issues/"+strconv.Itoa(iid), nil)
	respBody, _, err := c.doRequest(ctx, http.MethodGet, urlStr, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to fetch issue %d: %w", iid, err)
	}

	var issue Issue
	if err := json.Unmarshal(respBody, &issue); err != nil {
		return nil, fmt.Errorf("failed to parse issue response: %w", err)
	}

	return &issue, nil
}

// FetchMilestones retrieves milestones from the project with optional state filter.
// state can be: "active", "closed", or "" (all).
func (c *Client) FetchMilestones(ctx context.Context, state string) ([]Milestone, error) {
	params := map[string]string{
		"per_page": strconv.Itoa(MaxPageSize),
	}
	if state != "" {
		params["state"] = state
	}

	urlStr := c.buildURL("/projects/"+c.projectPath()+"/milestones", params)
	respBody, _, err := c.doRequest(ctx, http.MethodGet, urlStr, nil)
	if err != nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Capture and log the raw response body plus the wrapped *json.UnmarshalTypeError/json.SyntaxError to identify the mismatch
  2. Confirm the client base URL points at the GitLab API host (not a web UI or SSO redirect)
  3. Compare the payload against the Issue struct fields; update struct types/json tags to match your GitLab version
  4. Retry if the body could have been truncated by a transient gateway error

Example fix

// before
issue, err := client.FetchIssueByIID(ctx, iid)
// after
issue, err := client.FetchIssueByIID(ctx, iid)
if err != nil {
    var syn *json.SyntaxError
    if errors.As(err, &syn) {
        log.Printf("invalid JSON at offset %d — body likely HTML/empty", syn.Offset)
    }
    return err
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Confirm base URL is the API host before calling
if !strings.HasSuffix(strings.TrimSuffix(gitlabHost, "/"), "/api") && !strings.Contains(gitlabHost, "/api/") {
    // client should build /api/v4 paths itself; a web-UI URL will yield HTML
    log.Printf("warning: %s may not be an API host", gitlabHost)
}

Type guard

func isJSONParseError(err error) bool {
    var se *json.SyntaxError
    var te *json.UnmarshalTypeError
    return errors.As(err, &se) || errors.As(err, &te)
}

Try / catch

issue, err := client.FetchIssueByIID(ctx, iid)
if err != nil {
    if isJSONParseError(err) {
        // body was HTML/empty — inspect proxy config, not the issue ID
        return fmt.Errorf("non-JSON issue payload: %w", err)
    }
    return err
}
if issue == nil || issue.IID == 0 {
    return errors.New("empty issue payload")
}

Prevention

When it happens

Trigger: Calling Client.FetchIssueByIID when a proxy/SSO layer returns HTML instead of JSON, the body is empty or truncated, or a GitLab API change introduces fields whose types conflict with the Issue struct (e.g. string vs number).

Common situations: Corporate proxies injecting HTML error/login pages; self-hosted GitLab versions with divergent issue payloads; misconfigured base URL hitting a non-API route; gzip/encoding issues stripping the body.

Understand the failure class

Related errors


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