Skip to content

Improve rate limit error messages for AI agents #2385

@danmoseley

Description

@danmoseley

Summary

When the GitHub API returns a rate limit error, the message surfaced to the AI model as a tool result is either missing the retry duration entirely, or buries it inside a noisy raw HTTP string. This makes it hard for agents to know how long to wait before retrying.

All tool errors flow through NewGitHubAPIErrorResponse in pkg/errors/error.go, which formats the tool result as:

<tool message>: <err.Error()>

For a primary rate limit (RateLimitError), the agent sees:

search code: GET https://api.github.com/search/code: 403 API rate limit exceeded for user ID 12345. [rate reset in 47s]

The reset time is present but buried inside a raw HTTP method + URL + status string.

For a secondary (abuse) rate limit (AbuseRateLimitError), the agent sees:

search code: GET https://api.github.com/search/code: 403 You have exceeded a secondary rate limit and have been temporarily blocked from content creation. Please retry your request again later.

The Retry-After duration is completely absent — it is populated in the struct from the HTTP header but dropped by AbuseRateLimitError.Error() (tracked upstream in google/go-github#4180).

Suggested fix

In NewGitHubAPIErrorResponse, use errors.As to detect rate limit error types and return a clean, agent-friendly message:

func NewGitHubAPIErrorResponse(ctx context.Context, message string, resp *github.Response, err error) *mcp.CallToolResult {
    apiErr := newGitHubAPIError(message, resp, err)
    if ctx != nil {
        _, _ = addGitHubAPIErrorToContext(ctx, apiErr)
    }

    var rateLimitErr *github.RateLimitError
    if errors.As(err, &rateLimitErr) {
        retryIn := time.Until(rateLimitErr.Rate.Reset.Time).Round(time.Second)
        return utils.NewToolResultError(fmt.Sprintf(
            "%s: GitHub API rate limit exceeded. Retry after %v.",
            message, retryIn))
    }

    var abuseErr *github.AbuseRateLimitError
    if errors.As(err, &abuseErr) {
        if abuseErr.RetryAfter != nil {
            return utils.NewToolResultError(fmt.Sprintf(
                "%s: GitHub secondary rate limit exceeded. Retry after %v.",
                message, abuseErr.RetryAfter.Round(time.Second)))
        }
        return utils.NewToolResultError(fmt.Sprintf(
            "%s: GitHub secondary rate limit exceeded. Wait before retrying.",
            message))
    }

    return utils.NewToolResultErrorFromErr(message, err)
}

This gives the agent clear, actionable output:

search code: GitHub API rate limit exceeded. Retry after 47s.
search code: GitHub secondary rate limit exceeded. Retry after 60s.

Note: This fix is independent of google/go-github#4180 — it reads RetryAfter directly from the struct rather than relying on .Error(). The go-github fix benefits all other callers of the library.

Note

This issue was drafted with the assistance of GitHub Copilot.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions