gastownhall/beads · error

jira API returned %d: %s

Error message

jira API returned %d: %s

What it means

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.

Source

Thrown at internal/jira/client.go:397

		_ = resp.Body.Close()
		if err != nil {
			lastErr = fmt.Errorf("failed to read response (attempt %d/%d): %w", attempt+1, MaxRetries+1, err)
			continue
		}

		// PUT returns 204 No Content on success
		if resp.StatusCode == http.StatusNoContent {
			return nil, nil
		}

		if resp.StatusCode >= 200 && resp.StatusCode < 300 {
			return respBody, nil
		}

		// Permanent failures — no retry.
		switch resp.StatusCode {
		case http.StatusBadRequest, http.StatusUnauthorized, http.StatusForbidden, http.StatusNotFound:
			return nil, fmt.Errorf("jira API returned %d: %s", resp.StatusCode, string(respBody))
		}

		// Retry on rate-limiting and server errors with exponential backoff.
		retriable := resp.StatusCode == http.StatusTooManyRequests ||
			resp.StatusCode == http.StatusInternalServerError ||
			resp.StatusCode == http.StatusBadGateway ||
			resp.StatusCode == http.StatusServiceUnavailable ||
			resp.StatusCode == http.StatusGatewayTimeout

		if retriable {
			delay := RetryDelay * time.Duration(1<<uint(attempt))
			useServerDelay := false

			// Use Retry-After header if present (no jitter — respect server-mandated delay)
			if retryAfter := resp.Header.Get("Retry-After"); retryAfter != "" {
				if seconds, parseErr := strconv.Atoi(retryAfter); parseErr == nil {
					delay = time.Duration(seconds) * time.Second
					useServerDelay = true

View on GitHub (pinned to 71377f2769)

Solutions

  1. Read the embedded response body in the message — Jira's error JSON usually names the exact problem.
  2. For 401: regenerate the API token and verify jira.api_token/JIRA_API_TOKEN matches the username/email exactly.
  3. For 403: check the account's project permissions in Jira admin; verify the account is not deactivated.
  4. For 404: verify the issue key/project key and that jira.url points at the correct site (e.g. https://yourorg.atlassian.net).
  5. For 400: fix the request payload or JQL; test the same query in the Jira web UI or REST browser.

Example fix

// before: wrong field name in payload → 400
payload := map[string]any{"fields": map[string]any{"summary": s, "prority": map[string]any{"name": p}}}
// after: correct field name
payload := map[string]any{"fields": map[string]any{"summary": s, "priority": map[string]any{"name": p}}}
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight auth + project check before real operations
resp, _ := http.Get(jiraURL + "/rest/api/3/myself")
if resp != nil && (resp.StatusCode == 401 || resp.StatusCode == 403) {
    return fmt.Errorf("credentials/permissions invalid before starting (HTTP %d)", resp.StatusCode)
}

Try / catch

if err != nil {
    if strings.Contains(err.Error(), "jira API returned") {
        var code int
        fmt.Sscanf(err.Error(), "jira API returned %d", &code)
        switch code {
        case 401: return refreshCredentials()
        case 403: return fmt.Errorf("permission denied on project")
        case 404: return fmt.Errorf("issue/project key does not exist")
        default: return fmt.Errorf("bad request, inspect Jira body: %w", err)
        }
    }
    return err
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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