aws / aws-lambda-java-libs

Official mirror for interface definitions and helper classes for Java code running on the AWS Lambda platform.
https://aws.amazon.com/lambda/
Apache License 2.0
521 stars 231 forks source link

APIGatewayV2HTTPEvent does not properly deserialize event #477

Open kgregory-chariot opened 7 months ago

kgregory-chariot commented 7 months ago

When processing an invocation from an HTTP API Gateway, the APIGatewayV2HTTPEvent does not appear to deserialize nested objects. These objects are successfully deserialized when using Map<String,Object>.

Note: this is different from https://github.com/aws/aws-lambda-java-libs/issues/432, although may have the same root cause.

Buildable project attached

aws-lambda-java-core version: 1.2.3 aws-lambda-java-events version: 3.11.5 Java target version: 11 Java runtime version: 21

Runtime ARN: arn:aws:lambda:us-east-1::runtime:02ff9a81932ab0e699171762afcb5aa2f8c2524ac6e34498612b55defb9c2e7f

HTTP gateway configuration (extract from CloudFormation template):

  APIGateway:
    Type:                               "AWS::ApiGatewayV2::Api"
    Properties: 
      Name:                             !Sub "${AWS::StackName}"
      Description:                      "Invokes the bucket-listing Lambda"
      ProtocolType:                     "HTTP"

  APIGatewayGetRoute:
    Type:                               "AWS::ApiGatewayV2::Route"
    Properties: 
      ApiId:                            !Ref APIGateway
      RouteKey:                         "GET /{proxy+}"
      Target:                           !Sub "integrations/${APIGatewayLambdaIntegration}"

  APIGatewayPutRoute:
    Type:                               "AWS::ApiGatewayV2::Route"
    Properties: 
      ApiId:                            !Ref APIGateway
      RouteKey:                         "PUT /{proxy+}"
      Target:                           !Sub "integrations/${APIGatewayLambdaIntegration}"

  APIGatewayPostRoute:
    Type:                               "AWS::ApiGatewayV2::Route"
    Properties: 
      ApiId:                            !Ref APIGateway
      RouteKey:                         "POST /"
      Target:                           !Sub "integrations/${APIGatewayLambdaIntegration}"

  APIGatewayLambdaIntegration:
    Type:                               "AWS::ApiGatewayV2::Integration"
    Properties: 
      ApiId:                            !Ref APIGateway
      Description:                      "Handles all requests for API operations"
      IntegrationMethod:                "POST"
      IntegrationType:                  "AWS_PROXY"
      IntegrationUri:                   !Sub "arn:${AWS::Partition}:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaFunction.Arn}/invocations"
      PayloadFormatVersion:             "1.0"

Invocation command:

curl -XPUT -d '{"foo": 123, "bar":456, "baz": [9, 8]}' 'https://redacted.execute-api.us-east-1.amazonaws.com/something'

Version 1: uses APIGatewayV2HTTPEvent

public class HttpGWLambda1
implements RequestHandler<APIGatewayV2HTTPEvent,APIGatewayV2HTTPResponse>
{
    @Override
    public APIGatewayV2HTTPResponse handleRequest(APIGatewayV2HTTPEvent event, Context context)
    {
        System.out.println(event);
        System.out.println(event.getRequestContext().getHttp());

        return APIGatewayV2HTTPResponse.builder()
               .withStatusCode(200)
               .withBody("Hello, world")
               .build();
    }
}

Output from first println(), with account number and GW endpoint redacted, but no other formatting. Note the fields that show null values:

APIGatewayV2HTTPEvent(version=1.0, routeKey=null, rawPath=null, rawQueryString=null, cookies=null, headers={Content-Length=38, Content-Type=application/x-www-form-urlencoded, Host=redacted.execute-api.us-east-1.amazonaws.com, User-Agent=curl/7.68.0, X-Amzn-Trace-Id=Root=1-662273ce-2b40f35d65612f3032a6ea11, X-Forwarded-For=173.49.152.157, X-Forwarded-Port=443, X-Forwarded-Proto=https, accept=*/*}, queryStringParameters=null, pathParameters={proxy=something}, stageVariables=null, body=eyJmb28iOiAxMjMsICJiYXIiOjQ1NiwgImJheiI6IFs5LCA4XX0=, isBase64Encoded=true, requestContext=APIGatewayV2HTTPEvent.RequestContext(routeKey=null, accountId=123456789012, stage=$default, apiId=redacted, domainName=redacted.execute-api.us-east-1.amazonaws.com, domainPrefix=redacted, time=null, timeEpoch=0, http=null, authorizer=null, requestId=WecIThGXIAMEbKQ=))

Output from second println() (attempting to retrieve the HTTP invocation information) is null.

Version 2: uses Map<String,Object>

public class HttpGWLambda2
implements RequestHandler<Map<String,Object>,APIGatewayV2HTTPResponse>
{
    @Override
    public APIGatewayV2HTTPResponse handleRequest(Map<String,Object> event, Context context)
    {
        System.out.println(event);

        return APIGatewayV2HTTPResponse.builder()
               .withStatusCode(200)
               .withBody("Hello, world")
               .build();
    }
}

Output from this version (again, with identifying information redacted, but otherwise unchanged). Note that child objects are populated:

{version=1.0, resource=/{proxy+}, path=/something, httpMethod=PUT, headers={Content-Length=38, Content-Type=application/x-www-form-urlencoded, Host=redacted.execute-api.us-east-1.amazonaws.com, User-Agent=curl/7.68.0, X-Amzn-Trace-Id=Root=1-66227575-38824f105e19d3c407288d96, X-Forwarded-For=redacted, X-Forwarded-Port=443, X-Forwarded-Proto=https, accept=*/*}, multiValueHeaders={Content-Length=[38], Content-Type=[application/x-www-form-urlencoded], Host=[redacted.execute-api.us-east-1.amazonaws.com], User-Agent=[curl/7.68.0], X-Amzn-Trace-Id=[Root=1-66227575-38824f105e19d3c407288d96], X-Forwarded-For=[redacted], X-Forwarded-Port=[443], X-Forwarded-Proto=[https], accept=[*/*]}, queryStringParameters=null, multiValueQueryStringParameters=null, requestContext={accountId=123456789012, apiId=redacted, domainName=redacted.execute-api.us-east-1.amazonaws.com, domainPrefix=redacted, extendedRequestId=WedKcjkCIAMEb1Q=, httpMethod=PUT, identity={accessKey=null, accountId=null, caller=null, cognitoAmr=null, cognitoAuthenticationProvider=null, cognitoAuthenticationType=null, cognitoIdentityId=null, cognitoIdentityPoolId=null, principalOrgId=null, sourceIp=redacted, user=null, userAgent=curl/7.68.0, userArn=null}, path=/something, protocol=HTTP/1.1, requestId=WedKcjkCIAMEb1Q=, requestTime=19/Apr/2024:13:45:25 +0000, requestTimeEpoch=1713534325773, resourceId=PUT /{proxy+}, resourcePath=/{proxy+}, stage=$default}, pathParameters={proxy=something}, stageVariables=null, body=eyJmb28iOiAxMjMsICJiYXIiOjQ1NiwgImJheiI6IFs5LCA4XX0=, isBase64Encoded=true}
kgregory-chariot commented 7 months ago

Taking a look at the raw JSON data (via reading as InputStream), I wonder if this class is intended to handle the request from my integration: it's missing several top-level attributes, and has different names for others. However, I see that APIGatewayV2ProxyRequestEvent, which I would have thought a better choice, is deprecated and was intended to support WebSocket requests.