diff --git a/CHANGELOG.md b/CHANGELOG.md index f935386..b77db61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## 1.1.0 + + * Add ability to customize the HttpClient global timeout + ## 1.0.0 * Add `netstandard1.6` target diff --git a/src/SpeakEasy.IntegrationTests/SpeakEasy.IntegrationTests.csproj b/src/SpeakEasy.IntegrationTests/SpeakEasy.IntegrationTests.csproj index 305ff6b..67b9d78 100644 --- a/src/SpeakEasy.IntegrationTests/SpeakEasy.IntegrationTests.csproj +++ b/src/SpeakEasy.IntegrationTests/SpeakEasy.IntegrationTests.csproj @@ -4,7 +4,7 @@ SpeakEasy.IntegrationTests 2.0.0 jonnii - netcoreapp2.0 + netcoreapp2.1 SpeakEasy.IntegrationTests SpeakEasy.IntegrationTests SpeakEasy.IntegrationTests diff --git a/src/SpeakEasy.Specifications/HttpClientSettingsSpecs.cs b/src/SpeakEasy.Specifications/HttpClientSettingsSpecs.cs index 088c3a3..e4608ea 100644 --- a/src/SpeakEasy.Specifications/HttpClientSettingsSpecs.cs +++ b/src/SpeakEasy.Specifications/HttpClientSettingsSpecs.cs @@ -133,13 +133,13 @@ class when_replacing_middleware_with_same_type settings.Middleware.Replace(replacement); It should_replace_middleware_in_place = () => - settings.Middleware.AtPosition(1).ShouldBeOfExactType(); + settings.Middleware.AtPosition(2).ShouldBeOfExactType(); It should_have_correct_middleware_count = () => - settings.Middleware.Count.ShouldEqual(3); + settings.Middleware.Count.ShouldEqual(4); It should_replace_instance = () => - settings.Middleware.AtPosition(1).ShouldBeTheSameAs(replacement); + settings.Middleware.AtPosition(2).ShouldBeTheSameAs(replacement); } class TestMiddleware : IHttpMiddleware diff --git a/src/SpeakEasy.Specifications/HttpClientSpecs.cs b/src/SpeakEasy.Specifications/HttpClientSpecs.cs index 57d0849..5e6ecc9 100644 --- a/src/SpeakEasy.Specifications/HttpClientSpecs.cs +++ b/src/SpeakEasy.Specifications/HttpClientSpecs.cs @@ -53,10 +53,24 @@ class when_building_system_client static SystemHttpClient system_http_client; Because of = () => - system_http_client = client.BuildSystemClient(new CookieContainer()); + system_http_client = client.BuildSystemClient(new CookieContainer(), null); It should_create_client = () => system_http_client.ShouldNotBeNull(); + + It should_create_with_system_default_timeout = () => + system_http_client.Timeout.ShouldEqual(new SystemHttpClient().Timeout); + } + + class when_building_system_client_with_custom_timeout + { + static SystemHttpClient system_http_client; + + Because of = () => + system_http_client = client.BuildSystemClient(new CookieContainer(), TimeSpan.FromMinutes(10)); + + It should_create_with_custom_timeout = () => + system_http_client.Timeout.ShouldEqual(TimeSpan.FromMinutes(10)); } class when_getting_collection_resource diff --git a/src/SpeakEasy.Specifications/UserAgentSpecs.cs b/src/SpeakEasy.Specifications/UserAgentSpecs.cs index 699b8c7..ab6a6c0 100644 --- a/src/SpeakEasy.Specifications/UserAgentSpecs.cs +++ b/src/SpeakEasy.Specifications/UserAgentSpecs.cs @@ -8,7 +8,7 @@ class UserAgentSpecs class default_user_agent { It should_contain_app_version = () => - UserAgent.SpeakEasy.Name.ShouldEqual("SpeakEasy/1.0.0.0"); + UserAgent.SpeakEasy.Name.ShouldContain("SpeakEasy/1.0"); } } } diff --git a/src/SpeakEasy/HttpClient.cs b/src/SpeakEasy/HttpClient.cs index 8181a3a..61a4daa 100644 --- a/src/SpeakEasy/HttpClient.cs +++ b/src/SpeakEasy/HttpClient.cs @@ -1,3 +1,4 @@ +using System; using System.Net; using System.Net.Http; using System.Threading; @@ -43,7 +44,7 @@ internal HttpClient(string rootUrl, HttpClientSettings settings) settings.Validate(); var cookieContainer = new CookieContainer(); - var client = BuildSystemClient(cookieContainer); + var client = BuildSystemClient(cookieContainer, settings.DefaultTimeout); requestRunner = new RequestRunner( client, @@ -75,7 +76,7 @@ internal HttpClient( public Resource Root { get; } - internal System.Net.Http.HttpClient BuildSystemClient(CookieContainer cookieContainer) + internal System.Net.Http.HttpClient BuildSystemClient(CookieContainer cookieContainer, TimeSpan? defaultTimeout) { var handler = new HttpClientHandler { @@ -89,6 +90,11 @@ internal System.Net.Http.HttpClient BuildSystemClient(CookieContainer cookieCont var httpClient = new System.Net.Http.HttpClient(handler); + if (defaultTimeout != null) + { + httpClient.Timeout = defaultTimeout.Value; + } + settings.Authenticator.Authenticate(httpClient); return httpClient; diff --git a/src/SpeakEasy/HttpClientSettings.cs b/src/SpeakEasy/HttpClientSettings.cs index ff64da5..37a7d68 100644 --- a/src/SpeakEasy/HttpClientSettings.cs +++ b/src/SpeakEasy/HttpClientSettings.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using SpeakEasy.ArrayFormatters; using SpeakEasy.Authenticators; using SpeakEasy.Middleware; @@ -26,6 +27,7 @@ public HttpClientSettings() Serializers.Add(new DefaultJsonSerializer()); Serializers.Add(new TextPlainSerializer()); + Middleware.Append(new TimeoutMiddleware()); Middleware.Append(new UserAgentMiddleware()); } @@ -69,6 +71,12 @@ public HttpClientSettings() /// public bool IsValid => Serializers.Any() && ArrayFormatter != null; + /// + /// The default timeout for the HttpClient to 30 minutes, + /// to use the system default (100 seconds) set this property to null. + /// + public TimeSpan? DefaultTimeout { get; set; } = TimeSpan.FromMinutes(30); + /// /// Configures the give serializer /// diff --git a/src/SpeakEasy/Middleware/TimeoutMiddleware.cs b/src/SpeakEasy/Middleware/TimeoutMiddleware.cs new file mode 100644 index 0000000..3fa606b --- /dev/null +++ b/src/SpeakEasy/Middleware/TimeoutMiddleware.cs @@ -0,0 +1,23 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace SpeakEasy.Middleware +{ + public class TimeoutMiddleware : IHttpMiddleware + { + public IHttpMiddleware Next { get; set; } + + public async Task Invoke(IHttpRequest request, CancellationToken cancellationToken) + { + try + { + return await Next.Invoke(request, cancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested) + { + throw new TimeoutException(); + } + } + } +}