Adding Websockets to your AWS Serverless application

The quickest way to add Websockets to your AWS Serverless application

functions/websocket.py

Let’s create a handler for connecting, disconnecting, receiving messages and sending messages:

import os
import json
import boto3REQUEST_HANDLED = {"statusCode": 200}def connection_manager(event, context):connection_id = event["requestContext"].get("connectionId")if event["requestContext"]["eventType"] == "CONNECT":
        print("Connect requested")
        # you might want to store the connection_id in a database of some sort
        return REQUEST_HANDLED
    elif event["requestContext"]["eventType"] == "DISCONNECT":
        print("Disconnect requested")
        return REQUEST_HANDLEDdef handle_incoming_ws_message(event, context):
    """
    When a message comes in, just echo it back to the sender
    """body = _get_event_body(event)
    body['type'] = 'echoReply'
    connection_id = event["requestContext"].get("connectionId")
    _send_to_connection(connection_id, body, event)return REQUEST_HANDLEDdef default_message(event, context):
    """
    Send back error when unrecognized WebSocket action is received.
    """
    print("Unrecognized WebSocket action received.")
    connection_id = event["requestContext"].get("connectionId")
    send_ws_message(connection_id, {'type':'invalidRequest', 'error':'Unrecognized WebSocket action received.'})
    return REQUEST_HANDLEDdef send_ws_message(connection_id, body):
    if not isinstance(body, str):
        body = json.dumps(body)
    _send_to_connection(connection_id, body)def _get_event_body(event):
    try:
        return json.loads(event.get("body", ""))
    except ValueError:
        print("event body could not be JSON decoded.")
        return {}def _send_to_connection(connection_id, data):
    endpoint = os.environ['WEBSOCKET_API_ENDPOINT']
    gatewayapi = boto3.client("apigatewaymanagementapi",
                              endpoint_url=endpoint)
    return gatewayapi.post_to_connection(ConnectionId=connection_id,
                                         Data=data.encode('utf-8'))

requirements.txt

# replace by latest as listed on https://pypi.org/project/botocore/
botocore==1.12.133

serverless.yml

service: myapp # replace by your app name
package:
  excludeDevDependencies: falsecustom:
  pythonRequirements:
    noDeploy: [] # do not ignore botocoreprovider:
  name: aws
  [..]
  region: us-east-1 # replace by the region you want to deploy to
  websocketsApiName: ${self:service}-${opt:stage}-websocketApi
  websocketsApiRouteSelectionExpression: $request.body.actioniamRoleStatements:
  - Effect: Allow
    Action:
      - "execute-api:ManageConnections"
    Resource:
      - "arn:aws:execute-api:*:*:**/@connections/*"functions:
  # allows clients to connect and disconnect
  connectionManager:
    handler: functions/websocket.connection_manager
    events:
      - websocket:
        route: $connect
      - websocket:
        route: $disconnect  # Catch-all route for unsupported actions, like HTML 404 
  defaultMessage: 
    handler: functions/websocket.default_message
    events:
      - websocket:
          route: $default  # Handle incoming websocket messages
  incomingMessage:
    handler: functions/websocket.handle_incoming_ws_message
    events:
      - websocket:
          route: sendMessageenvironment:
  WEBSOCKET_API_ENDPOINT: !Join ['', ['https://', !Ref WebsocketsApi, '.execute-api.', "${self:region}", '.amazonaws.com/', "${opt:stage}/"]]plugins:
  - serverless-python-requirements
serverless plugin install -n serverless-python-requirements
serverless deploy --stage mystage
[..]
Serverless: Stack update finished...
Service Information
[..]
endpoints:
GET - https://chjpngb558.execute-api.us-east-1.amazonaws.com/mystage/functions/some_rest_handler/handle
wss://rs1bcx8hn2.execute-api.us-east-1.amazonaws.com/mystage

Testing

Install wscat:

npm install -g wscat
wscat -c <YOUR_WEBSOCKET_ENDPOINT>
connected (press CTRL+C to quit)
> 
> {"action": "sendMessage", "favoriteColor": "green"}< {"action": "sendMessage", "favoriteColor": "green", "type": "echoReply"}> {"action": "haltAndCatchFire"}< {"type":"invalidRequest", "error":"Unrecognized WebSocket action received."}

Implementing a real client

Implementing a javascript client is outside the scope of this article. I would recommend to use a library to handle websocket connections though, or a framework integration. For our VueJS app, we used vue-native-websocket which provided automatic reconnect and VueX integration. For an example of a full project including a client, see this article

Access control

Note that your websocket endpoint is currently public to the world and probably needs some access control. Because you probably already have some kind of authorizer set up for your REST endpoints, you might try to adapt / reuse it for websockets. Alternatively, refer to the Api Gateway Docs for possible solutions.

Troubleshooting

wscat shows a 502 error when connecting

See the cloudwatch logs for the connectionManager function

After sending a message, you get an Internal Server Error Response

The Internal Server Error is generating by the Api Gateway and hides the actual error encountered by your lambda function. See the cloudwatch logs for your lambda function (eg incomingMessage or defaultMessage) and read for solutions to more specific problems

An error response without any sign of trouble in cloudwatch

Probably means you did not return a response from your lambda function. The function completed successfully (hence no error), but the Api gateway got no response and thus throws an error. Check whether your lambda functions end with return REQUEST_HANDLED

Unknown service: ‘apigatewaymanagementapi’

Botocore is probably too old. Make sure you added it to requirements.txt, added the serverless-python-requirements plugin, and configured it with noDeploy: []

Task timed out after 6.01 seconds

A lambda function probably tried to access the WEBSOCKET_API_ENDPOINT but failed because you run your lambda in a VPC which does not have internet access.

functions:
  defaultMessage:
    handler: functions/websocket.default_message
    events:
      - websocket:
          route: $default
    vpc:
      subnetIds:
        - !Ref NatSubnet
  # Handle incoming websocket messages
  incomingMessage:
    handler: functions/websocket.handle_incoming_ws_message
    events:
      - websocket:
          route: sendMessage
    vpc:
      subnetIds:
        - !Ref NatSubnet
resources:
  Resources:
    # Use the NatSubnet for Lambda functions which require outbound internet access
    # See https://aws.amazon.com/premiumsupport/knowledge-center/internet-access-lambda-function/
    NatSubnet:
      Type: AWS::EC2::Subnet
      Properties:
        VpcId: !Ref VPC
        CidrBlock: 10.0.3.0/24
        Tags:
          - Key: Name
            Value: '${self:service}-${self:provider.stage}-nat-subnet'    NatGatewayEIP:
      Type: AWS::EC2::EIP    NatGateway:
      Type: AWS::EC2::NatGateway
      Properties:
        AllocationId: !GetAtt NatGatewayEIP.AllocationId
        SubnetId: !Ref Subnet1
        Tags:
          - Key: Name
            Value: '${self:service}-${self:provider.stage}-nat-gateway'    NatRouteTable:
      Type: AWS::EC2::RouteTable
      Properties:
        VpcId: !Ref VPC
        Tags:
          - Key: Name
            Value: '${self:service}-${self:provider.stage}-nat-rtb'    NatInternetRoute:
      Type: AWS::EC2::Route
      DependsOn: NatGateway
      Properties:
        DestinationCidrBlock: 0.0.0.0/0
        RouteTableId: !Ref NatRouteTable
        NatGatewayId: !Ref NatGateway    NatSubnetRouteTableAssociation:
      Type: AWS::EC2::SubnetRouteTableAssociation
      Properties:
        SubnetId: !Ref NatSubnet
        RouteTableId: !Ref NatRouteTable

Artificial Industry

Co-pilot on your digital journey

Nino van Hooff

Written by

Artificial Industry

Co-pilot on your digital journey