diff --git a/.autover/changes/f89246dc-be19-4bd4-aa8a-4f3354419b97.json b/.autover/changes/f89246dc-be19-4bd4-aa8a-4f3354419b97.json
new file mode 100644
index 000000000..b0e3851f5
--- /dev/null
+++ b/.autover/changes/f89246dc-be19-4bd4-aa8a-4f3354419b97.json
@@ -0,0 +1,11 @@
+{
+ "Projects": [
+ {
+ "Name": "Amazon.Lambda.TestTool",
+ "Type": "Minor",
+ "ChangelogMessages": [
+ "API Gateway emulator: optional IntegrationType=Http to reverse-proxy requests to a non-Lambda HTTP Endpoint while keeping the emulator origin"
+ ]
+ }
+ ]
+}
diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/ApiGatewayRouteConfig.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/ApiGatewayRouteConfig.cs
index eb6a54070..f42508ee7 100644
--- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/ApiGatewayRouteConfig.cs
+++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Models/ApiGatewayRouteConfig.cs
@@ -27,4 +27,9 @@ public class ApiGatewayRouteConfig
/// The API Gateway HTTP Path of the Lambda function
///
public required string Path { get; set; }
+
+ ///
+ /// The integration type: "Lambda" (default) or "Http". When "Http", the request is proxied to the Endpoint URL instead of invoking a Lambda.
+ ///
+ public string? IntegrationType { get; set; }
}
diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/ApiGatewayEmulatorProcess.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/ApiGatewayEmulatorProcess.cs
index b09c74e12..0f849b8ef 100644
--- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/ApiGatewayEmulatorProcess.cs
+++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Processes/ApiGatewayEmulatorProcess.cs
@@ -53,6 +53,7 @@ public static ApiGatewayEmulatorProcess Startup(RunCommandSettings settings, Can
builder.Services.AddApiGatewayEmulatorServices();
builder.Services.AddSingleton();
+ builder.Services.AddHttpClient();
string? serviceHttpUrl = null;
string? serviceHttpsUrl = null;
@@ -83,7 +84,7 @@ public static ApiGatewayEmulatorProcess Startup(RunCommandSettings settings, Can
app.Logger.LogInformation("The API Gateway Emulator is available at: {ServiceUrl}", serviceHttpsUrl ?? serviceHttpUrl);
});
- app.Map("/{**catchAll}", async (HttpContext context, IApiGatewayRouteConfigService routeConfigService, ILambdaClient lambdaClient) =>
+ app.Map("/{**catchAll}", async (HttpContext context, IApiGatewayRouteConfigService routeConfigService, ILambdaClient lambdaClient, IHttpClientFactory httpClientFactory) =>
{
var routeConfig = routeConfigService.GetRouteConfig(context.Request.Method, context.Request.Path);
if (routeConfig == null)
@@ -94,7 +95,35 @@ public static ApiGatewayEmulatorProcess Startup(RunCommandSettings settings, Can
return;
}
- // Convert ASP.NET Core request to API Gateway event object
+ // HTTP integration: proxy request to the backend URL
+ if (string.Equals(routeConfig.IntegrationType, "Http", StringComparison.OrdinalIgnoreCase))
+ {
+ var endpoint = routeConfig.Endpoint ?? throw new InvalidOperationException($"HTTP route {routeConfig.LambdaResourceName} requires Endpoint.");
+ var targetUrl = $"{endpoint.TrimEnd('/')}{context.Request.Path}{context.Request.QueryString}";
+ var httpClient = httpClientFactory.CreateClient();
+ using var request = new HttpRequestMessage(new HttpMethod(context.Request.Method), targetUrl);
+ if (context.Request.ContentLength > 0 && (context.Request.Method == "POST" || context.Request.Method == "PUT" || context.Request.Method == "PATCH"))
+ {
+ request.Content = new StreamContent(context.Request.Body);
+ if (context.Request.ContentType != null)
+ request.Content.Headers.ContentType = System.Net.Http.Headers.MediaTypeHeaderValue.Parse(context.Request.ContentType);
+ }
+ foreach (var header in context.Request.Headers.Where(h => !string.Equals(h.Key, "Host", StringComparison.OrdinalIgnoreCase)))
+ {
+ if (!request.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray()))
+ request.Content?.Headers.TryAddWithoutValidation(header.Key, header.Value.ToArray());
+ }
+ var response = await httpClient.SendAsync(request, context.RequestAborted);
+ context.Response.StatusCode = (int)response.StatusCode;
+ foreach (var header in response.Headers)
+ context.Response.Headers[header.Key] = string.Join(", ", header.Value);
+ if (response.Content.Headers.ContentType != null)
+ context.Response.ContentType = response.Content.Headers.ContentType.ToString();
+ await response.Content.CopyToAsync(context.Response.Body, context.RequestAborted);
+ return;
+ }
+
+ // Convert ASP.NET Core request to API Gateway event object (Lambda integration)
var lambdaRequestStream = new MemoryStream();
if (settings.ApiGatewayEmulatorMode.Equals(ApiGatewayEmulatorMode.HttpV2))
{
diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/ApiGatewayRouteConfigService.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/ApiGatewayRouteConfigService.cs
index e8676be36..0416ee17d 100644
--- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/ApiGatewayRouteConfigService.cs
+++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Services/ApiGatewayRouteConfigService.cs
@@ -143,6 +143,22 @@ private bool IsRouteConfigValid(ApiGatewayRouteConfig routeConfig)
return false;
}
+ if (string.Equals(routeConfig.IntegrationType, "Http", StringComparison.OrdinalIgnoreCase))
+ {
+ if (string.IsNullOrWhiteSpace(routeConfig.Endpoint))
+ {
+ _logger.LogError("HTTP integration requires a non-empty Endpoint for route {Lambda} {Method} {Path}.",
+ routeConfig.LambdaResourceName, routeConfig.HttpMethod, routeConfig.Path);
+ return false;
+ }
+ if (!Uri.TryCreate(routeConfig.Endpoint, UriKind.Absolute, out var uri) || !uri.Scheme.StartsWith("http", StringComparison.OrdinalIgnoreCase))
+ {
+ _logger.LogError("HTTP integration Endpoint must be a valid HTTP(s) URL for route {Lambda} {Method} {Path}.",
+ routeConfig.LambdaResourceName, routeConfig.HttpMethod, routeConfig.Path);
+ return false;
+ }
+ }
+
// Special case for root path
if (routeConfig.Path == "/") return true;
diff --git a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Services/ApiGatewayRouteConfigServiceTests.cs b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Services/ApiGatewayRouteConfigServiceTests.cs
index a0cdb8cbb..aa73770f5 100644
--- a/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Services/ApiGatewayRouteConfigServiceTests.cs
+++ b/Tools/LambdaTestTool-v2/tests/Amazon.Lambda.TestTool.UnitTests/Services/ApiGatewayRouteConfigServiceTests.cs
@@ -471,4 +471,78 @@ public void ProperlyMatchRouteConfigs()
Assert.Equal("F1", result20?.LambdaResourceName);
Assert.Equal("F1", result21?.LambdaResourceName);
}
+
+ [Fact]
+ public void Constructor_LoadsHttpIntegrationWhenEndpointIsValid()
+ {
+ var routeConfig = new ApiGatewayRouteConfig
+ {
+ LambdaResourceName = "HttpBackend",
+ HttpMethod = "GET",
+ Path = "/proxy/{proxy+}",
+ IntegrationType = "Http",
+ Endpoint = "http://127.0.0.1:5000"
+ };
+
+ _mockEnvironmentManager
+ .Setup(m => m.GetEnvironmentVariables())
+ .Returns(new Dictionary
+ {
+ { Constants.LambdaConfigEnvironmentVariablePrefix, JsonSerializer.Serialize(routeConfig) }
+ });
+
+ var service = new ApiGatewayRouteConfigService(_mockEnvironmentManager.Object, _mockLogger.Object);
+
+ var result = service.GetRouteConfig("GET", "/proxy/hello");
+ Assert.NotNull(result);
+ Assert.Equal("Http", result.IntegrationType);
+ Assert.Equal("http://127.0.0.1:5000", result.Endpoint);
+ }
+
+ [Fact]
+ public void Constructor_IgnoresHttpIntegrationWithoutEndpoint()
+ {
+ var routeConfig = new ApiGatewayRouteConfig
+ {
+ LambdaResourceName = "HttpBackend",
+ HttpMethod = "GET",
+ Path = "/proxy/{proxy+}",
+ IntegrationType = "Http"
+ };
+
+ _mockEnvironmentManager
+ .Setup(m => m.GetEnvironmentVariables())
+ .Returns(new Dictionary
+ {
+ { Constants.LambdaConfigEnvironmentVariablePrefix, JsonSerializer.Serialize(routeConfig) }
+ });
+
+ var service = new ApiGatewayRouteConfigService(_mockEnvironmentManager.Object, _mockLogger.Object);
+
+ Assert.Null(service.GetRouteConfig("GET", "/proxy/hello"));
+ }
+
+ [Fact]
+ public void Constructor_IgnoresHttpIntegrationWithInvalidEndpoint()
+ {
+ var routeConfig = new ApiGatewayRouteConfig
+ {
+ LambdaResourceName = "HttpBackend",
+ HttpMethod = "GET",
+ Path = "/proxy/{proxy+}",
+ IntegrationType = "Http",
+ Endpoint = "not-a-url"
+ };
+
+ _mockEnvironmentManager
+ .Setup(m => m.GetEnvironmentVariables())
+ .Returns(new Dictionary
+ {
+ { Constants.LambdaConfigEnvironmentVariablePrefix, JsonSerializer.Serialize(routeConfig) }
+ });
+
+ var service = new ApiGatewayRouteConfigService(_mockEnvironmentManager.Object, _mockLogger.Object);
+
+ Assert.Null(service.GetRouteConfig("GET", "/proxy/hello"));
+ }
}