{"record":{"id":"d81dbdf94248f304","repo":"gastownhall/beads","slug":"max-retries-d-exceeded-w-d81dbd","errorCode":null,"errorMessage":"max retries (%d) exceeded: %w","messagePattern":"max retries \\((.+?)\\) exceeded: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/gitlab/client.go","lineNumber":188,"sourceCode":"\t\t\tif !useServerDelay {\n\t\t\t\tif half := int64(delay / 2); half > 0 {\n\t\t\t\t\tdelay += time.Duration(rand.Int64N(half)) //nolint:gosec // G404: jitter for retry backoff does not need crypto rand\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tlastErr = fmt.Errorf(\"transient error %d (attempt %d/%d)\", resp.StatusCode, attempt+1, MaxRetries+1)\n\t\t\tselect {\n\t\t\tcase <-ctx.Done():\n\t\t\t\treturn nil, nil, ctx.Err()\n\t\t\tcase <-time.After(delay):\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\treturn nil, nil, fmt.Errorf(\"API error: %s (status %d)\", string(respBody), resp.StatusCode)\n\t}\n\n\treturn nil, nil, fmt.Errorf(\"max retries (%d) exceeded: %w\", MaxRetries+1, lastErr)\n}\n\n// applyFilter adds IssueFilter fields as query parameters to the params map.\n// ProjectID filtering is done client-side (not supported by GitLab API on group endpoints).\nfunc applyFilter(params map[string]string, filter *IssueFilter) {\n\tif filter == nil {\n\t\treturn\n\t}\n\tif filter.Labels != \"\" {\n\t\tparams[\"labels\"] = filter.Labels\n\t}\n\tif filter.Milestone != \"\" {\n\t\tparams[\"milestone\"] = filter.Milestone\n\t}\n\tif filter.Assignee != \"\" {\n\t\tparams[\"assignee_username\"] = filter.Assignee\n\t}\n}","sourceCodeStart":170,"sourceCodeEnd":206,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/gitlab/client.go#L170-L206","documentation":"After MaxRetries+1 attempts, every attempt failed with a transient error (network failure, body read failure, or retryable status). doRequest returns this final error wrapping the last underlying error (lastErr). The caller sees both the retry budget exhausted and the root cause in the chain.","triggerScenarios":"Sustained network outage, persistent 429/5xx across the full retry window, or repeated mid-body read failures; any client API call (list/create/update issues) during that window returns this after the retry loop.","commonSituations":"GitLab maintenance window longer than the retry backoff; VPN/proxy down for the whole retry duration; hard rate-limit (429) persisting because the app keeps hammering after the error returns.","solutions":["Inspect errors.Unwrap / %v of the error to see the last root cause (connection refused, 429, read failure)","Wait for the outage/rate-limit window to pass and resume with backoff; don't tight-loop retries","Increase MaxRetries or add caller-level exponential backoff for known flaky networks","Monitor GitLab health/status page for your instance before debugging app code"],"exampleFix":"// before\nissues, _, err := client.ListIssues(ctx, f)\nif err != nil { return err } // opaque 'max retries exceeded'\n// after\nif err != nil { return fmt.Errorf(\"list issues: %w\", err) } // preserve chain; log with %+v to see root cause","handlingStrategy":"retry","validationCode":"if err := pingGitLab(base, token); err != nil {\n\treturn fmt.Errorf(\"GitLab unreachable before sync: %w\", err)\n}","typeGuard":"func isRetryExhausted(err error) bool {\n\treturn strings.Contains(err.Error(), \"max retries\")\n}","tryCatchPattern":"if err != nil {\n\tif isRetryExhausted(err) {\n\t\tlog.Printf(\"GitLab down or rate-limited; root cause: %v\", err)\n\t\ttime.Sleep(5 * time.Minute) // resume later\n\t}\n\treturn err\n}","preventionTips":["Log with %+v to keep the wrapped root cause visible","Add circuit-breaker logic instead of immediate re-looping after exhaustion","Size MaxRetries/backoff to your instance's realistic outage windows","Alert on GitLab status so on-call knows the failure is upstream"],"tags":["retry","network","timeout","gitlab"],"backgroundTag":"max-retries-exceeded","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}