From ca9426b1aa6be7706de6703f9b84d64e56d5b623 Mon Sep 17 00:00:00 2001 From: Dan Moseley Date: Sat, 25 Apr 2026 15:16:54 -0600 Subject: [PATCH] Include RetryAfter in AbuseRateLimitError.Error() output When RetryAfter is populated, append '[retry after ]' to the error string. This makes the retry information visible to callers that surface .Error() (e.g. logging, AI agent frameworks) without needing to use errors.As to inspect the struct directly. This is consistent with RateLimitError.Error() which already includes timing via formatRateReset. Fixes google/go-github#4180. --- github/github.go | 8 +++++-- github/github_test.go | 55 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/github/github.go b/github/github.go index 42b6382c6da..91af5aa665f 100644 --- a/github/github.go +++ b/github/github.go @@ -1363,9 +1363,13 @@ type AbuseRateLimitError struct { } func (r *AbuseRateLimitError) Error() string { - return fmt.Sprintf("%v %v: %v %v", + retryInfo := "" + if r.RetryAfter != nil && *r.RetryAfter > 0 { + retryInfo = fmt.Sprintf(" [retry after %v]", r.RetryAfter.Round(time.Second)) + } + return fmt.Sprintf("%v %v: %v %v%v", r.Response.Request.Method, sanitizeURL(r.Response.Request.URL), - r.Response.StatusCode, r.Message) + r.Response.StatusCode, r.Message, retryInfo) } // Is returns whether the provided error equals this error. diff --git a/github/github_test.go b/github/github_test.go index de67ed75661..cecc72b6ed4 100644 --- a/github/github_test.go +++ b/github/github_test.go @@ -3277,16 +3277,51 @@ func TestAbuseRateLimitError(t *testing.T) { t.Fatal(err) } - r := &AbuseRateLimitError{ - Response: &http.Response{ - Request: &http.Request{Method: "PUT", URL: u}, - StatusCode: http.StatusTooManyRequests, - }, - Message: "", - } - if got, want := r.Error(), "PUT https://example.com: 429 "; got != want { - t.Errorf("AbuseRateLimitError = %q, want %q", got, want) - } + t.Run("nil RetryAfter", func(t *testing.T) { + t.Parallel() + r := &AbuseRateLimitError{ + Response: &http.Response{ + Request: &http.Request{Method: "PUT", URL: u}, + StatusCode: http.StatusTooManyRequests, + }, + Message: "", + } + if got, want := r.Error(), "PUT https://example.com: 429 "; got != want { + t.Errorf("AbuseRateLimitError = %q, want %q", got, want) + } + }) + + t.Run("with RetryAfter", func(t *testing.T) { + t.Parallel() + d := 60 * time.Second + r := &AbuseRateLimitError{ + Response: &http.Response{ + Request: &http.Request{Method: "GET", URL: u}, + StatusCode: http.StatusForbidden, + }, + Message: "rate limited", + RetryAfter: &d, + } + if got, want := r.Error(), "GET https://example.com: 403 rate limited [retry after 1m0s]"; got != want { + t.Errorf("AbuseRateLimitError = %q, want %q", got, want) + } + }) + + t.Run("zero RetryAfter", func(t *testing.T) { + t.Parallel() + d := 0 * time.Second + r := &AbuseRateLimitError{ + Response: &http.Response{ + Request: &http.Request{Method: "POST", URL: u}, + StatusCode: http.StatusForbidden, + }, + Message: "rate limited", + RetryAfter: &d, + } + if got, want := r.Error(), "POST https://example.com: 403 rate limited"; got != want { + t.Errorf("AbuseRateLimitError = %q, want %q", got, want) + } + }) } func TestBareDo_returnsOpenBody(t *testing.T) {