diff --git a/.autover/changes/8c1d9773-a398-4953-97d4-9747e9606611.json b/.autover/changes/8c1d9773-a398-4953-97d4-9747e9606611.json
new file mode 100644
index 000000000..3d2b74e7b
--- /dev/null
+++ b/.autover/changes/8c1d9773-a398-4953-97d4-9747e9606611.json
@@ -0,0 +1,18 @@
+{
+ "Projects": [
+ {
+ "Name": "Amazon.Lambda.AspNetCoreServer",
+ "Type": "Patch",
+ "ChangelogMessages": [
+ "Null-safe marshalling for Path, HttpMethod, path parameters, and headers to avoid NullReferenceException with incomplete API Gateway events"
+ ]
+ },
+ {
+ "Name": "Amazon.Lambda.TestTool",
+ "Type": "Patch",
+ "ChangelogMessages": [
+ "Populate RequestContext (and Path/HttpMethod defaults) when the API Gateway emulator builds REST APIGatewayProxyRequest events"
+ ]
+ }
+ ]
+}
diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs
index 4e3e7a35d..9819d5bb3 100644
--- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs
+++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/APIGatewayProxyFunction.cs
@@ -157,6 +157,18 @@ protected override Amazon.Lambda.Core.ResponseStreaming.HttpResponseStreamPrelud
///
protected override void MarshallRequest(InvokeFeatures features, APIGatewayProxyRequest apiGatewayRequest, ILambdaContext lambdaContext)
{
+ if (apiGatewayRequest == null)
+ {
+ _logger.LogError("MarshallRequest: apiGatewayRequest is null");
+ throw new ArgumentNullException(nameof(apiGatewayRequest));
+ }
+
+ _logger.LogDebug("MarshallRequest: Path={Path}, HttpMethod={HttpMethod}, Resource={Resource}, RequestContext={HasContext}",
+ apiGatewayRequest.Path ?? "(null)",
+ apiGatewayRequest.HttpMethod ?? "(null)",
+ apiGatewayRequest.Resource ?? "(null)",
+ apiGatewayRequest.RequestContext != null);
+
{
var authFeatures = (IHttpAuthenticationFeature)features;
@@ -201,7 +213,8 @@ protected override void MarshallRequest(InvokeFeatures features, APIGatewayProxy
var rawQueryString = Utilities.CreateQueryStringParameters(
apiGatewayRequest.QueryStringParameters, apiGatewayRequest.MultiValueQueryStringParameters, true);
- requestFeatures.RawTarget = apiGatewayRequest.Path + rawQueryString;
+ var pathForTarget = apiGatewayRequest.Path ?? "/";
+ requestFeatures.RawTarget = pathForTarget + rawQueryString;
requestFeatures.QueryString = rawQueryString;
requestFeatures.Path = path;
@@ -365,24 +378,24 @@ protected virtual string ParseHttpPath(APIGatewayProxyRequest apiGatewayRequest)
if (apiGatewayRequest.PathParameters != null && apiGatewayRequest.PathParameters.TryGetValue("proxy", out var proxy) &&
!string.IsNullOrEmpty(apiGatewayRequest.Resource))
{
- var proxyPath = proxy;
+ var proxyPath = proxy ?? "";
path = apiGatewayRequest.Resource.Replace("{proxy+}", proxyPath);
// Adds all the rest of non greedy parameters in apiGateway.Resource to the path
foreach (var pathParameter in apiGatewayRequest.PathParameters.Where(pp => pp.Key != "proxy"))
{
- path = path.Replace($"{{{pathParameter.Key}}}", pathParameter.Value);
+ path = path.Replace($"{{{pathParameter.Key}}}", pathParameter.Value ?? "");
}
}
if (string.IsNullOrEmpty(path))
{
- path = apiGatewayRequest.Path;
+ path = apiGatewayRequest.Path ?? "/";
}
- if (!path.StartsWith("/"))
+ if (string.IsNullOrEmpty(path) || !path.StartsWith("/"))
{
- path = "/" + path;
+ path = "/" + (path ?? "");
}
return path;
}
@@ -394,7 +407,7 @@ protected virtual string ParseHttpPath(APIGatewayProxyRequest apiGatewayRequest)
///
protected virtual string ParseHttpMethod(APIGatewayProxyRequest apiGatewayRequest)
{
- return apiGatewayRequest.HttpMethod;
+ return apiGatewayRequest.HttpMethod ?? "GET";
}
///
diff --git a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/Utilities.cs b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/Utilities.cs
index 6e96840c8..5aee012c7 100644
--- a/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/Utilities.cs
+++ b/Libraries/src/Amazon.Lambda.AspNetCoreServer/Internal/Utilities.cs
@@ -190,14 +190,21 @@ internal static void SetHeadersCollection(IHeaderDictionary headers, IDictionary
{
foreach (var kvp in multiValues)
{
- headers[kvp.Key] = new StringValues(kvp.Value.ToArray());
+ var values = kvp.Value?.Where(v => v != null).ToArray() ?? Array.Empty();
+ if (values.Length > 0)
+ {
+ headers[kvp.Key] = new StringValues(values);
+ }
}
}
else if (singleValues?.Count > 0)
{
foreach (var kvp in singleValues)
{
- headers[kvp.Key] = new StringValues(kvp.Value);
+ if (kvp.Value != null)
+ {
+ headers[kvp.Key] = new StringValues(kvp.Value);
+ }
}
}
}
diff --git a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/HttpContextExtensions.cs b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/HttpContextExtensions.cs
index f7e95614f..5387ac0d5 100644
--- a/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/HttpContextExtensions.cs
+++ b/Tools/LambdaTestTool-v2/src/Amazon.Lambda.TestTool/Extensions/HttpContextExtensions.cs
@@ -177,13 +177,34 @@ public static async Task ToApiGatewayRequest(
pathParameters = encodedPathParameters;
}
+ var pathValue = path ?? "/";
+ var resourceValue = apiGatewayRouteConfig.Path ?? "";
+ var httpMethodValue = request.Method ?? "GET";
+ var userAgent = request.Headers.UserAgent.ToString();
+
var proxyRequest = new APIGatewayProxyRequest
{
- Resource = apiGatewayRouteConfig.Path,
- Path = path,
- HttpMethod = request.Method,
+ Resource = resourceValue,
+ Path = pathValue,
+ HttpMethod = httpMethodValue,
Body = body,
- IsBase64Encoded = false
+ IsBase64Encoded = false,
+ RequestContext = new APIGatewayProxyRequest.ProxyRequestContext
+ {
+ Path = pathValue,
+ AccountId = "123456789012",
+ ResourceId = "test-invoke-resource-id",
+ Stage = "test-invoke-stage",
+ RequestId = HttpRequestUtility.GenerateRequestId(),
+ Identity = new APIGatewayProxyRequest.RequestIdentity
+ {
+ SourceIp = "127.0.0.1",
+ UserAgent = userAgent
+ },
+ ResourcePath = resourceValue,
+ HttpMethod = httpMethodValue,
+ ApiId = "test-invoke-api-id"
+ }
};
if (headers.Any())