Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/servus.akka
6 changes: 3 additions & 3 deletions src/GaudiHTTP.API.Tests/CoreAPISpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ namespace GaudiHTTP.API.Tests;

public class CoreAPISpec
{
private static readonly ApiGeneratorOptions ApiOptions = new()
private static ApiGeneratorOptions MakeApiOptions() => new()
{
ExcludeAttributes =
[
Expand All @@ -17,10 +17,10 @@ public class CoreAPISpec

private static Task VerifyAssembly<T>()
{
return Verify(typeof(T).Assembly.GeneratePublicApi(ApiOptions));
return Verify(typeof(T).Assembly.GeneratePublicApi(MakeApiOptions()));
}

[Fact]
[Fact(Timeout = 5000)]
public Task ApproveCore()
{
return VerifyAssembly<IGaudiHttpClient>();
Expand Down
4 changes: 2 additions & 2 deletions src/GaudiHTTP.AcceptanceTests/H10/RedirectSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ private async Task<HttpResponseMessage> SendAsync(ResponseMap map, HttpRequestMe

private static ResponseMap CreateBaseMap() => new ResponseMap()
.On("/hello", HttpStatusCode.OK, "Hello World")
.On("/echo", req =>
.On("/echo", async req =>
{
var body = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult() ?? "";
var body = req.Content != null ? await req.Content.ReadAsStringAsync() : "";
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(body)
Expand Down
4 changes: 2 additions & 2 deletions src/GaudiHTTP.AcceptanceTests/H2/RedirectSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ private async Task<HttpResponseMessage> SendAsync(ResponseMap map, HttpRequestMe

private static ResponseMap CreateBaseMap() => new ResponseMap()
.On("/hello", HttpStatusCode.OK, "Hello World")
.On("/echo", req =>
.On("/echo", async req =>
{
var body = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult() ?? "";
var body = req.Content != null ? await req.Content.ReadAsStringAsync() : "";
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(body)
Expand Down
4 changes: 2 additions & 2 deletions src/GaudiHTTP.AcceptanceTests/H3/RedirectSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ private async Task<HttpResponseMessage> SendAsync(ResponseMap map, HttpRequestMe

private static ResponseMap CreateBaseMap() => new ResponseMap()
.On("/hello", HttpStatusCode.OK, "Hello World")
.On("/echo", req =>
.On("/echo", async req =>
{
var body = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult() ?? "";
var body = req.Content != null ? await req.Content.ReadAsStringAsync() : "";
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(body)
Expand Down
4 changes: 2 additions & 2 deletions src/GaudiHTTP.AcceptanceTests/TLS/IntegrationSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ public async Task Post_echo_should_echo_body_over_https()
{
var payload = "TLS echo payload";
var map = new ResponseMap()
.On("/echo", req =>
.On("/echo", async req =>
{
var reqBody = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult() ?? "";
var reqBody = req.Content != null ? await req.Content.ReadAsStringAsync() : "";
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(reqBody)
Expand Down
4 changes: 2 additions & 2 deletions src/GaudiHTTP.AcceptanceTests/TLS/RedirectSpec.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ private async Task<HttpResponseMessage> SendAsync(ResponseMap map, HttpRequestMe

private static ResponseMap CreateBaseMap() => new ResponseMap()
.On("/hello", HttpStatusCode.OK, "Hello World")
.On("/echo", req =>
.On("/echo", async req =>
{
var body = req.Content?.ReadAsStringAsync().GetAwaiter().GetResult() ?? "";
var body = req.Content != null ? await req.Content.ReadAsStringAsync() : "";
return new HttpResponseMessage(HttpStatusCode.OK)
{
Content = new StringContent(body)
Expand Down
52 changes: 40 additions & 12 deletions src/GaudiHTTP.Benchmarks/Internal/BenchmarkServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,14 @@ public sealed class BenchmarkServer : IAsyncDisposable

public int Http30Port { get; private set; }

public bool IsQuicAvailable { get; private set; }

public async ValueTask InitializeAsync(IAllocationProfiler? profiler = null)
{
_cert = GenerateSelfSignedCert();

var quicAvailable = QuicListenerIsSupported();

var builder = WebApplication.CreateBuilder();
builder.Logging.ClearProviders();

Expand All @@ -41,12 +45,15 @@ public async ValueTask InitializeAsync(IAllocationProfiler? profiler = null)
options.Listen(IPAddress.Loopback, 0, lo =>
lo.Protocols = HttpProtocols.Http2);

// HTTP/3 (QUIC+TLS) listener
options.Listen(IPAddress.Loopback, 0, lo =>
if (quicAvailable)
{
lo.Protocols = HttpProtocols.Http3;
lo.UseHttps(cert);
});
// HTTP/3 (QUIC+TLS) listener
options.Listen(IPAddress.Loopback, 0, lo =>
{
lo.Protocols = HttpProtocols.Http3;
lo.UseHttps(cert);
});
}

// Raise HTTP/2 limits to support high-concurrency benchmarks (CL=256+).
options.Limits.Http2.MaxStreamsPerConnection = 512;
Expand All @@ -59,32 +66,53 @@ public async ValueTask InitializeAsync(IAllocationProfiler? profiler = null)
options.Limits.MaxConcurrentUpgradedConnections = null;
});

builder.WebHost.UseQuic(quic =>
if (quicAvailable)
{
quic.MaxBidirectionalStreamCount = 512;
quic.MaxUnidirectionalStreamCount = 32;
});
builder.WebHost.UseQuic(quic =>
{
quic.MaxBidirectionalStreamCount = 512;
quic.MaxUnidirectionalStreamCount = 32;
});
}

var app = builder.Build();

RegisterRoutes(app, profiler);

await app.StartAsync();

// Kestrel returns addresses in listener-registration order:
// index 0 = HTTP/1.1, index 1 = HTTP/2, index 2 = HTTP/3
var addresses = app.Services.GetRequiredService<IServer>()
.Features.Get<IServerAddressesFeature>()!
.Addresses
.ToArray();

Http11Port = new Uri(addresses[0]).Port;
Http20Port = new Uri(addresses[1]).Port;
Http30Port = new Uri(addresses[2]).Port;
Http30Port = quicAvailable ? new Uri(addresses[2]).Port : 0;
IsQuicAvailable = quicAvailable;

_app = app;
}

private static bool QuicListenerIsSupported()
{
try
{
var type = Type.GetType("System.Net.Quic.QuicListener, System.Net.Quic");
if (type is null)
{
return false;
}

var prop = type.GetProperty("IsSupported", System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
return prop?.GetValue(null) is true;
}
catch
{
return false;
}
}

public async ValueTask DisposeAsync()
{
if (_app is not null)
Expand Down
2 changes: 1 addition & 1 deletion src/GaudiHTTP.Benchmarks/Internal/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public EngineBenchmarkConfig()
WithArtifactsPath(artifactsPath);
AddJob(Job.Default.WithGcServer(true));
AddDiagnoser(MemoryDiagnoser.Default);
AddDiagnoser(new EventPipeProfiler(EventPipeProfile.GcVerbose));
AddDiagnoser(ThreadingDiagnoser.Default);
AddExporter(MarkdownExporter.GitHub);
AddExporter(HttpVersionColorExporter.Default);
AddExporter(AllocationByTypeExporter.Default);
Expand Down
12 changes: 0 additions & 12 deletions src/GaudiHTTP.IntegrationTests.Client/TestInitializer.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ public override HttpRequestMessage ProcessRequest(HttpRequestMessage request)
public async Task Handler_should_inject_request_headers_that_reach_server()
{
// Create a separate client with header-injecting handler
var system = await GetActorSystemAsync();
var client = CreateClientWithHandler<HeaderInjectionHandler>();
var client = await CreateClientWithHandlerAsync<HeaderInjectionHandler>();

var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/echo-headers");
var response = await client.SendAsync(request, CancellationToken);
Expand All @@ -77,7 +76,7 @@ public async Task Handler_should_inject_request_headers_that_reach_server()
[Fact(Timeout = 10000)]
public async Task Handler_should_fail_per_request_when_throwing()
{
var client = CreateClientWithHandler<FailingHandler>();
var client = await CreateClientWithHandlerAsync<FailingHandler>();

var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/ping");
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => client.SendAsync(request, CancellationToken));
Expand All @@ -87,7 +86,7 @@ public async Task Handler_should_fail_per_request_when_throwing()
[Fact(Timeout = 10000)]
public async Task Handler_should_fail_only_faulted_request_while_others_succeed()
{
var client = CreateClientWithHandler<ConditionalFailingHandler>();
var client = await CreateClientWithHandlerAsync<ConditionalFailingHandler>();

// Send a failing request
var failingRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/ping");
Expand All @@ -107,10 +106,10 @@ public async Task Handler_should_fail_only_faulted_request_while_others_succeed(
Assert.Equal(HttpStatusCode.OK, goodResponse.StatusCode);
}

private IGaudiHttpClient CreateClientWithHandler<THandler>() where THandler : GaudiHandler
private async Task<IGaudiHttpClient> CreateClientWithHandlerAsync<THandler>() where THandler : GaudiHandler
{
var services = new ServiceCollection();
services.AddSingleton(GetActorSystemAsync().Result);
services.AddSingleton(await GetActorSystemAsync());

var clientOptions = new GaudiClientOptions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ public override HttpRequestMessage ProcessRequest(HttpRequestMessage request)
public async Task Handler_should_inject_request_headers_that_reach_server()
{
// Create a separate client with header-injecting handler
var system = await GetActorSystemAsync();
var client = CreateClientWithHandler<HeaderInjectionHandler>();
var client = await CreateClientWithHandlerAsync<HeaderInjectionHandler>();

var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/echo-headers");
var response = await client.SendAsync(request, CancellationToken);
Expand All @@ -77,7 +76,7 @@ public async Task Handler_should_inject_request_headers_that_reach_server()
[Fact(Timeout = 10000)]
public async Task Handler_should_fail_per_request_when_throwing()
{
var client = CreateClientWithHandler<FailingHandler>();
var client = await CreateClientWithHandlerAsync<FailingHandler>();

var request = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/ping");
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => client.SendAsync(request, CancellationToken));
Expand All @@ -87,7 +86,7 @@ public async Task Handler_should_fail_per_request_when_throwing()
[Fact(Timeout = 10000)]
public async Task Handler_should_fail_only_faulted_request_while_others_succeed()
{
var client = CreateClientWithHandler<ConditionalFailingHandler>();
var client = await CreateClientWithHandlerAsync<ConditionalFailingHandler>();

// Send a failing request
var failingRequest = new HttpRequestMessage(HttpMethod.Get, $"{BaseUri}/ping");
Expand All @@ -107,10 +106,10 @@ public async Task Handler_should_fail_only_faulted_request_while_others_succeed(
Assert.Equal(HttpStatusCode.OK, goodResponse.StatusCode);
}

private IGaudiHttpClient CreateClientWithHandler<THandler>() where THandler : GaudiHandler
private async Task<IGaudiHttpClient> CreateClientWithHandlerAsync<THandler>() where THandler : GaudiHandler
{
var services = new ServiceCollection();
services.AddSingleton(GetActorSystemAsync().Result);
services.AddSingleton(await GetActorSystemAsync());

var clientOptions = new GaudiClientOptions
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,8 @@ public async ValueTask DisposeAsync()

await _clientProvider.DisposeAsync();
}

Tracing.Disable();
}

/// <summary>
Expand Down
1 change: 1 addition & 0 deletions src/GaudiHTTP.Tests.Shared/ActorSystemFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ public async ValueTask DisposeAsync()
{
await System.Terminate().WaitAsync(TimeSpan.FromSeconds(30));
await System.WhenTerminated.WaitAsync(TimeSpan.FromSeconds(30));
Servus.Senf.Tracing.Disable();
}
}
5 changes: 3 additions & 2 deletions src/GaudiHTTP.Tests.Shared/FakeResponse.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
using System.Collections.Frozen;
using System.Text;

namespace GaudiHTTP.Tests.Shared;

public static class FakeResponse
{
private static readonly Dictionary<int, string> ReasonPhrases = new()
private static readonly FrozenDictionary<int, string> ReasonPhrases = new Dictionary<int, string>
{
[200] = "OK", [201] = "Created", [204] = "No Content",
[301] = "Moved Permanently", [302] = "Found", [304] = "Not Modified",
[307] = "Temporary Redirect", [308] = "Permanent Redirect",
[400] = "Bad Request", [401] = "Unauthorized", [403] = "Forbidden",
[404] = "Not Found", [429] = "Too Many Requests",
[500] = "Internal Server Error", [502] = "Bad Gateway", [503] = "Service Unavailable"
};
}.ToFrozenDictionary();

private static string GetReason(int status) => ReasonPhrases.GetValueOrDefault(status, "Unknown");

Expand Down
21 changes: 16 additions & 5 deletions src/GaudiHTTP.Tests.Shared/ResponseMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace GaudiHTTP.Tests.Shared;
/// </summary>
public sealed class ResponseMap
{
private readonly List<(Func<HttpRequestMessage, bool> Predicate, Func<HttpRequestMessage, HttpResponseMessage> Factory)> _mappings = [];
private readonly List<(Func<HttpRequestMessage, bool> Predicate, Func<HttpRequestMessage, Task<HttpResponseMessage>> Factory)> _mappings = [];

/// <summary>
/// Maps a request path to a static response with the given status and body.
Expand All @@ -25,7 +25,7 @@ public ResponseMap On(string path, HttpStatusCode status, string body)
{
Content = new StringContent(body)
};
return response;
return Task.FromResult(response);
}
));
return this;
Expand All @@ -35,6 +35,17 @@ public ResponseMap On(string path, HttpStatusCode status, string body)
/// Maps a request path to a dynamic response produced by the given factory.
/// </summary>
public ResponseMap On(string path, Func<HttpRequestMessage, HttpResponseMessage> factory)
{
_mappings.Add((
req => string.Equals(req.RequestUri?.AbsolutePath, path, StringComparison.OrdinalIgnoreCase),
req => Task.FromResult(factory(req))));
return this;
}

/// <summary>
/// Maps a request path to an async response produced by the given factory.
/// </summary>
public ResponseMap On(string path, Func<HttpRequestMessage, Task<HttpResponseMessage>> factory)
{
_mappings.Add((
req => string.Equals(req.RequestUri?.AbsolutePath, path, StringComparison.OrdinalIgnoreCase),
Expand All @@ -48,20 +59,20 @@ public ResponseMap On(string path, Func<HttpRequestMessage, HttpResponseMessage>
/// </summary>
public ResponseMap On(Func<HttpRequestMessage, bool> predicate, Func<HttpRequestMessage, HttpResponseMessage> factory)
{
_mappings.Add((predicate, factory));
_mappings.Add((predicate, req => Task.FromResult(factory(req))));
return this;
}

/// <summary>
/// Resolves a request to a response. Returns a 404 for unmapped paths.
/// </summary>
internal HttpResponseMessage Resolve(HttpRequestMessage request)
internal async Task<HttpResponseMessage> ResolveAsync(HttpRequestMessage request)
{
foreach (var (predicate, factory) in _mappings)
{
if (predicate(request))
{
var response = factory(request);
var response = await factory(request);
response.RequestMessage = request;
return response;
}
Expand Down
Loading
Loading