Search code examples
node.jsamazon-web-serviceswebsocketaws-api-gateway

AWS API Gatewat WebSocket: Request Templates body of request after transformations is missing in integration backend


I am building my app on AWS and my app uses websocket like this:

Frontend WebSocket client ---> AWS API Gateway Websocket API ----> Backend in EC2 instance.

Now, in order to let my backend Express code know how to send message to a particular client, I have let it know the connectionId of a websocket client / a user. I am following these two answers:

which have explained very clearly.

Below is my configuration in AWS API Gateway WebSocket API:

enter image description here

What I did was using Request Template, matching all incoming requests, to transform the content which will be sent to my integration endpoint as the body of the request. Eventually I want to keep the original request, while adding attributes (like Connection Id) on top of it. For testing purpose, I set the following template:

{
    "myConnectionId": "$context.connectionId",
    "body": "$context"
}

This worked and I can check in AWS CloudWatch that the request body after transform is

{
   "myConnectionId":"MFEdof95tjMCK0w=",
   "body":"{routeKey=$connect, disconnectStatusCode=null, messageId=null, eventType=CONNECT, extendedRequestId=MFEdoFJPtjMF64Q=, requestTime=17/Jan/2022:07:29:01 +0000, messageDirection=IN, disconnectReason=null, stage=production, connectedAt=1642404541417, requestTimeEpoch=1642404541418, identity={cognitoIdentityPoolId=null, cognitoIdentityId=null, principalOrgId=null, cognitoAuthenticationType=null, userArn=null, userAgent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36, accountId=null, caller=null, sourceIp=219.102.102.145, accessKey=null, cognitoAuthenticationProvider=null, user=null}, requestId=MFEdoFJPtjMF64Q=, domainName=123.execute-api.ap-northeast-1.amazonaws.com, connectionId=MFEdof95tjMCK0w=, apiId=hd5zymklr8}"
}

enter image description here

However, in my Express backend running in EC2 instance, even though the endpoint is triggered, the request body is empty and there is no place to find myConnectionId in it.

Backend: NodeJS / ExpressJS, and in index.ts:

  app.get('/connect', function(_req, res) {
    logger.info(`/connect _req: ${Object.keys(_req)}`);
    logger.info(`/connect _req.query: ${JSON.stringify(_req.query)}`);
    logger.info(`/connect _req.params: ${JSON.stringify(_req.params)}`);
    logger.info(`/connect _req.body: ${JSON.stringify(_req.body)}`);
    logger.info(`/connect _req.headers: ${JSON.stringify(_req.headers)}`);
    res.send('/connect hahaha success');
  });

Log output:

2022-Jan-17 05:05:00:50  info: /connect _req: _readableState,_events,_eventsCount,_maxListeners,socket,httpVersionMajor,httpVersionMinor,httpVersion,complete,rawHeaders,rawTrailers,aborted,upgrade,url,method,statusCode,statusMessage,client,_consuming,_dumped,next,baseUrl,originalUrl,_parsedUrl,params,query,res,_startAt,_startTime,_remoteAddress,body,_parsedOriginalUrl,route
2022-Jan-17 05:05:00:50  info: /connect _req.query: {}
2022-Jan-17 05:05:00:50  info: /connect _req.params: {}
2022-Jan-17 05:05:00:50  info: /connect _req.body: {}
2022-Jan-17 05:05:00:50  info: /connect _req.headers: {"x-amzn-apigateway-api-id":"hd5zymklr8","x-amzn-trace-id":"Root=1-61e5232c-626a05ff264650183a73c98a","user-agent":"AmazonAPIGateway_hd5zymklr8","content-type":"application/json","accept":"application/json","host":"NLB-docloud-internal-ea0692d1e2c8186c.elb.ap-northeast-1.amazonaws.com","connection":"Keep-Alive"}

The request is indeed sent to the endpoint, but why is the request body empty?

How is the content lost?


Solution

  • I have done some research on this.

    As it has been discussed in this post: AWS API Gateway Websockets -- where is the connectionID?, the accepted answers (the solution I was trying in my question above) are assuming your are using Lambda as the backend integration.

    However, I am using Express JS running in EC2 instance and VPC Link.

    In my case, the issue actually is solved by one of the upvoted answers (but not the accepted one): https://stackoverflow.com/a/65639742/3703783.

    The official documentation is here:Setting up data mapping for WebSocket APIs.

    Update

    The above method will add the connectionId to the header of the integration request, but only having the connectionId is not enough - we also need the info like username so that the backend is able to know which connectionId to use when sending message to a particular user. This will do:

    aws apigatewayv2 update-integration 
    --integration-id xxx 
    --api-id xxx 
    --request-parameters 'integration.request.header.userinfo'='route.request.body' 
    
    

    Also, I also found that in my original question, instead of doing

    {
        "myConnectionId": "$context.connectionId",
        "body": "$context"
    }
    
    

    I should simply do:

    {
      "integration.request.header.connectionId":"$context.connectionId",
    }
    

    and this will add the connectionId to the request header.