Skip to content

Commit 2057c2a

Browse files
slang25martincostello
authored andcommitted
Add CancellationToken to OnIntercepted (#144)
* Add CancellationToken to OnIntercepted * Some feedback * LF * More feedback work * .gitattributes text=auto * Add a couple of tests * More feedback * Add another test * Missing type arg
1 parent 14d903d commit 2057c2a

File tree

10 files changed

+243
-16
lines changed

10 files changed

+243
-16
lines changed

.gitattributes

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
* text=auto
12
*.sh eol=lf

src/HttpClientInterception/DelegateHelpers.cs

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Net.Http;
6+
using System.Threading;
67
using System.Threading.Tasks;
78

89
namespace JustEat.HttpClientInterception
@@ -25,14 +26,14 @@ internal static class DelegateHelpers
2526
/// <returns>
2627
/// The converted delegate if <paramref name="onIntercepted"/> has a value; otherwise <see langword="null"/>.
2728
/// </returns>
28-
internal static Func<HttpRequestMessage, Task<bool>>? ConvertToBooleanTask(Action<HttpRequestMessage>? onIntercepted)
29+
internal static Func<HttpRequestMessage, CancellationToken, Task<bool>>? ConvertToBooleanTask(Action<HttpRequestMessage>? onIntercepted)
2930
{
3031
if (onIntercepted == null)
3132
{
3233
return null;
3334
}
3435

35-
return (message) =>
36+
return (message, _) =>
3637
{
3738
onIntercepted(message);
3839
return TrueTask;
@@ -47,14 +48,14 @@ internal static class DelegateHelpers
4748
/// <returns>
4849
/// The converted delegate if <paramref name="onIntercepted"/> has a value; otherwise <see langword="null"/>.
4950
/// </returns>
50-
internal static Func<HttpRequestMessage, Task<bool>>? ConvertToBooleanTask(Predicate<HttpRequestMessage>? onIntercepted)
51+
internal static Func<HttpRequestMessage, CancellationToken, Task<bool>>? ConvertToBooleanTask(Predicate<HttpRequestMessage>? onIntercepted)
5152
{
5253
if (onIntercepted == null)
5354
{
5455
return null;
5556
}
5657

57-
return (message) => Task.FromResult(onIntercepted(message));
58+
return (message, _) => Task.FromResult(onIntercepted(message));
5859
}
5960

6061
/// <summary>
@@ -65,18 +66,54 @@ internal static class DelegateHelpers
6566
/// <returns>
6667
/// The converted delegate if <paramref name="onIntercepted"/> has a value; otherwise <see langword="null"/>.
6768
/// </returns>
68-
internal static Func<HttpRequestMessage, Task<bool>>? ConvertToBooleanTask(Func<HttpRequestMessage, Task>? onIntercepted)
69+
internal static Func<HttpRequestMessage, CancellationToken, Task<bool>>? ConvertToBooleanTask(Func<HttpRequestMessage, Task>? onIntercepted)
6970
{
7071
if (onIntercepted == null)
7172
{
7273
return null;
7374
}
7475

75-
return async (message) =>
76+
return ConvertToBooleanTask((message, _) => onIntercepted(message));
77+
}
78+
79+
/// <summary>
80+
/// Converts a function delegate for an intercepted message to return a
81+
/// <see cref="Task{TResult}"/> which returns <see langword="true"/>.
82+
/// </summary>
83+
/// <param name="onIntercepted">An optional delegate to convert.</param>
84+
/// <returns>
85+
/// The converted delegate if <paramref name="onIntercepted"/> has a value; otherwise <see langword="null"/>.
86+
/// </returns>
87+
internal static Func<HttpRequestMessage, CancellationToken, Task<bool>>? ConvertToBooleanTask(Func<HttpRequestMessage, CancellationToken, Task>? onIntercepted)
88+
{
89+
if (onIntercepted == null)
90+
{
91+
return null;
92+
}
93+
94+
return async (message, token) =>
7695
{
77-
await onIntercepted(message).ConfigureAwait(false);
96+
await onIntercepted(message, token).ConfigureAwait(false);
7897
return true;
7998
};
8099
}
100+
101+
/// <summary>
102+
/// Converts a function delegate for an intercepted message to return a
103+
/// <see cref="Task{TResult}"/> which returns <see langword="true"/>.
104+
/// </summary>
105+
/// <param name="onIntercepted">An optional delegate to convert.</param>
106+
/// <returns>
107+
/// The converted delegate if <paramref name="onIntercepted"/> has a value; otherwise <see langword="null"/>.
108+
/// </returns>
109+
internal static Func<HttpRequestMessage, CancellationToken, Task<bool>>? ConvertToBooleanTask(Func<HttpRequestMessage, Task<bool>>? onIntercepted)
110+
{
111+
if (onIntercepted == null)
112+
{
113+
return null;
114+
}
115+
116+
return async (message, _) => await onIntercepted(message).ConfigureAwait(false);
117+
}
81118
}
82119
}

src/HttpClientInterception/HttpClientInterceptorOptions.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -330,14 +330,15 @@ public HttpClientInterceptorOptions Register(HttpRequestInterceptionBuilder buil
330330
/// Gets the HTTP response, if any, set up for the specified HTTP request as an asynchronous operation.
331331
/// </summary>
332332
/// <param name="request">The HTTP request to try and get the intercepted response for.</param>
333+
/// <param name="cancellationToken">The optional token to monitor for cancellation requests.</param>
333334
/// <returns>
334335
/// A <see cref="Task{TResult}"/> that returns the HTTP response to use, if any,
335336
/// for <paramref name="request"/>; otherwise <see langword="null"/>.
336337
/// </returns>
337338
/// <exception cref="ArgumentNullException">
338339
/// <paramref name="request"/> is <see langword="null"/>.
339340
/// </exception>
340-
public virtual async Task<HttpResponseMessage?> GetResponseAsync(HttpRequestMessage request)
341+
public virtual async Task<HttpResponseMessage?> GetResponseAsync(HttpRequestMessage request, CancellationToken cancellationToken = default)
341342
{
342343
if (request == null)
343344
{
@@ -354,7 +355,7 @@ public HttpClientInterceptorOptions Register(HttpRequestInterceptionBuilder buil
354355
var response = matchResult.Item2;
355356

356357
// If Item1 is true, then Item2 is non-null
357-
if (response!.OnIntercepted != null && !await response.OnIntercepted(request).ConfigureAwait(false))
358+
if (response!.OnIntercepted != null && !await response.OnIntercepted(request, cancellationToken).ConfigureAwait(false))
358359
{
359360
return null;
360361
}

src/HttpClientInterception/HttpInterceptionResponse.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.IO;
77
using System.Net;
88
using System.Net.Http;
9+
using System.Threading;
910
using System.Threading.Tasks;
1011

1112
namespace JustEat.HttpClientInterception
@@ -48,7 +49,7 @@ internal sealed class HttpInterceptionResponse
4849

4950
internal IEnumerable<KeyValuePair<string, IEnumerable<string>>>? ResponseHeaders { get; set; }
5051

51-
internal Func<HttpRequestMessage, Task<bool>>? OnIntercepted { get; set; }
52+
internal Func<HttpRequestMessage, CancellationToken, Task<bool>>? OnIntercepted { get; set; }
5253

5354
internal Version? Version { get; set; }
5455
}

src/HttpClientInterception/HttpRequestInterceptionBuilder.cs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using System.IO;
77
using System.Net;
88
using System.Net.Http;
9+
using System.Threading;
910
using System.Threading.Tasks;
1011

1112
namespace JustEat.HttpClientInterception
@@ -35,7 +36,7 @@ public class HttpRequestInterceptionBuilder
3536

3637
private HttpMethod _method = HttpMethod.Get;
3738

38-
private Func<HttpRequestMessage, Task<bool>>? _onIntercepted;
39+
private Func<HttpRequestMessage, CancellationToken, Task<bool>>? _onIntercepted;
3940

4041
private Func<HttpRequestMessage, Task<bool>>? _requestMatcher;
4142

@@ -79,7 +80,7 @@ public HttpRequestInterceptionBuilder()
7980
/// </remarks>
8081
public HttpRequestInterceptionBuilder For(Predicate<HttpRequestMessage> predicate)
8182
{
82-
_requestMatcher = predicate == null ? null : DelegateHelpers.ConvertToBooleanTask(predicate);
83+
_requestMatcher = predicate == null ? null : new Func<HttpRequestMessage, Task<bool>>((message) => Task.FromResult(predicate(message)));
8384
return this;
8485
}
8586

@@ -690,6 +691,39 @@ public HttpRequestInterceptionBuilder WithInterceptionCallback(Func<HttpRequestM
690691
/// The current <see cref="HttpRequestInterceptionBuilder"/>.
691692
/// </returns>
692693
public HttpRequestInterceptionBuilder WithInterceptionCallback(Func<HttpRequestMessage, Task<bool>> onIntercepted)
694+
{
695+
_onIntercepted = DelegateHelpers.ConvertToBooleanTask(onIntercepted);
696+
return this;
697+
}
698+
699+
/// <summary>
700+
/// Sets an asynchronous callback to use to use when a request is intercepted that returns
701+
/// <see langword="true"/> if the request should be intercepted or <see langword="false"/>
702+
/// if the request should not be intercepted.
703+
/// </summary>
704+
/// <param name="onIntercepted">A delegate to a method to await when a request is intercepted.</param>
705+
/// <returns>
706+
/// The current <see cref="HttpRequestInterceptionBuilder"/>.
707+
/// </returns>
708+
public HttpRequestInterceptionBuilder WithInterceptionCallback(Func<HttpRequestMessage, CancellationToken, Task> onIntercepted)
709+
{
710+
_onIntercepted = DelegateHelpers.ConvertToBooleanTask(onIntercepted);
711+
return this;
712+
}
713+
714+
/// <summary>
715+
/// Sets an asynchronous callback to use to use when a request is intercepted that returns
716+
/// <see langword="true"/> if the request should be intercepted or <see langword="false"/>
717+
/// if the request should not be intercepted.
718+
/// </summary>
719+
/// <param name="onIntercepted">
720+
/// A delegate to a method to await when a request is intercepted which returns a <see langword="bool"/>
721+
/// indicating whether the request should be intercepted or not.
722+
/// </param>
723+
/// <returns>
724+
/// The current <see cref="HttpRequestInterceptionBuilder"/>.
725+
/// </returns>
726+
public HttpRequestInterceptionBuilder WithInterceptionCallback(Func<HttpRequestMessage, CancellationToken, Task<bool>> onIntercepted)
693727
{
694728
_onIntercepted = onIntercepted;
695729
return this;

src/HttpClientInterception/InterceptingHttpMessageHandler.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
5454
await _options.OnSend(request).ConfigureAwait(false);
5555
}
5656

57-
var response = await _options.GetResponseAsync(request).ConfigureAwait(false);
57+
var response = await _options.GetResponseAsync(request, cancellationToken).ConfigureAwait(false);
5858

5959
if (response == null && _options.OnMissingRegistration != null)
6060
{

src/HttpClientInterception/PublicAPI.Shipped.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithContentStream(
4848
JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithInterceptionCallback(System.Action<System.Net.Http.HttpRequestMessage> onIntercepted) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder
4949
JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithInterceptionCallback(System.Func<System.Net.Http.HttpRequestMessage, System.Threading.Tasks.Task<bool>> onIntercepted) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder
5050
JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithInterceptionCallback(System.Func<System.Net.Http.HttpRequestMessage, System.Threading.Tasks.Task> onIntercepted) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder
51+
JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithInterceptionCallback(System.Func<System.Net.Http.HttpRequestMessage, System.Threading.CancellationToken, System.Threading.Tasks.Task<bool>> onIntercepted) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder
52+
JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithInterceptionCallback(System.Func<System.Net.Http.HttpRequestMessage, System.Threading.CancellationToken, System.Threading.Tasks.Task> onIntercepted) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder
5153
JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithInterceptionCallback(System.Predicate<System.Net.Http.HttpRequestMessage> onIntercepted) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder
5254
JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithMediaType(string mediaType) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder
5355
JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithReason(string reasonPhrase) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder
@@ -88,4 +90,4 @@ static JustEat.HttpClientInterception.HttpRequestInterceptionBuilderExtensions.W
8890
static JustEat.HttpClientInterception.HttpRequestInterceptionBuilderExtensions.WithFormContent(this JustEat.HttpClientInterception.HttpRequestInterceptionBuilder builder, System.Collections.Generic.IEnumerable<System.Collections.Generic.KeyValuePair<string, string>> parameters) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder
8991
virtual JustEat.HttpClientInterception.HttpClientInterceptorOptions.CreateHttpClient(System.Net.Http.HttpMessageHandler innerHandler = null) -> System.Net.Http.HttpClient
9092
virtual JustEat.HttpClientInterception.HttpClientInterceptorOptions.CreateHttpMessageHandler() -> System.Net.Http.DelegatingHandler
91-
virtual JustEat.HttpClientInterception.HttpClientInterceptorOptions.GetResponseAsync(System.Net.Http.HttpRequestMessage request) -> System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage>
93+
virtual JustEat.HttpClientInterception.HttpClientInterceptorOptions.GetResponseAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task<System.Net.Http.HttpResponseMessage>

tests/HttpClientInterception.Tests/Examples.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using System.Net;
88
using System.Net.Http;
99
using System.Text.Json;
10+
using System.Threading;
1011
using System.Threading.Tasks;
1112
using JustEat.HttpClientInterception.GitHub;
1213
using Newtonsoft.Json.Linq;
@@ -627,5 +628,44 @@ public static async Task Intercept_Http_Get_For_Json_Object_Using_System_Text_Js
627628
content.RootElement.GetProperty("Id").GetInt32().ShouldBe(1);
628629
content.RootElement.GetProperty("Link").GetString().ShouldBe("https://www.just-eat.co.uk/privacy-policy");
629630
}
631+
632+
[Fact]
633+
public static async Task Inject_Latency_For_Http_Get_With_Cancellation()
634+
{
635+
// Arrange
636+
var latency = TimeSpan.FromMilliseconds(50);
637+
638+
var builder = new HttpRequestInterceptionBuilder()
639+
.ForHost("www.google.co.uk")
640+
.WithInterceptionCallback(async (_, token) =>
641+
{
642+
try
643+
{
644+
await Task.Delay(latency, token);
645+
}
646+
catch (TaskCanceledException)
647+
{
648+
// Ignored
649+
}
650+
finally
651+
{
652+
// Assert
653+
token.IsCancellationRequested.ShouldBeTrue();
654+
}
655+
});
656+
657+
var options = new HttpClientInterceptorOptions()
658+
.Register(builder);
659+
660+
using var cts = new CancellationTokenSource(TimeSpan.Zero);
661+
662+
using var client = options.CreateHttpClient();
663+
664+
// Act
665+
await client.GetAsync("http://www.google.co.uk", cts.Token);
666+
667+
// Assert
668+
cts.IsCancellationRequested.ShouldBeTrue();
669+
}
630670
}
631671
}

tests/HttpClientInterception.Tests/HttpClientInterceptorOptionsTests.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Net;
99
using System.Net.Http;
1010
using System.Net.Http.Headers;
11+
using System.Threading;
1112
using System.Threading.Tasks;
1213
using Shouldly;
1314
using Xunit;
@@ -762,9 +763,9 @@ internal HeaderMatchingOptions(Predicate<HttpRequestHeaders> matchHeaders)
762763
_matchHeaders = matchHeaders;
763764
}
764765

765-
public override async Task<HttpResponseMessage> GetResponseAsync(HttpRequestMessage request)
766+
public override async Task<HttpResponseMessage> GetResponseAsync(HttpRequestMessage request, CancellationToken cancellationToken = default)
766767
{
767-
HttpResponseMessage response = await base.GetResponseAsync(request);
768+
HttpResponseMessage response = await base.GetResponseAsync(request, cancellationToken);
768769

769770
if (response != null && _matchHeaders(request.Headers))
770771
{

0 commit comments

Comments
 (0)