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:
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}"
}
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?
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.
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.