{"record":{"id":"cf35d377442fbd28","repo":"gastownhall/beads","slug":"fetch-issue-s-w","errorCode":null,"errorMessage":"fetch issue %s: %w","messagePattern":"fetch issue (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/jira/client.go","lineNumber":141,"sourceCode":"\n// apiBase returns the versioned REST API base URL, e.g. \"https://host/rest/api/3\".\nfunc (c *Client) apiBase() string {\n\tv := c.APIVersion\n\tif v == \"\" {\n\t\tv = \"3\"\n\t}\n\treturn c.URL + \"/rest/api/\" + v\n}\n\n// FetchIssueTimestamp fetches the updated timestamp for a single Jira issue.\nfunc (c *Client) FetchIssueTimestamp(ctx context.Context, jiraKey string) (time.Time, error) {\n\tvar zero time.Time\n\n\tapiURL := fmt.Sprintf(\"%s/issue/%s?fields=updated\", c.apiBase(), url.PathEscape(jiraKey))\n\n\tbody, err := c.doRequest(ctx, \"GET\", apiURL, nil)\n\tif err != nil {\n\t\treturn zero, fmt.Errorf(\"fetch issue %s: %w\", jiraKey, err)\n\t}\n\n\tvar result struct {\n\t\tFields struct {\n\t\t\tUpdated string `json:\"updated\"`\n\t\t} `json:\"fields\"`\n\t}\n\n\tif err := json.Unmarshal(body, &result); err != nil {\n\t\treturn zero, fmt.Errorf(\"parse Jira response: %w\", err)\n\t}\n\n\tupdated, err := ParseTimestamp(result.Fields.Updated)\n\tif err != nil {\n\t\treturn zero, fmt.Errorf(\"parse Jira timestamp: %w\", err)\n\t}\n\n\treturn updated, nil","sourceCodeStart":123,"sourceCodeEnd":159,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/jira/client.go#L123-L159","documentation":"FetchIssueTimestamp performs a GET to {apiBase}/issue/{key}?fields=updated and wraps any transport/HTTP error from doRequest as 'fetch issue %s: %w' with the Jira issue key. The wrapped cause carries the real reason (network failure, 401/403, 404, rate limit).","triggerScenarios":"Calling FetchIssueTimestamp when the Jira instance is unreachable, credentials are invalid, the issue key does not exist (or PathEscape-mangled key is malformed), or the API base URL is wrong.","commonSituations":"Expired or rotated Jira API tokens; VPN/network outage; wrong jiraBase configured so apiBase points to a non-existent REST path; issue deleted or key typo'd; Jira rate limiting (429).","solutions":["Inspect the wrapped cause: fix auth (token/credentials) if 401/403, or the URL/base if 404","Verify network connectivity and VPN access to the Jira host","Confirm the issue key is correct and the issue still exists in Jira","Retry on transient failures (429/5xx), honoring Jira rate-limit headers"],"exampleFix":"// before\nt, err := client.FetchIssueTimestamp(ctx, \"WRONG-999\")\n// after\n// verify key exists, then handle/retry\nt, err := client.FetchIssueTimestamp(ctx, \"PROJ-123\")\nif err != nil {\n    var retriable = errors.Is(err, context.DeadlineExceeded) // inspect cause\n    _ = retriable\n}","handlingStrategy":"retry","validationCode":"// verify reachability before calling\nresp, err := http.Get(jiraBase + \"/rest/api/2/myself\")\nif err != nil || resp.StatusCode >= 400 { /* fix auth/url first */ }","typeGuard":null,"tryCatchPattern":"t, err := client.FetchIssueTimestamp(ctx, key)\nif err != nil {\n    var we interface{ Temporary() bool }\n    if errors.As(err, &we) && we.Temporary() {\n        // retry with backoff\n    }\n    // else: auth/404 — do not retry, fix credentials or key\n}","preventionTips":["Validate Jira credentials and base URL in a startup smoke check","Handle 429 with backoff honoring Retry-After","Confirm issue keys before lookup; escape keys properly"],"tags":["jira","http","network","api-client"],"backgroundTag":"jira-fetch-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}