diff --git a/Octokit/Helpers/ResponseHeaders.cs b/Octokit/Helpers/ResponseHeaders.cs new file mode 100644 index 0000000000..139fe19e09 --- /dev/null +++ b/Octokit/Helpers/ResponseHeaders.cs @@ -0,0 +1,10 @@ +namespace Octokit +{ + public static class ResponseHeaders + { + public const string RateLimitLimit = "x-ratelimit-limit"; + public const string RateLimitRemaining = "x-ratelimit-remaining"; + public const string RateLimitUsed = "x-ratelimit-used"; + public const string RateLimitReset = "x-ratelimit-reset"; + } +} diff --git a/Octokit/Http/Connection.cs b/Octokit/Http/Connection.cs index c0fc81f4ee..4be811d1f9 100644 --- a/Octokit/Http/Connection.cs +++ b/Octokit/Http/Connection.cs @@ -783,9 +783,11 @@ static Exception GetExceptionForUnauthorized(IResponse response) static Exception GetExceptionForForbidden(IResponse response) { - string body = response.Body as string ?? ""; + var body = response.Body as string ?? ""; - if (body.Contains("rate limit exceeded")) + if (body.Contains("rate limit exceeded") + || (response.Headers.ContainsKey(ResponseHeaders.RateLimitRemaining) + && response.Headers[ResponseHeaders.RateLimitRemaining] == "0")) { return new RateLimitExceededException(response); } diff --git a/Octokit/Http/HttpClientAdapter.cs b/Octokit/Http/HttpClientAdapter.cs index 0e54074dff..33bf98f467 100644 --- a/Octokit/Http/HttpClientAdapter.cs +++ b/Octokit/Http/HttpClientAdapter.cs @@ -198,6 +198,28 @@ public async Task SendAsync(HttpRequestMessage request, Can var response = await _http.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken).ConfigureAwait(false); // Need to determine time on client computer as soon as possible. var receivedTime = DateTimeOffset.Now; + + if (response.StatusCode == HttpStatusCode.Forbidden + && response.Headers.Contains(ResponseHeaders.RateLimitRemaining) + && response.Headers.GetValues(ResponseHeaders.RateLimitRemaining).FirstOrDefault() == "0") + { + var nowInEpochSecondsUtc = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var resetTimeInEpochSecondsUtc = long.Parse(response.Headers.GetValues(ResponseHeaders.RateLimitReset).First()); + + var timeToWait = resetTimeInEpochSecondsUtc > nowInEpochSecondsUtc + ? TimeSpan.FromSeconds(resetTimeInEpochSecondsUtc - nowInEpochSecondsUtc) + : TimeSpan.Zero; + + // Could rate limit refreshes be up to an hour in the future? + // Should we conditionally retry only if it's a short period in the future + if (timeToWait < TimeSpan.FromSeconds(30)) + { + await Task.Delay(timeToWait, cancellationToken); + + return await SendAsync(clonedRequest, cancellationToken); + } + } + // Since Properties are stored as objects, serialize to HTTP round-tripping string (Format: r) // Resolution is limited to one-second, matching the resolution of the HTTP Date header request.Properties[ApiInfoParser.ReceivedTimeHeaderName] = @@ -251,7 +273,7 @@ public async Task SendAsync(HttpRequestMessage request, Can return response; } - + public static async Task CloneHttpRequestMessageAsync(HttpRequestMessage oldRequest) { var newRequest = new HttpRequestMessage(oldRequest.Method, oldRequest.RequestUri);