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"
}