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())