I am building my app on AWS and yy app uses websocket like this:
Frontend WebSocket client ---> AWS API Gateway Websocket API ----> Backend in EC2 instance
Here is how it works:
With AWS API Gateway, WebSocket API takes action set by my $connect
integration. In my current configuration, I have set the VPC Link integration with HTTP Method Any on the target url. When the Frontend tries to make a websocket connection with the API Gateway, WebSocket API's $connect
method is triggered and the AWS WebSocket API calls my backend HTTP endpoint <BACKEND_URL>/connect
.
Frontend: ReactJS / Javascript Native Websocket: In my component that uses websocket:
useEffect(() => {
const orgId = localData.get('currentOrganizationId');
const username = localData.get('username');
let socket = new WebSocket(process.env.REACT_APP_WEBSOCKET_URL); // this is the AWS WebSocket URL after I have deployed it.
socket.onopen = function(e) {
console.log('socket on onopen');
const info = JSON.stringify({orgId:orgId, username: username, action: "message"});
socket.send(info);
};
socket.onmessage = function(event) {
console.log(`[message] Data received from server: ${event.data}`);
};
socket.onclose = function(event) {
if (event.wasClean) {
console.log(`[close] Connection closed cleanly, code=${event.code} reason=${event.reason}`);
} else {
console.log(`[close] Connection died; code=${event.code}`);
}
};
socket.onerror = function(error) {
console.log(`[error] ${error.message}`);
};
}, [])
AWS WebSocket API configuration:
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');
});
app.put('/default', function(_req, res) {
logger.info(`/default _req.query: ${JSON.stringify(_req.query)}`);
logger.info(`/default _req.params: ${JSON.stringify(_req.params)}`);
logger.info(`/default _req.body: ${JSON.stringify(_req.body)}`);
logger.info(`/default _req.headers: ${JSON.stringify(_req.headers)}`);
res.send('/default hahaha default');
});
Now, this works perfectly. When I load frontend in my browser, in the EC2 instance I can see the Express's log that \connect
is triggered and things get printed when socket.onopen()
is successful in the frontend code:
2022-Jan-17 11:51:29:5129 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 11:51:29:5129 info: /connect _req.query: {}
2022-Jan-17 11:51:29:5129 info: /connect _req.params: {}
2022-Jan-17 11:51:29:5129 info: /connect _req.body: {}
2022-Jan-17 11:51:29:5129 info: /connect _req.headers: {"accept-encoding":"gzip, deflate, br","accept-language":"en-US,en;q=0.9","cache-control":"no-cache","origin":"http://127.0.0.1:3000","pragma":"no-cache","sec-websocket-extensions":"permessage-deflate; client_max_window_bits","sec-websocket-key":"w0HoFw7+RtvLi3KWgT2OBw==","sec-websocket-version":"13","user-agent":"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","x-amzn-trace-id":"Root=1-61e4d9b1-671cf2d36097a75435133215","x-forwarded-for":"219.102.102.145","x-forwarded-port":"443","x-forwarded-proto":"https","x-amzn-apigateway-api-id":"hd5zymklr8","host":"NLB-docloud-internal-ea0692d1e2c8186c.elb.ap-northeast-1.amazonaws.com","connection":"Keep-Alive"}
2022-Jan-17 11:51:29:5129 info: /default _req.query: {}
2022-Jan-17 11:51:29:5129 info: /default _req.params: {}
2022-Jan-17 11:51:29:5129 info: /default _req.body: {"orgId":"1","username":"staff_a","action":"message"}
2022-Jan-17 11:51:29:5129 info: /default _req.headers: {"user-agent":"AmazonAPIGateway_hd5zymklr8","x-amzn-apigateway-api-id":"hd5zymklr8","host":"NLB-docloud-internal-ea0692d1e2c8186c.elb.ap-northeast-1.amazonaws.com","content-length":"53","content-type":"application/json; charset=UTF-8","connection":"Keep-Alive"}
Also, /default
is triggered immediately and a message {"orgId":"1","username":"staff_a","action":"message"}
is received because in the frontend code I am calling:
const info = JSON.stringify({orgId:orgId, username: username, action: "message"});
socket.send(info);
immediately after socket.onopen()
is successful.
So far so good.
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:
https://stackoverflow.com/a/59220644/3703783
https://stackoverflow.com/a/65112135/3703783
which have explained very clearly.
Basically I need to de-select Use Proxy Integration
and configure Request Templates
.
Here is my config:
However, the connection fails. I have tried setting the Template Key to both \$default
and $default
, and they all fail. Note that this is not to be confuses with the $default
Route, next to the $connect
route. We are focusing entirely on the $connect
route now and its Request Template Key value just happens to be $default
to match all requests.
The Express's log in EC2 instance is:
2022-Jan-17 12:04:49:449 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 12:04:49:449 info: /connect _req.query: {}
2022-Jan-17 12:04:49:449 info: /connect _req.params: {}
2022-Jan-17 12:04:49:449 info: /connect _req.body: {}
2022-Jan-17 12:04:49:449 info: /connect _req.headers: {"x-amzn-apigateway-api-id":"hd5zymklr8","x-amzn-trace-id":"Root=1-61e4dccd-254aa4f9581373b00f8ef54d","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 \connect
endpoint is still triggered like before, however the connection fails and since socket.onopen()
is not successful, no message is sent by socket.send(info);
In chrome dev mode, I can see the following error messages:
How come that the connection fails even though \connect
endpoint is still triggered like before?
I also noticed that the _req.headers
is much shorter than before:
{
"x-amzn-apigateway-api-id":"hd5zymklr8",
"x-amzn-trace-id":"Root=1-61e4dccd-254aa4f9581373b00f8ef54d",
"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 _req.headers
when things work well:
{
"accept-encoding":"gzip, deflate, br",
"accept-language":"en-US,en;q=0.9",
"cache-control":"no-cache",
"origin":"http://127.0.0.1:3000",
"pragma":"no-cache",
"sec-websocket-extensions":"permessage-deflate; client_max_window_bits",
"sec-websocket-key":"w0HoFw7+RtvLi3KWgT2OBw==",
"sec-websocket-version":"13",
"user-agent":"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",
"x-amzn-trace-id":"Root=1-61e4d9b1-671cf2d36097a75435133215",
"x-forwarded-for":"219.102.102.145",
"x-forwarded-port":"443",
"x-forwarded-proto":"https",
"x-amzn-apigateway-api-id":"hd5zymklr8",
"host":"NLB-docloud-internal-ea0692d1e2c8186c.elb.ap-northeast-1.amazonaws.com",
"connection":"Keep-Alive"
}
Answering myself just in case this may help someone.
It is actually very very trivial. I have to configure templated transformations on the response received from my backend integration before sending the message through to the client!
I configured the Response Key to be $default
and with no further specifications:
And it works.
It seems that API Gateway's WebSocket API is really geared towards using Lambda as the backend integration.
Using other types of backend integration, you would face many painful problems, and the AWS documents / tutorials are NOT helping at all for types of backend integration other than Lambda.