mirror of
https://github.com/zoriya/octokit.net.git
synced 2025-12-06 07:16:09 +00:00
handle case insensitive headers when parsing for API rate limiting (#2175)
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Reactive.Linq;
|
||||
@@ -19,10 +20,15 @@ public class EnterpriseProbeTests
|
||||
[InlineData(HttpStatusCode.NotFound)]
|
||||
public async Task ReturnsExistsForResponseWithCorrectHeadersRegardlessOfResponse(HttpStatusCode httpStatusCode)
|
||||
{
|
||||
var headers = new Dictionary<string, string>()
|
||||
{
|
||||
{ "Server", "REVERSE-PROXY" },
|
||||
{ "X-GitHub-Request-Id", Guid.NewGuid().ToString() }
|
||||
};
|
||||
var response = Substitute.For<IResponse>();
|
||||
response.StatusCode.Returns(httpStatusCode);
|
||||
response.Headers["Server"].Returns("REVERSE-PROXY");
|
||||
response.Headers.ContainsKey("X-GitHub-Request-Id").Returns(true);
|
||||
response.Headers.Returns(headers);
|
||||
|
||||
var productHeader = new ProductHeaderValue("GHfW", "99");
|
||||
var httpClient = Substitute.For<IHttpClient>();
|
||||
httpClient.Send(Args.Request, CancellationToken.None).Returns(Task.FromResult(response));
|
||||
@@ -43,10 +49,16 @@ public class EnterpriseProbeTests
|
||||
[Fact]
|
||||
public async Task ReturnsExistsForApiExceptionWithCorrectHeaders()
|
||||
{
|
||||
|
||||
var headers = new Dictionary<string, string>()
|
||||
{
|
||||
{ "Server", "GitHub.com" },
|
||||
{ "X-GitHub-Request-Id", Guid.NewGuid().ToString() }
|
||||
};
|
||||
var httpClient = Substitute.For<IHttpClient>();
|
||||
var response = Substitute.For<IResponse>();
|
||||
response.Headers["Server"].Returns("GitHub.com");
|
||||
response.Headers.ContainsKey("X-GitHub-Request-Id").Returns(true);
|
||||
response.Headers.Returns(headers);
|
||||
|
||||
var apiException = new ApiException(response);
|
||||
httpClient.Send(Args.Request, CancellationToken.None).ThrowsAsync(apiException);
|
||||
var productHeader = new ProductHeaderValue("GHfW", "99");
|
||||
|
||||
@@ -53,6 +53,17 @@ namespace Octokit.Tests.Exceptions
|
||||
Assert.Equal(30, abuseException.RetryAfterSeconds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithRetryAfterCaseInsensitiveHeader_PopulatesRetryAfterSeconds()
|
||||
{
|
||||
var headerDictionary = new Dictionary<string, string> { { "retry-after", "20" } };
|
||||
|
||||
var response = new Response(HttpStatusCode.Forbidden, null, headerDictionary, "application/json");
|
||||
var abuseException = new AbuseException(response);
|
||||
|
||||
Assert.Equal(20, abuseException.RetryAfterSeconds);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void WithZeroHeaderValue_RetryAfterSecondsIsZero()
|
||||
{
|
||||
|
||||
@@ -32,6 +32,28 @@ namespace Octokit.Tests
|
||||
Assert.Equal("5634b0b187fd2e91e3126a75006cc4fa", apiInfo.Etag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ParsesApiInfoFromCaseInsensitiveHeaders()
|
||||
{
|
||||
var headers = new Dictionary<string, string>
|
||||
{
|
||||
{ "x-accepted-oauth-scopes", "user" },
|
||||
{ "x-oauth-scopes", "user, public_repo, repo, gist" },
|
||||
{ "x-ratelimit-limit", "5000" },
|
||||
{ "x-ratelimit-remaining", "4997" },
|
||||
{ "etag", "5634b0b187fd2e91e3126a75006cc4fa" }
|
||||
};
|
||||
|
||||
var apiInfo = ApiInfoParser.ParseResponseHeaders(headers);
|
||||
|
||||
Assert.NotNull(apiInfo);
|
||||
Assert.Equal(new[] { "user" }, apiInfo.AcceptedOauthScopes.ToArray());
|
||||
Assert.Equal(new[] { "user", "public_repo", "repo", "gist" }, apiInfo.OauthScopes.ToArray());
|
||||
Assert.Equal(5000, apiInfo.RateLimit.Limit);
|
||||
Assert.Equal(4997, apiInfo.RateLimit.Remaining);
|
||||
Assert.Equal("5634b0b187fd2e91e3126a75006cc4fa", apiInfo.Etag);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BadHeadersAreIgnored()
|
||||
{
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
@@ -103,7 +104,7 @@ namespace Octokit
|
||||
|
||||
static bool IsEnterpriseResponse(IResponse response)
|
||||
{
|
||||
return response.Headers.ContainsKey("X-GitHub-Request-Id");
|
||||
return response.Headers.Any(h => string.Equals(h.Key, "X-GitHub-Request-Id", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
#if !NO_SERIALIZABLE
|
||||
using System.Runtime.Serialization;
|
||||
@@ -44,11 +46,11 @@ namespace Octokit
|
||||
|
||||
private static int? ParseRetryAfterSeconds(IResponse response)
|
||||
{
|
||||
string secondsValue;
|
||||
if (!response.Headers.TryGetValue("Retry-After", out secondsValue)) { return null; }
|
||||
var header = response.Headers.FirstOrDefault(h => string.Equals(h.Key, "Retry-After", StringComparison.OrdinalIgnoreCase));
|
||||
if (header.Equals(default(KeyValuePair<string, string>))) { return null; }
|
||||
|
||||
int retrySeconds;
|
||||
if (!int.TryParse(secondsValue, out retrySeconds)) { return null; }
|
||||
if (!int.TryParse(header.Value, out retrySeconds)) { return null; }
|
||||
if (retrySeconds < 0) { return null; }
|
||||
|
||||
return retrySeconds;
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace Octokit
|
||||
/// </summary>
|
||||
/// <typeparam name="TKey"></typeparam>
|
||||
/// <typeparam name="TValue"></typeparam>
|
||||
[Obsolete("This component is no longer used, and will be removed in a future update")]
|
||||
public class ConcurrentCache<TKey, TValue>
|
||||
{
|
||||
Dictionary<TKey, TValue> _cache = new Dictionary<TKey, TValue>();
|
||||
|
||||
@@ -16,6 +16,16 @@ namespace Octokit.Internal
|
||||
static readonly Regex _linkRelRegex = new Regex("rel=\"(next|prev|first|last)\"", regexOptions);
|
||||
static readonly Regex _linkUriRegex = new Regex("<(.+)>", regexOptions);
|
||||
|
||||
static KeyValuePair<string, string> LookupHeader(IDictionary<string, string> headers, string key)
|
||||
{
|
||||
return headers.FirstOrDefault(h => string.Equals(h.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
static bool Exists(KeyValuePair<string, string> kvp)
|
||||
{
|
||||
return !kvp.Equals(default(KeyValuePair<string, string>));
|
||||
}
|
||||
|
||||
public static ApiInfo ParseResponseHeaders(IDictionary<string, string> responseHeaders)
|
||||
{
|
||||
Ensure.ArgumentNotNull(responseHeaders, nameof(responseHeaders));
|
||||
@@ -25,28 +35,32 @@ namespace Octokit.Internal
|
||||
var acceptedOauthScopes = new List<string>();
|
||||
string etag = null;
|
||||
|
||||
if (responseHeaders.ContainsKey("X-Accepted-OAuth-Scopes"))
|
||||
var acceptedOauthScopesKey = LookupHeader(responseHeaders, "X-Accepted-OAuth-Scopes");
|
||||
if (Exists(acceptedOauthScopesKey))
|
||||
{
|
||||
acceptedOauthScopes.AddRange(responseHeaders["X-Accepted-OAuth-Scopes"]
|
||||
acceptedOauthScopes.AddRange(acceptedOauthScopesKey.Value
|
||||
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(x => x.Trim()));
|
||||
}
|
||||
|
||||
if (responseHeaders.ContainsKey("X-OAuth-Scopes"))
|
||||
var oauthScopesKey = LookupHeader(responseHeaders, "X-OAuth-Scopes");
|
||||
if (Exists(oauthScopesKey))
|
||||
{
|
||||
oauthScopes.AddRange(responseHeaders["X-OAuth-Scopes"]
|
||||
oauthScopes.AddRange(oauthScopesKey.Value
|
||||
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(x => x.Trim()));
|
||||
}
|
||||
|
||||
if (responseHeaders.ContainsKey("ETag"))
|
||||
var etagKey = LookupHeader(responseHeaders, "ETag");
|
||||
if (Exists(etagKey))
|
||||
{
|
||||
etag = responseHeaders["ETag"];
|
||||
etag = etagKey.Value;
|
||||
}
|
||||
|
||||
if (responseHeaders.ContainsKey("Link"))
|
||||
var linkKey = LookupHeader(responseHeaders, "Link");
|
||||
if (Exists(linkKey))
|
||||
{
|
||||
var links = responseHeaders["Link"].Split(',');
|
||||
var links = linkKey.Value.Split(',');
|
||||
foreach (var link in links)
|
||||
{
|
||||
var relMatch = _linkRelRegex.Match(link);
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
#if !NO_SERIALIZABLE
|
||||
using System.Runtime.Serialization;
|
||||
#endif
|
||||
@@ -65,11 +66,32 @@ namespace Octokit
|
||||
[Parameter(Key = "reset")]
|
||||
public long ResetAsUtcEpochSeconds { get; private set; }
|
||||
|
||||
static KeyValuePair<string, string> LookupHeader(IDictionary<string, string> headers, string key)
|
||||
{
|
||||
return headers.FirstOrDefault(h => string.Equals(h.Key, key, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
static bool Exists(KeyValuePair<string, string> kvp)
|
||||
{
|
||||
return !kvp.Equals(default(KeyValuePair<string, string>));
|
||||
}
|
||||
|
||||
static long GetHeaderValueAsInt32Safe(IDictionary<string, string> responseHeaders, string key)
|
||||
{
|
||||
string value;
|
||||
long result;
|
||||
return !responseHeaders.TryGetValue(key, out value) || value == null || !long.TryParse(value, out result)
|
||||
|
||||
var foundKey = LookupHeader(responseHeaders, key);
|
||||
if (!Exists(foundKey))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(foundKey.Value))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return !long.TryParse(foundKey.Value, out result)
|
||||
? 0
|
||||
: result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user