{"record":{"id":"06d625ef67c531be","repo":"gastownhall/beads","slug":"jira-api-returned-d-s","errorCode":null,"errorMessage":"jira API returned %d: %s","messagePattern":"jira API returned (.+?): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/jira/client.go","lineNumber":397,"sourceCode":"\t\t_ = resp.Body.Close()\n\t\tif err != nil {\n\t\t\tlastErr = fmt.Errorf(\"failed to read response (attempt %d/%d): %w\", attempt+1, MaxRetries+1, err)\n\t\t\tcontinue\n\t\t}\n\n\t\t// PUT returns 204 No Content on success\n\t\tif resp.StatusCode == http.StatusNoContent {\n\t\t\treturn nil, nil\n\t\t}\n\n\t\tif resp.StatusCode >= 200 && resp.StatusCode < 300 {\n\t\t\treturn respBody, nil\n\t\t}\n\n\t\t// Permanent failures — no retry.\n\t\tswitch resp.StatusCode {\n\t\tcase http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound:\n\t\t\treturn nil, fmt.Errorf(\"jira API returned %d: %s\", resp.StatusCode, string(respBody))\n\t\t}\n\n\t\t// Retry on rate-limiting and server errors with exponential backoff.\n\t\tretriable := resp.StatusCode == http.StatusTooManyRequests ||\n\t\t\tresp.StatusCode == http.StatusInternalServerError ||\n\t\t\tresp.StatusCode == http.StatusBadGateway ||\n\t\t\tresp.StatusCode == http.StatusServiceUnavailable ||\n\t\t\tresp.StatusCode == http.StatusGatewayTimeout\n\n\t\tif retriable {\n\t\t\tdelay := RetryDelay * time.Duration(1<<uint(attempt))\n\t\t\tuseServerDelay := false\n\n\t\t\t// Use Retry-After header if present (no jitter — respect server-mandated delay)\n\t\t\tif retryAfter := resp.Header.Get(\"Retry-After\"); retryAfter != \"\" {\n\t\t\t\tif seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil {\n\t\t\t\t\tdelay = time.Duration(seconds) * time.Second\n\t\t\t\t\tuseServerDelay = true","sourceCodeStart":379,"sourceCodeEnd":415,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/jira/client.go#L379-L415","documentation":"doRequest returns this immediately (no retry) when Jira responds with a status the client classifies as a permanent client failure: 400 Bad Request, 401 Unauthorized, 403 Forbidden, or 404 Not Found. The message embeds the raw response body so the server's own error explanation is visible. Retrying cannot fix these — the request itself or the credentials/URL must change.","triggerScenarios":"Any doRequest call (issue fetch/create/update, JQL search) where resp.StatusCode is one of 400, 401, 403, 404. The full response body is included in the message.","commonSituations":"Expired or wrong API token (401); token valid but account lacks permission on the project (403); typo in issue key or project key (404); malformed JQL or JSON payload (400); deleted issue (404); using a Cloud token against Server or vice versa.","solutions":["Read the embedded response body in the message — Jira's error JSON usually names the exact problem.","For 401: regenerate the API token and verify jira.api_token/JIRA_API_TOKEN matches the username/email exactly.","For 403: check the account's project permissions in Jira admin; verify the account is not deactivated.","For 404: verify the issue key/project key and that jira.url points at the correct site (e.g. https://yourorg.atlassian.net).","For 400: fix the request payload or JQL; test the same query in the Jira web UI or REST browser."],"exampleFix":"// before: wrong field name in payload → 400\npayload := map[string]any{\"fields\": map[string]any{\"summary\": s, \"prority\": map[string]any{\"name\": p}}}\n// after: correct field name\npayload := map[string]any{\"fields\": map[string]any{\"summary\": s, \"priority\": map[string]any{\"name\": p}}}","handlingStrategy":"try-catch","validationCode":"// Pre-flight auth + project check before real operations\nresp, _ := http.Get(jiraURL + \"/rest/api/3/myself\")\nif resp != nil && (resp.StatusCode == 401 || resp.StatusCode == 403) {\n    return fmt.Errorf(\"credentials/permissions invalid before starting (HTTP %d)\", resp.StatusCode)\n}","typeGuard":null,"tryCatchPattern":"if err != nil {\n    if strings.Contains(err.Error(), \"jira API returned\") {\n        var code int\n        fmt.Sscanf(err.Error(), \"jira API returned %d\", &code)\n        switch code {\n        case 401: return refreshCredentials()\n        case 403: return fmt.Errorf(\"permission denied on project\")\n        case 404: return fmt.Errorf(\"issue/project key does not exist\")\n        default: return fmt.Errorf(\"bad request, inspect Jira body: %w\", err)\n        }\n    }\n    return err\n}","preventionTips":["Validate the API token and username with a /myself call at startup.","Verify project keys against the Jira admin before first use.","Never hardcode tokens; rotate before expiry and reload from the secret store.","Test JQL/queries in the Jira UI before encoding them in code.","Confirm jira.url matches the correct Cloud site or Server base URL."],"tags":["jira","http","api","authentication","client-error"],"backgroundTag":"http-4xx-client-error","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}