node.jsreactjsamazon-web-serviceswebsocketaws-api-gateway

AWS API Gateway WebSocket: connection fails when not selecting Proxy Integration and use Request Templates


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: enter image description here

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: enter image description here

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:

enter image description here

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


Solution

  • 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!

    enter image description here

    I configured the Response Key to be $default and with no further specifications: enter image description here

    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.