Skip to content

Commit 92ff70b

Browse files
daverantnickfloyd
andauthored
BREAKING CHANGE (behavior): Modify caching to only attempt to update the response cache if a 2xx response code is received from GitHub (#2877)
Only update response cache for successful api responses Co-authored-by: Nick Floyd <[email protected]>
1 parent 1dc9c5e commit 92ff70b

File tree

3 files changed

+121
-21
lines changed

3 files changed

+121
-21
lines changed

Octokit.Tests/Caching/CachingHttpClientTests.cs

Lines changed: 105 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Collections.Immutable;
34
using System.Linq;
45
using System.Net;
56
using System.Net.Http;
@@ -105,8 +106,8 @@ public async Task UsesCachedResponseIfEtagIsPresentAndGithubReturns304()
105106
}
106107

107108
[Theory]
108-
[MemberData(nameof(NonNotModifiedHttpStatusCodesWithSetCacheFailure))]
109-
public async Task UsesGithubResponseIfEtagIsPresentAndGithubReturnsNon304(HttpStatusCode httpStatusCode, bool setCacheThrows)
109+
[MemberData(nameof(SuccessHttpStatusCodesWithSetCacheFailure))]
110+
public async Task UsesGithubResponseIfEtagIsPresentAndGithubReturnsSuccessCode(HttpStatusCode httpStatusCode, bool setCacheThrows)
110111
{
111112
// arrange
112113
var underlyingClient = Substitute.For<IHttpClient>();
@@ -146,20 +147,50 @@ public async Task UsesGithubResponseIfEtagIsPresentAndGithubReturnsNon304(HttpSt
146147
responseCache.Received(1).SetAsync(request, Arg.Is<CachedResponse.V1>(v1 => new ResponseComparer().Equals(v1, CachedResponse.V1.Create(githubResponse))));
147148
}
148149

149-
public static IEnumerable<object[]> NonNotModifiedHttpStatusCodesWithSetCacheFailure()
150+
public static IEnumerable<object[]> SuccessHttpStatusCodesWithSetCacheFailure()
150151
{
151-
foreach (var statusCode in Enum.GetValues(typeof(HttpStatusCode)))
152+
var setCacheFails = new[] { true, false };
153+
154+
foreach (var cacheFail in setCacheFails)
155+
{
156+
foreach (var statusCode in _successStatusCodes.Cast<object>())
157+
{
158+
yield return new[] { statusCode, cacheFail };
159+
yield return new[] { statusCode, cacheFail };
160+
}
161+
}
162+
}
163+
164+
private static readonly IImmutableList<HttpStatusCode> _successStatusCodes = Enumerable
165+
.Range(200, 100)
166+
.Where(code => Enum.IsDefined(typeof(HttpStatusCode), code))
167+
.Cast<HttpStatusCode>()
168+
.ToImmutableList();
169+
170+
private static readonly IImmutableList<string> _invalidETags = new[]
171+
{
172+
null, string.Empty
173+
}.ToImmutableList();
174+
175+
public static IEnumerable<object[]> SuccessHttpStatusCodesWithSetCacheFailureAndInvalidETags()
176+
{
177+
var setCacheFails = new[] { true, false };
178+
foreach (var etag in _invalidETags)
152179
{
153-
if (statusCode.Equals(HttpStatusCode.NotModified)) continue;
154-
yield return new[] { statusCode, true };
155-
yield return new[] { statusCode, false };
180+
foreach (var cacheFail in setCacheFails)
181+
{
182+
foreach (var statusCode in _successStatusCodes.Cast<object>())
183+
{
184+
yield return new[] { statusCode, cacheFail, etag };
185+
yield return new[] { statusCode, cacheFail, etag };
186+
}
187+
}
156188
}
157189
}
158190

159191
[Theory]
160-
[InlineData(true)]
161-
[InlineData(false)]
162-
public async Task UsesGithubResponseIfCachedEntryIsNull(bool setCacheThrows)
192+
[MemberData(nameof(SuccessHttpStatusCodesWithSetCacheFailure))]
193+
public async Task UsesGithubResponseIfCachedEntryIsNull(HttpStatusCode httpStatusCode, bool setCacheThrows)
163194
{
164195
// arrange
165196
var underlyingClient = Substitute.For<IHttpClient>();
@@ -171,6 +202,7 @@ public async Task UsesGithubResponseIfCachedEntryIsNull(bool setCacheThrows)
171202
var cancellationToken = CancellationToken.None;
172203

173204
var githubResponse = Substitute.For<IResponse>();
205+
githubResponse.StatusCode.Returns(httpStatusCode);
174206

175207
underlyingClient.Send(Arg.Is<IRequest>(req => req == request && !req.Headers.Any()), cancellationToken).ReturnsForAnyArgs(githubResponse);
176208
responseCache.GetAsync(request).ReturnsNull();
@@ -194,9 +226,8 @@ public async Task UsesGithubResponseIfCachedEntryIsNull(bool setCacheThrows)
194226
}
195227

196228
[Theory]
197-
[InlineData(true)]
198-
[InlineData(false)]
199-
public async Task UsesGithubResponseIfGetCachedEntryThrows(bool setCacheThrows)
229+
[MemberData(nameof(SuccessHttpStatusCodesWithSetCacheFailure))]
230+
public async Task UsesGithubResponseIfGetCachedEntryThrows(HttpStatusCode httpStatusCode, bool setCacheThrows)
200231
{
201232
// arrange
202233
var underlyingClient = Substitute.For<IHttpClient>();
@@ -208,6 +239,7 @@ public async Task UsesGithubResponseIfGetCachedEntryThrows(bool setCacheThrows)
208239
var cancellationToken = CancellationToken.None;
209240

210241
var githubResponse = Substitute.For<IResponse>();
242+
githubResponse.StatusCode.Returns(httpStatusCode);
211243

212244
underlyingClient.Send(Arg.Is<IRequest>(req => req == request && !req.Headers.Any()), cancellationToken).ReturnsForAnyArgs(githubResponse);
213245
responseCache.GetAsync(Args.Request).ThrowsForAnyArgs<Exception>();
@@ -231,11 +263,8 @@ public async Task UsesGithubResponseIfGetCachedEntryThrows(bool setCacheThrows)
231263
}
232264

233265
[Theory]
234-
[InlineData(null, true)]
235-
[InlineData(null, false)]
236-
[InlineData("", true)]
237-
[InlineData("", false)]
238-
public async Task UsesGithubResponseIfCachedEntryEtagIsNullOrEmpty(string etag, bool setCacheThrows)
266+
[MemberData(nameof(SuccessHttpStatusCodesWithSetCacheFailureAndInvalidETags))]
267+
public async Task UsesGithubResponseIfCachedEntryEtagIsNullOrEmpty(HttpStatusCode httpStatusCode, bool setCacheThrows, string etag)
239268
{
240269
// arrange
241270
var underlyingClient = Substitute.For<IHttpClient>();
@@ -251,6 +280,7 @@ public async Task UsesGithubResponseIfCachedEntryEtagIsNullOrEmpty(string etag,
251280
var cancellationToken = CancellationToken.None;
252281

253282
var githubResponse = Substitute.For<IResponse>();
283+
githubResponse.StatusCode.Returns(httpStatusCode);
254284

255285
underlyingClient.Send(Arg.Is<IRequest>(req => req == request && !req.Headers.Any()), cancellationToken).ReturnsForAnyArgs(githubResponse);
256286
responseCache.GetAsync(request).Returns(cachedV1Response);
@@ -272,6 +302,63 @@ public async Task UsesGithubResponseIfCachedEntryEtagIsNullOrEmpty(string etag,
272302
responseCache.Received(1).GetAsync(request);
273303
responseCache.Received(1).SetAsync(request, Arg.Is<CachedResponse.V1>(v1 => new ResponseComparer().Equals(v1, CachedResponse.V1.Create(githubResponse))));
274304
}
305+
306+
public static IEnumerable<object[]> DoesNotUpdateCacheData()
307+
{
308+
var codesToExclude = _successStatusCodes
309+
.Add(HttpStatusCode.NotModified);
310+
var failureCodes = Enum
311+
.GetValues(typeof(HttpStatusCode))
312+
.Cast<HttpStatusCode>()
313+
.Except(codesToExclude)
314+
.ToList();
315+
var hasCachedResponses = new[] { false, true };
316+
317+
foreach (var etag in _invalidETags)
318+
{
319+
foreach (var hasCachedResponse in hasCachedResponses)
320+
{
321+
foreach (var statusCode in failureCodes)
322+
{
323+
yield return new object[]
324+
{
325+
statusCode, hasCachedResponse, etag
326+
};
327+
}
328+
}
329+
}
330+
}
331+
332+
[Theory]
333+
[MemberData(nameof(DoesNotUpdateCacheData))]
334+
public async Task DoesNotUpdateCacheIfGitHubResponseIsNotSuccessCode(HttpStatusCode httpStatusCode, bool hasCachedResponse, string etag)
335+
{
336+
// arrange
337+
var underlyingClient = Substitute.For<IHttpClient>();
338+
var responseCache = Substitute.For<IResponseCache>();
339+
var request = Substitute.For<IRequest>();
340+
request.Method.Returns(HttpMethod.Get);
341+
request.Headers.Returns(new Dictionary<string, string>());
342+
343+
var cachedResponse = Substitute.For<IResponse>();
344+
cachedResponse.Headers.Returns(etag == null ? new Dictionary<string, string>() : new Dictionary<string, string> { { "ETag", etag } });
345+
346+
var cachedV1Response = CachedResponse.V1.Create(cachedResponse);
347+
348+
var githubResponse = Substitute.For<IResponse>();
349+
githubResponse.StatusCode.Returns(httpStatusCode);
350+
351+
underlyingClient.Send(request).Returns(githubResponse);
352+
responseCache.GetAsync(request).Returns(hasCachedResponse ? cachedV1Response : null);
353+
354+
var cachingHttpClient = new CachingHttpClient(underlyingClient, responseCache);
355+
356+
// act
357+
_ = await cachingHttpClient.Send(request, CancellationToken.None);
358+
359+
// assert
360+
responseCache.DidNotReceiveWithAnyArgs().SetAsync(Arg.Any<IRequest>(), Arg.Any<CachedResponse.V1>());
361+
}
275362
}
276363

277364
public class TheSetRequestTimeoutMethod

Octokit/Caching/CachingHttpClient.cs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,12 @@ public async Task<IResponse> Send(IRequest request, CancellationToken cancellati
4040
return cachedResponse;
4141
}
4242

43-
TrySetCachedResponse(request, conditionalResponse);
43+
_ = TrySetCachedResponse(request, conditionalResponse);
4444
return conditionalResponse;
4545
}
4646

4747
var response = await _httpClient.Send(request, cancellationToken);
48-
TrySetCachedResponse(request, response);
48+
_ = TrySetCachedResponse(request, response);
4949
return response;
5050
}
5151

@@ -65,6 +65,10 @@ private async Task TrySetCachedResponse(IRequest request, IResponse response)
6565
{
6666
try
6767
{
68+
if(!response.IsSuccessStatusCode())
69+
{
70+
return;
71+
}
6872
await _responseCache.SetAsync(request, CachedResponse.V1.Create(response));
6973
}
7074
catch (Exception)

Octokit/Helpers/HttpClientExtensions.cs renamed to Octokit/Helpers/HttpExtensions.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace Octokit
66
{
7-
public static class HttpClientExtensions
7+
public static class HttpExtensions
88
{
99
public static Task<IResponse> Send(this IHttpClient httpClient, IRequest request)
1010
{
@@ -13,5 +13,14 @@ public static Task<IResponse> Send(this IHttpClient httpClient, IRequest request
1313

1414
return httpClient.Send(request, CancellationToken.None);
1515
}
16+
17+
/// <summary>
18+
/// Gets a value that indicates whether the HTTP response was successful.
19+
/// </summary>
20+
public static bool IsSuccessStatusCode(this IResponse response)
21+
{
22+
Ensure.ArgumentNotNull(response, nameof(response));
23+
return (int) response.StatusCode >= 200 && (int) response.StatusCode <= 299;
24+
}
1625
}
1726
}

0 commit comments

Comments
 (0)