gastownhall/beads · error

parse issue response: %w

Error message

parse issue response: %w

What it means

GetIssue wraps json.Unmarshal errors for the GET /issue/{key} response. The request succeeded but the body is not valid JSON or doesn't match the Issue struct (id, key, self, fields). Thrown also from CreateIssue's hydration step. Typically indicates an HTML/error body returned with a success status.

Source

Thrown at internal/jira/client.go:246

		}
		nextPageToken = result.NextPageToken
	}

	return allIssues, nil
}

// GetIssue fetches a single Jira issue by key (e.g., "PROJ-123").
func (c *Client) GetIssue(ctx context.Context, key string) (*Issue, error) {
	apiURL := fmt.Sprintf("%s/issue/%s?fields=%s", c.apiBase(), url.PathEscape(key), searchFields)

	body, err := c.doRequest(ctx, "GET", apiURL, nil)
	if err != nil {
		return nil, fmt.Errorf("get issue %s: %w", key, err)
	}

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

	return &issue, nil
}

// CreateIssue creates a new issue in Jira.
// fields should include "project", "summary", "issuetype", and optionally other fields.
func (c *Client) CreateIssue(ctx context.Context, fields map[string]interface{}) (*Issue, error) {
	payload := map[string]interface{}{"fields": fields}
	data, err := json.Marshal(payload)
	if err != nil {
		return nil, fmt.Errorf("marshal create request: %w", err)
	}

	apiURL := fmt.Sprintf("%s/issue", c.apiBase())

	body, err := c.doRequest(ctx, "POST", apiURL, data)
	if err != nil {

View on GitHub (pinned to 71377f2769)

Solutions

  1. Inspect the raw body (log a prefix of it) to identify HTML vs JSON vs unexpected schema.
  2. Confirm the base URL and APIVersion so requests hit /rest/api/N not a UI route.
  3. Test the endpoint with curl and the same auth to bypass/identify interception layers.
  4. Update struct tags if your Jira version changed the issue response shape.

Example fix

// before
var issue Issue
if err := json.Unmarshal(body, &issue); err != nil {
    return nil, fmt.Errorf("parse issue response: %w", err)
}
// after: surface body for diagnosis
if err := json.Unmarshal(body, &issue); err != nil {
    return nil, fmt.Errorf("parse issue response: %w (body: %.200s)", err, string(body))
}
Defensive patterns

Strategy: type-guard

Validate before calling

func isJSONBody(b []byte) bool {
    s := bytes.TrimLeft(b, " \t\r\n")
    return len(s) > 0 && (s[0] == '{' || s[0] == '[')
}

Type guard

func issueLooksValid(i *jira.Issue) bool {
    return i != nil && i.ID != "" && i.Key != ""
}

Try / catch

issue, err := client.GetIssue(ctx, key)
if err != nil && strings.Contains(err.Error(), "parse issue response") {
    // non-JSON 2xx body: check proxy/SSO interception and base URL
    log.Printf("issue fetch for %s returned unparseable body; bypass gateway and retry", key)
    return
}

Prevention

When it happens

Trigger: GET /issue/{key} returns 200 with a non-JSON body: proxy/SSO HTML page, an API-gateway error envelope, or a response shape incompatible with the Issue struct.

Common situations: Corporate proxies or captive portals rewriting responses; wrong base URL routed to a login page; Jira plugins modifying issue payloads; mismatched API version returning unexpected schema.

Understand the failure class

Background: "Invalid JSON response" and "Failed to parse response" errors: when an API answers 200 but the body isn't the JSON your library expected — this error's family across 28 libraries.

Related errors


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