Skip to content

Commit 9bbc949

Browse files
committed
[ECO-5650] Added a way to inject external HTTP client, updated tests for the same
1 parent 30f785e commit 9bbc949

File tree

14 files changed

+226
-34
lines changed

14 files changed

+226
-34
lines changed

.github/workflows/package.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ jobs:
8989
${{ github.workspace }}/*.nupkg
9090
9191
package-push:
92-
runs-on: macos-13
92+
runs-on: macos-14
9393
env:
9494
DOTNET_NOLOGO: true
9595

.github/workflows/run-tests-macos-mono.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77

88
jobs:
99
check:
10-
runs-on: macos-13
10+
runs-on: macos-14
1111
env:
1212
DOTNET_NOLOGO: true
1313

.github/workflows/run-tests-macos.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77

88
jobs:
99
check:
10-
runs-on: macos-13
10+
runs-on: macos-14
1111
env:
1212
DOTNET_NOLOGO: true
1313

src/IO.Ably.Shared/ClientOptions.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Net.Http;
34
using System.Threading;
45
using IO.Ably.Transport;
56
using Newtonsoft.Json;
@@ -413,6 +414,16 @@ public bool UseBinaryProtocol
413414
/// </summary>
414415
public Dictionary<string, string> Agents { get; set; }
415416

417+
/// <summary>
418+
/// Allows injection of an externally managed HttpClient instance.
419+
/// When provided, the library will use this HttpClient instead of creating its own.
420+
/// This enables proper HttpClient lifecycle management and connection pooling.
421+
/// The injected HttpClient should be configured with appropriate timeouts and handlers.
422+
/// Default: null (library creates its own HttpClient).
423+
/// </summary>
424+
[JsonIgnore]
425+
public HttpClient HttpClient { get; set; }
426+
416427
[JsonIgnore]
417428
internal Func<DateTimeOffset> NowFunc
418429
{

src/IO.Ably.Shared/Http/AblyHttpClient.cs

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ internal class AblyHttpClient : IAblyHttpClient
1414
private readonly Random _random = new Random();
1515
private string _realtimeConnectedFallbackHost;
1616

17-
internal AblyHttpClient(AblyHttpOptions options, HttpMessageHandler messageHandler = null)
17+
internal AblyHttpClient(AblyHttpOptions options)
1818
{
1919
Now = options.NowFunc;
2020
Logger = options.Logger ?? DefaultLogger.LoggerInstance;
2121
Options = options;
22-
CreateInternalHttpClient(options.HttpRequestTimeout, messageHandler);
22+
CreateInternalHttpClient(options.HttpRequestTimeout, options.HttpClient);
2323
SendAsync = InternalSendAsync;
2424
}
2525

@@ -68,11 +68,22 @@ internal void SetPreferredHost(string currentHost)
6868
}
6969
}
7070

71-
internal void CreateInternalHttpClient(TimeSpan timeout, HttpMessageHandler messageHandler)
71+
internal void CreateInternalHttpClient(TimeSpan timeout, HttpClient externalHttpClient = null)
7272
{
73-
Client = messageHandler != null ? new HttpClient(messageHandler) : new HttpClient();
74-
Client.DefaultRequestHeaders.Add("X-Ably-Version", Defaults.ProtocolVersion); // RSC7a
75-
Client.DefaultRequestHeaders.Add(Agent.AblyAgentHeader, Agent.AblyAgentIdentifier(Options.Agents)); // RSC7d
73+
Client = externalHttpClient ?? new HttpClient();
74+
75+
// RSC7a
76+
if (!Client.DefaultRequestHeaders.Contains("X-Ably-Version"))
77+
{
78+
Client.DefaultRequestHeaders.Add("X-Ably-Version", Defaults.ProtocolVersion);
79+
}
80+
81+
// RSC7d
82+
if (!Client.DefaultRequestHeaders.Contains(Agent.AblyAgentHeader))
83+
{
84+
Client.DefaultRequestHeaders.Add(Agent.AblyAgentHeader, Agent.AblyAgentIdentifier(Options.Agents));
85+
}
86+
7687
Client.Timeout = timeout;
7788
}
7889

src/IO.Ably.Shared/Http/AblyHttpOptions.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Net.Http;
34

45
namespace IO.Ably
56
{
@@ -37,6 +38,8 @@ internal class AblyHttpOptions
3738

3839
public Dictionary<string, string> Agents { get; set; }
3940

41+
public HttpClient HttpClient { get; set; }
42+
4043
public AblyHttpOptions()
4144
{
4245
// Used for testing
@@ -73,6 +76,7 @@ public AblyHttpOptions(ClientOptions options)
7376
FallbackHostsUseDefault = options.FallbackHostsUseDefault;
7477
AddRequestIds = options.AddRequestIds;
7578
Agents = options.Agents;
79+
HttpClient = options.HttpClient;
7680

7781
NowFunc = options.NowFunc;
7882
Logger = options.Logger;

src/IO.Ably.Tests.Shared/AuthTests/AuthSandboxSpecs.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public async Task RestClient_WhenTokenExpired_ShouldNotRetryAndRaiseError(Protoc
6262
// Get a very short lived token and wait for it to expire
6363
var authClient = await GetRestClient(protocol);
6464
var almostExpiredToken = await authClient.AblyAuth.RequestTokenAsync(new TokenParams { ClientId = "123", Ttl = TimeSpan.FromMilliseconds(1) });
65-
await Task.Delay(TimeSpan.FromMilliseconds(2));
65+
await Task.Delay(TimeSpan.FromMilliseconds(500));
6666

6767
// Modify the expiry date to fool the client it has a valid token
6868
almostExpiredToken.Expires = DateTimeOffset.UtcNow.AddHours(1);

src/IO.Ably.Tests.Shared/Infrastructure/AblyRealtimeSpecs.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Net.Http;
34
using System.Threading;
45
using System.Threading.Tasks;
56
using FluentAssertions;
@@ -75,13 +76,12 @@ private static AblyRealtime GetRealtimeClientWithFakeMessageHandler(ClientOption
7576
{
7677
var clientOptions = options ?? new ClientOptions(ValidKey);
7778
clientOptions.SkipInternetCheck = true; // This is for the Unit tests
78-
var client = new AblyRealtime(clientOptions, mobileDevice);
7979
if (fakeMessageHandler != null)
8080
{
81-
client.RestClient.HttpClient.CreateInternalHttpClient(TimeSpan.FromSeconds(10), fakeMessageHandler);
81+
clientOptions.HttpClient = new HttpClient(fakeMessageHandler);
8282
}
8383

84-
return client;
84+
return new AblyRealtime(clientOptions, mobileDevice);
8585
}
8686

8787
internal AblyRealtime GetRealtimeClient(Action<ClientOptions> optionsAction, Func<AblyRequest, Task<AblyResponse>> handleRequestFunc = null)

src/IO.Ably.Tests.Shared/Rest/AblyHttpClientSpecs.cs

Lines changed: 91 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Collections.Generic;
1+
using System;
2+
using System.Collections.Generic;
23
using System.Linq;
34
using System.Net;
45
using System.Net.Http;
@@ -41,7 +42,7 @@ public async Task WhenCallingUrl_AddsDefaultAblyLibraryVersionHeader()
4142
{
4243
var response = new HttpResponseMessage(HttpStatusCode.Accepted) { Content = new StringContent("Success") };
4344
var handler = new FakeHttpMessageHandler(response);
44-
var client = new AblyHttpClient(new AblyHttpOptions(), handler);
45+
var client = new AblyHttpClient(new AblyHttpOptions { HttpClient = new HttpClient(handler) });
4546

4647
await client.Execute(new AblyRequest("/test", HttpMethod.Get));
4748
var values = handler.LastRequest.Headers.GetValues("X-Ably-Version").ToArray();
@@ -55,7 +56,7 @@ public async Task WhenCallingUrl_AddsRequestIdIfSetTrue()
5556
{
5657
var response = new HttpResponseMessage(HttpStatusCode.Accepted) { Content = new StringContent("Success") };
5758
var handler = new FakeHttpMessageHandler(response);
58-
var client = new AblyHttpClient(new AblyHttpOptions { AddRequestIds = true }, handler);
59+
var client = new AblyHttpClient(new AblyHttpOptions { AddRequestIds = true, HttpClient = new HttpClient(handler) });
5960
var ablyRequest = new AblyRequest("/test", HttpMethod.Get);
6061
ablyRequest.AddHeaders(new Dictionary<string, string> { { "request_id", "custom_request_id" } });
6162
await client.Execute(ablyRequest);
@@ -69,7 +70,7 @@ public async Task WhenCallingUrlWithPostParamsAndEmptyBody_PassedTheParamsAsUrlE
6970
{
7071
var response = new HttpResponseMessage(HttpStatusCode.Accepted) { Content = new StringContent("Success") };
7172
var handler = new FakeHttpMessageHandler(response);
72-
var client = new AblyHttpClient(new AblyHttpOptions(), handler);
73+
var client = new AblyHttpClient(new AblyHttpOptions { HttpClient = new HttpClient(handler) });
7374

7475
var ablyRequest = new AblyRequest("/test", HttpMethod.Post)
7576
{
@@ -88,7 +89,7 @@ public async Task WhenCallingUrl_AddsDefaultAblyAgentHeader()
8889
{
8990
var response = new HttpResponseMessage(HttpStatusCode.Accepted) { Content = new StringContent("Success") };
9091
var handler = new FakeHttpMessageHandler(response);
91-
var client = new AblyHttpClient(new AblyHttpOptions(), handler);
92+
var client = new AblyHttpClient(new AblyHttpOptions { HttpClient = new HttpClient(handler) });
9293

9394
await client.Execute(new AblyRequest("/test", HttpMethod.Get));
9495
string[] values = handler.LastRequest.Headers.GetValues("Ably-Agent").ToArray();
@@ -127,9 +128,10 @@ public async Task WhenCallingUrl_AddsCustomizedAblyAgentHeader()
127128
{ "agent1", "value1" },
128129
{ "agent2", "value2" },
129130
},
131+
HttpClient = new HttpClient(handler)
130132
};
131133

132-
var client = new AblyHttpClient(ablyHttpOptions, handler);
134+
var client = new AblyHttpClient(ablyHttpOptions);
133135

134136
await client.Execute(new AblyRequest("/test", HttpMethod.Get));
135137
string[] values = handler.LastRequest.Headers.GetValues("Ably-Agent").ToArray();
@@ -190,5 +192,88 @@ public void IsRetryableResponse_WithErrorCode_ShouldReturnExpectedValue(
190192
AblyHttpClient.IsRetryableResponse(response).Should().Be(expected);
191193
}
192194
}
195+
196+
public class ExternalHttpClientSpecs
197+
{
198+
[Fact]
199+
public void WithExternalHttpClient_ShouldUseProvidedClient()
200+
{
201+
// Arrange
202+
var externalHttpClient = new HttpClient();
203+
var options = new AblyHttpOptions { HttpClient = externalHttpClient };
204+
205+
// Act
206+
var ablyHttpClient = new AblyHttpClient(options);
207+
208+
// Assert
209+
ablyHttpClient.Client.Should().BeSameAs(externalHttpClient);
210+
}
211+
212+
[Fact]
213+
public void WithExternalHttpClient_ShouldStillAddAblyHeaders()
214+
{
215+
// Arrange
216+
var externalHttpClient = new HttpClient();
217+
var options = new AblyHttpOptions { HttpClient = externalHttpClient };
218+
219+
// Act
220+
var ablyHttpClient = new AblyHttpClient(options);
221+
222+
// Assert
223+
ablyHttpClient.Client.DefaultRequestHeaders.Contains("X-Ably-Version").Should().BeTrue();
224+
ablyHttpClient.Client.DefaultRequestHeaders.Contains("Ably-Agent").Should().BeTrue();
225+
}
226+
227+
[Fact]
228+
public void WithExternalHttpClient_ShouldSetTimeout()
229+
{
230+
// Arrange
231+
var externalHttpClient = new HttpClient();
232+
var timeout = TimeSpan.FromSeconds(30);
233+
var options = new AblyHttpOptions
234+
{
235+
HttpClient = externalHttpClient,
236+
HttpRequestTimeout = timeout
237+
};
238+
239+
// Act
240+
var ablyHttpClient = new AblyHttpClient(options);
241+
242+
// Assert
243+
ablyHttpClient.Client.Timeout.Should().Be(timeout);
244+
}
245+
246+
[Fact]
247+
public void WithoutExternalHttpClient_ShouldCreateNewClient()
248+
{
249+
// Arrange
250+
var options = new AblyHttpOptions();
251+
options.HttpClient.Should().BeNull();
252+
253+
// Act
254+
var ablyHttpClient = new AblyHttpClient(options);
255+
256+
// Assert
257+
ablyHttpClient.Client.Should().NotBeNull();
258+
}
259+
260+
[Fact]
261+
public async Task WithExternalHttpClient_ShouldMakeSuccessfulRequests()
262+
{
263+
// Arrange
264+
var response = new HttpResponseMessage(HttpStatusCode.Accepted) { Content = new StringContent("Success") };
265+
var handler = new FakeHttpMessageHandler(response);
266+
var externalHttpClient = new HttpClient(handler);
267+
var options = new AblyHttpOptions { HttpClient = externalHttpClient };
268+
var ablyHttpClient = new AblyHttpClient(options);
269+
270+
// Act
271+
var result = await ablyHttpClient.Execute(new AblyRequest("/test", HttpMethod.Get));
272+
273+
// Assert
274+
result.StatusCode.Should().Be(HttpStatusCode.Accepted);
275+
handler.NumberOfRequests.Should().Be(1);
276+
}
277+
}
193278
}
194279
}

0 commit comments

Comments
 (0)