diff --git a/AwsLambda.Host.sln b/AwsLambda.Host.sln
index 14a1145b..5978adc2 100644
--- a/AwsLambda.Host.sln
+++ b/AwsLambda.Host.sln
@@ -78,7 +78,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AwsLambda.Host.Envelopes.Cl
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AwsLambda.Host.Testing", "src\AwsLambda.Host.Testing\AwsLambda.Host.Testing.csproj", "{9FA188D7-CF5F-4F87-B292-2AF69994FF12}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AwsLambda.Host.Examples.Testing", "examples\AwsLambda.Host.Examples.Testing\AwsLambda.Host.Examples.Testing.csproj", "{884FD7E2-72D3-422B-82B4-FF5E7F92CE6B}"
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AwsLambda.Host.Example.Testing", "AwsLambda.Host.Example.Testing", "{26B446C5-76B0-4190-9022-1BF514737E2E}"
+ ProjectSection(SolutionItems) = preProject
+ examples\AwsLambda.Host.Example.Testing\Properties\launchSettings.json = examples\AwsLambda.Host.Example.Testing\Properties\launchSettings.json
+ EndProjectSection
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests", "examples\AwsLambda.Host.Example.Testing\Tests\Tests.csproj", "{0EDDAE60-A252-4CB0-85E2-FEA0CA0C5818}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lambda", "examples\AwsLambda.Host.Example.Testing\Lambda\Lambda.csproj", "{6DB827E6-C534-4C6A-9128-05F6A05DD6C5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -366,18 +373,30 @@ Global
{9FA188D7-CF5F-4F87-B292-2AF69994FF12}.Release|x64.Build.0 = Release|Any CPU
{9FA188D7-CF5F-4F87-B292-2AF69994FF12}.Release|x86.ActiveCfg = Release|Any CPU
{9FA188D7-CF5F-4F87-B292-2AF69994FF12}.Release|x86.Build.0 = Release|Any CPU
- {884FD7E2-72D3-422B-82B4-FF5E7F92CE6B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {884FD7E2-72D3-422B-82B4-FF5E7F92CE6B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {884FD7E2-72D3-422B-82B4-FF5E7F92CE6B}.Debug|x64.ActiveCfg = Debug|Any CPU
- {884FD7E2-72D3-422B-82B4-FF5E7F92CE6B}.Debug|x64.Build.0 = Debug|Any CPU
- {884FD7E2-72D3-422B-82B4-FF5E7F92CE6B}.Debug|x86.ActiveCfg = Debug|Any CPU
- {884FD7E2-72D3-422B-82B4-FF5E7F92CE6B}.Debug|x86.Build.0 = Debug|Any CPU
- {884FD7E2-72D3-422B-82B4-FF5E7F92CE6B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {884FD7E2-72D3-422B-82B4-FF5E7F92CE6B}.Release|Any CPU.Build.0 = Release|Any CPU
- {884FD7E2-72D3-422B-82B4-FF5E7F92CE6B}.Release|x64.ActiveCfg = Release|Any CPU
- {884FD7E2-72D3-422B-82B4-FF5E7F92CE6B}.Release|x64.Build.0 = Release|Any CPU
- {884FD7E2-72D3-422B-82B4-FF5E7F92CE6B}.Release|x86.ActiveCfg = Release|Any CPU
- {884FD7E2-72D3-422B-82B4-FF5E7F92CE6B}.Release|x86.Build.0 = Release|Any CPU
+ {0EDDAE60-A252-4CB0-85E2-FEA0CA0C5818}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0EDDAE60-A252-4CB0-85E2-FEA0CA0C5818}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0EDDAE60-A252-4CB0-85E2-FEA0CA0C5818}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {0EDDAE60-A252-4CB0-85E2-FEA0CA0C5818}.Debug|x64.Build.0 = Debug|Any CPU
+ {0EDDAE60-A252-4CB0-85E2-FEA0CA0C5818}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {0EDDAE60-A252-4CB0-85E2-FEA0CA0C5818}.Debug|x86.Build.0 = Debug|Any CPU
+ {0EDDAE60-A252-4CB0-85E2-FEA0CA0C5818}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0EDDAE60-A252-4CB0-85E2-FEA0CA0C5818}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0EDDAE60-A252-4CB0-85E2-FEA0CA0C5818}.Release|x64.ActiveCfg = Release|Any CPU
+ {0EDDAE60-A252-4CB0-85E2-FEA0CA0C5818}.Release|x64.Build.0 = Release|Any CPU
+ {0EDDAE60-A252-4CB0-85E2-FEA0CA0C5818}.Release|x86.ActiveCfg = Release|Any CPU
+ {0EDDAE60-A252-4CB0-85E2-FEA0CA0C5818}.Release|x86.Build.0 = Release|Any CPU
+ {6DB827E6-C534-4C6A-9128-05F6A05DD6C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6DB827E6-C534-4C6A-9128-05F6A05DD6C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6DB827E6-C534-4C6A-9128-05F6A05DD6C5}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {6DB827E6-C534-4C6A-9128-05F6A05DD6C5}.Debug|x64.Build.0 = Debug|Any CPU
+ {6DB827E6-C534-4C6A-9128-05F6A05DD6C5}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {6DB827E6-C534-4C6A-9128-05F6A05DD6C5}.Debug|x86.Build.0 = Debug|Any CPU
+ {6DB827E6-C534-4C6A-9128-05F6A05DD6C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6DB827E6-C534-4C6A-9128-05F6A05DD6C5}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6DB827E6-C534-4C6A-9128-05F6A05DD6C5}.Release|x64.ActiveCfg = Release|Any CPU
+ {6DB827E6-C534-4C6A-9128-05F6A05DD6C5}.Release|x64.Build.0 = Release|Any CPU
+ {6DB827E6-C534-4C6A-9128-05F6A05DD6C5}.Release|x86.ActiveCfg = Release|Any CPU
+ {6DB827E6-C534-4C6A-9128-05F6A05DD6C5}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -407,6 +426,8 @@ Global
{DA647025-1B7B-425A-9405-8E015F6DA723} = {1C3C52D9-2936-4A0C-A9C8-F330F22B8359}
{6D40345B-6CCF-4A6C-8FA2-5BE6C693E3F7} = {1C3C52D9-2936-4A0C-A9C8-F330F22B8359}
{9FA188D7-CF5F-4F87-B292-2AF69994FF12} = {827E0CD3-B72D-47B6-A68D-7590B98EB39B}
- {884FD7E2-72D3-422B-82B4-FF5E7F92CE6B} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}
+ {26B446C5-76B0-4190-9022-1BF514737E2E} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F}
+ {0EDDAE60-A252-4CB0-85E2-FEA0CA0C5818} = {26B446C5-76B0-4190-9022-1BF514737E2E}
+ {6DB827E6-C534-4C6A-9128-05F6A05DD6C5} = {26B446C5-76B0-4190-9022-1BF514737E2E}
EndGlobalSection
EndGlobal
diff --git a/examples/AwsLambda.Host.Example.Testing/Lambda/Lambda.csproj b/examples/AwsLambda.Host.Example.Testing/Lambda/Lambda.csproj
new file mode 100644
index 00000000..cfcc1781
--- /dev/null
+++ b/examples/AwsLambda.Host.Example.Testing/Lambda/Lambda.csproj
@@ -0,0 +1,31 @@
+
+
+ Exe
+ net10.0
+ preview
+ enable
+ enable
+ true
+ Lambda
+
+ true
+
+ true
+ $(InterceptorsNamespaces);AwsLambda.Host
+ false
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
diff --git a/examples/AwsLambda.Host.Example.Testing/Lambda/Program.cs b/examples/AwsLambda.Host.Example.Testing/Lambda/Program.cs
new file mode 100644
index 00000000..fc3329b0
--- /dev/null
+++ b/examples/AwsLambda.Host.Example.Testing/Lambda/Program.cs
@@ -0,0 +1,22 @@
+using AwsLambda.Host.Builder;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+
+// Create the application builder
+var builder = LambdaApplication.CreateBuilder();
+
+builder.Services.ConfigureLambdaHostOptions(options =>
+{
+ options.ClearLambdaOutputFormatting = true;
+});
+
+// Build the Lambda application
+var lambda = builder.Build();
+
+// Map your handler - the event is automatically injected
+lambda.MapHandler(([Event] string name) => $"Hello {name}!");
+
+// Run the Lambda
+await lambda.RunAsync();
+
+public partial class Program;
diff --git a/examples/AwsLambda.Host.Examples.Testing/Properties/launchSettings.json b/examples/AwsLambda.Host.Example.Testing/Lambda/Properties/launchSettings.json
similarity index 100%
rename from examples/AwsLambda.Host.Examples.Testing/Properties/launchSettings.json
rename to examples/AwsLambda.Host.Example.Testing/Lambda/Properties/launchSettings.json
diff --git a/examples/AwsLambda.Host.Examples.Testing/appsettings.json b/examples/AwsLambda.Host.Example.Testing/Lambda/appsettings.json
similarity index 100%
rename from examples/AwsLambda.Host.Examples.Testing/appsettings.json
rename to examples/AwsLambda.Host.Example.Testing/Lambda/appsettings.json
diff --git a/examples/AwsLambda.Host.Example.Testing/Tests/LambdaHostTest.cs b/examples/AwsLambda.Host.Example.Testing/Tests/LambdaHostTest.cs
new file mode 100644
index 00000000..9d736874
--- /dev/null
+++ b/examples/AwsLambda.Host.Example.Testing/Tests/LambdaHostTest.cs
@@ -0,0 +1,139 @@
+using AwsLambda.Host.Options;
+using AwsLambda.Host.Testing;
+using JetBrains.Annotations;
+using Microsoft.Extensions.DependencyInjection;
+using Xunit;
+
+namespace Lambda.Host.Example.HelloWorld;
+
+[TestSubject(typeof(Program))]
+public class LambdaHostTest
+{
+ [Fact]
+ public async Task LambdaHost_CanStartWithoutError()
+ {
+ await using var factory = new LambdaApplicationFactory();
+
+ var client = factory.CreateClient();
+ // No need to wait for next request - server handles this automatically
+ var response = await client.InvokeAsync(
+ "Jonas",
+ TestContext.Current.CancellationToken
+ );
+ Assert.True(response.WasSuccess);
+ Assert.NotNull(response);
+ Assert.Equal("Hello Jonas!", response.Response);
+ }
+
+ [Fact]
+ public async Task LambdaHost_CrashesWithBadConfiguration_ThrowsException()
+ {
+ await using var factory = new LambdaApplicationFactory().WithWebHostBuilder(
+ builder =>
+ {
+ builder.ConfigureServices(
+ (_, services) =>
+ {
+ services.Configure(options =>
+ {
+ options.BootstrapOptions.RuntimeApiEndpoint = "http://localhost:3002";
+ });
+ }
+ );
+ }
+ );
+
+ var client = factory.CreateClient();
+ // No need to wait for next request - server handles this automatically
+ var response = await client.InvokeAsync(
+ "Jonas",
+ TestContext.Current.CancellationToken
+ );
+ Assert.True(response.WasSuccess);
+ Assert.NotNull(response);
+ Assert.Equal("Hello Jonas!", response.Response);
+ }
+
+ [Fact]
+ public async Task LambdaHost_ProcessesConcurrentInvocationsInFifoOrder()
+ {
+ await using var factory = new LambdaApplicationFactory();
+ var client = factory.CreateClient();
+
+ // Launch 5 concurrent invocations
+ var tasks = Enumerable
+ .Range(1, 5)
+ .Select(i =>
+ client.InvokeAsync(
+ $"User{i}",
+ TestContext.Current.CancellationToken
+ )
+ )
+ .ToArray();
+
+ var responses = await Task.WhenAll(tasks);
+
+ // All should complete successfully
+ Assert.All(responses, r => Assert.True(r.WasSuccess));
+ Assert.Equal("Hello User1!", responses[0].Response);
+ Assert.Equal("Hello User2!", responses[1].Response);
+ Assert.Equal("Hello User3!", responses[2].Response);
+ Assert.Equal("Hello User4!", responses[3].Response);
+ Assert.Equal("Hello User5!", responses[4].Response);
+ }
+
+ [Fact]
+ public async Task InvokeAsync_WithInvalidPayload_ReturnsError()
+ {
+ await using var factory = new LambdaApplicationFactory();
+ var client = factory.CreateClient();
+
+ var response = await client.InvokeAsync(
+ 123,
+ TestContext.Current.CancellationToken
+ );
+
+ Assert.False(response.WasSuccess);
+ Assert.NotNull(response.Error);
+ Assert.Contains("Json", response.Error!.ErrorType, StringComparison.OrdinalIgnoreCase);
+ }
+
+ [Fact]
+ public async Task InvokeAsync_WithPreCanceledToken_CancelsInvocation()
+ {
+ await using var factory = new LambdaApplicationFactory();
+ var client = factory.CreateClient();
+
+ using var cts = new CancellationTokenSource();
+ await cts.CancelAsync();
+
+ await Assert.ThrowsAsync(() =>
+ client.InvokeAsync("Jonas", cts.Token)
+ );
+ }
+
+ [Fact]
+ public async Task InvokeAsync_WithZeroTimeout_CancelsInvocation() =>
+ await Assert.ThrowsAsync(async () =>
+ {
+ try
+ {
+ await using var factory = new LambdaApplicationFactory();
+ var client = factory
+ .CreateClient()
+ .ConfigureOptions(options =>
+ options.InvocationHeaderOptions.ClientWaitTimeout = TimeSpan.Zero
+ );
+
+ await client.InvokeAsync(
+ "Jonas",
+ TestContext.Current.CancellationToken
+ );
+ }
+ catch (Exception e)
+ {
+ Console.WriteLine(e.GetType().FullName);
+ throw;
+ }
+ });
+}
diff --git a/examples/AwsLambda.Host.Example.Testing/Tests/Tests.csproj b/examples/AwsLambda.Host.Example.Testing/Tests/Tests.csproj
new file mode 100644
index 00000000..b686ce26
--- /dev/null
+++ b/examples/AwsLambda.Host.Example.Testing/Tests/Tests.csproj
@@ -0,0 +1,32 @@
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
diff --git a/examples/AwsLambda.Host.Examples.Testing/AwsLambda.Host.Examples.Testing.csproj b/examples/AwsLambda.Host.Examples.Testing/AwsLambda.Host.Examples.Testing.csproj
deleted file mode 100644
index 89192b1d..00000000
--- a/examples/AwsLambda.Host.Examples.Testing/AwsLambda.Host.Examples.Testing.csproj
+++ /dev/null
@@ -1,56 +0,0 @@
-
-
- Exe
- net10.0
- preview
- disable
- enable
- true
- Lambda
-
- true
-
- true
- $(InterceptorsNamespaces);AwsLambda.Host
- Lambda.Host.Example.HelloWorld
- false
-
-
-
- PreserveNewest
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
-
- PreserveNewest
-
-
-
diff --git a/examples/AwsLambda.Host.Examples.Testing/LambdaHostTest.cs b/examples/AwsLambda.Host.Examples.Testing/LambdaHostTest.cs
deleted file mode 100644
index 492a8ced..00000000
--- a/examples/AwsLambda.Host.Examples.Testing/LambdaHostTest.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using System.Threading.Tasks;
-using AwsLambda.Host.Testing;
-using JetBrains.Annotations;
-using Xunit;
-
-namespace Lambda.Host.Example.HelloWorld;
-
-[TestSubject(typeof(Program))]
-public class LambdaHostTest
-{
- [Fact]
- public async Task LambdaHost_CanStartWithoutError()
- {
- await using var factory = new WebApplicationFactory();
-
- var client = factory.CreateClient();
- await client.WaitForNextRequestAsync();
- var response = await client.InvokeAsync("Jonas");
- Assert.True(response.WasSuccess);
- Assert.NotNull(response);
- Assert.Equal("Hello Jonas!", response.Response);
- }
-}
diff --git a/examples/AwsLambda.Host.Examples.Testing/Program.cs b/examples/AwsLambda.Host.Examples.Testing/Program.cs
deleted file mode 100644
index cc93ac71..00000000
--- a/examples/AwsLambda.Host.Examples.Testing/Program.cs
+++ /dev/null
@@ -1,106 +0,0 @@
-using AwsLambda.Host.Builder;
-using Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-
-// Create the application builder
-var builder = LambdaApplication.CreateBuilder();
-
-builder.Services.ConfigureLambdaHostOptions(options =>
-{
- // options.BootstrapHttpClient = new HttpClient(new LoggingHttpHandler());
- options.ClearLambdaOutputFormatting = true;
- // options.BootstrapOptions.RuntimeApiEndpoint = "localhost:3002";
-});
-
-// Build the Lambda application
-var lambda = builder.Build();
-
-// Map your handler - the event is automatically injected
-lambda.MapHandler(([Event] string name) => $"Hello {name}!");
-
-// Run the Lambda
-await lambda.RunAsync();
-
-public partial class Program;
-
-// public class LoggingHttpHandler : DelegatingHandler
-// {
-// public LoggingHttpHandler(HttpMessageHandler innerHandler) =>
-// InnerHandler = innerHandler ?? throw new ArgumentNullException(nameof(innerHandler));
-//
-// // Convenience constructor for default handler
-// public LoggingHttpHandler()
-// : this(new HttpClientHandler()) { }
-//
-// protected override async Task SendAsync(
-// HttpRequestMessage request,
-// CancellationToken cancellationToken
-// )
-// {
-// // Log the request
-// Console.WriteLine("========== HTTP REQUEST ==========");
-// Console.WriteLine($"{request.Method} {request.RequestUri}");
-// Console.WriteLine($"Version: {request.Version}");
-//
-// Console.WriteLine("\nHeaders:");
-// foreach (var header in request.Headers)
-// Console.WriteLine($" {header.Key}: {string.Join(", ", header.Value)}");
-//
-// if (request.Content != null)
-// {
-// Console.WriteLine("\nContent Headers:");
-// foreach (var header in request.Content.Headers)
-// Console.WriteLine($" {header.Key}: {string.Join(", ", header.Value)}");
-//
-// // Buffer the content so we can read it without consuming it
-// await request.Content.LoadIntoBufferAsync();
-// var requestBody = await request.Content.ReadAsStringAsync();
-//
-// Console.WriteLine("\nBody:");
-// Console.WriteLine(requestBody);
-// }
-//
-// // Send the request through the inner handler (via base.SendAsync)
-// var response = await base.SendAsync(request, cancellationToken);
-//
-// // Log the response
-// Console.WriteLine("\n========== HTTP RESPONSE ==========");
-// Console.WriteLine($"Status: {(int)response.StatusCode} {response.StatusCode}");
-// Console.WriteLine($"Version: {response.Version}");
-//
-// Console.WriteLine("\nHeaders:");
-// foreach (var header in response.Headers)
-// Console.WriteLine($" {header.Key}: {string.Join(", ", header.Value)}");
-//
-// if (response.Content != null)
-// {
-// Console.WriteLine("\nContent Headers:");
-// foreach (var header in response.Content.Headers)
-// Console.WriteLine($" {header.Key}: {string.Join(", ", header.Value)}");
-//
-// // Read and log response body, then restore it
-// var responseBody = await response.Content.ReadAsStringAsync();
-// Console.WriteLine("\nBody:");
-// Console.WriteLine(responseBody);
-//
-// // Restore the content so it can be read again by the caller
-// var originalContentHeaders = response.Content.Headers.ToList();
-// response.Content = new StringContent(
-// responseBody,
-// Encoding.UTF8,
-// response.Content.Headers.ContentType?.MediaType ?? "application/json"
-// );
-//
-// // Restore all content headers
-// foreach (var header in originalContentHeaders)
-// {
-// response.Content.Headers.Remove(header.Key);
-// response.Content.Headers.TryAddWithoutValidation(header.Key, header.Value);
-// }
-// }
-//
-// Console.WriteLine("===================================\n");
-//
-// return response;
-// }
-// }
diff --git a/src/AwsLambda.Host.Testing/DeferredHostBuilder.cs b/src/AwsLambda.Host.Testing/DeferredHostBuilder.cs
index 06e8f127..7fe11895 100644
--- a/src/AwsLambda.Host.Testing/DeferredHostBuilder.cs
+++ b/src/AwsLambda.Host.Testing/DeferredHostBuilder.cs
@@ -16,11 +16,6 @@ namespace AwsLambda.Host.Testing;
// ConfigureHostBuilder
internal sealed class DeferredHostBuilder : IHostBuilder
{
- public IDictionary