AWS Websocket 403 Using PostToConnect

Using the V2 Golang SDK for AWS and trying to call PostToConnection from a Lambda I was consistently getting this 403 error result:

operation error ApiGatewayManagementApi: PostToConnection, https response error StatusCode: 403, RequestID: 965ae9ea-c537-43d0-ae5c-3a458ccbc069, ForbiddenException

I was trying to make the call from a different stack than the websocket connection was on so I thought that might be the problem. After much research the issue actually turned out to be simpler than that. There is nothing special about the stack that requires extra permission (unless the stages are in different regions, I haven’t done that yet but it looks like there is more work required if you are). The issue for me is that I had the endpoint wrong for the PostToConnection call. Calling the AWS websocket API requires that you know the right endpoint for the socket and that the AWS client uses that endpoint. There are two parts to the endpoint, the domain and the stage. The client needs to be configured properly so that it resolves to that endpoint. Here’s an example of how to do that. Note that this client only resolves properly for the API Gateway websocket endpoint so it shouldn’t be used as a general purpose AWS client.

// newAPIGatewayManagementClient creates a new API Gateway Management Client instance from the provided parameters. The
// new client will have a custom endpoint that resolves to the application's deployed API.
func newAPIGatewayManagementClient(cfg *aws.Config, endpointHost, endpointStage string) *apigatewaymanagementapi.Client {
  cp := cfg.Copy()
  cp.EndpointResolver = aws.EndpointResolverFunc(func(service, region string) (aws.Endpoint, error) {
    var endpoint url.URL
    endpoint.Path = endpointStage
    endpoint.Host = endpointHost 
    endpoint.Scheme = "https"
    return aws.Endpoint{
      SigningRegion: region,
      URL:           endpoint.String(),
    }, nil

  return apigatewaymanagementapi.NewFromConfig(cp)

The two interesting arguments to this function are endpointHost and endpointStage. These values should be captured when the websocket connection handler is first called to open a new socket. Here’s example code that captures these values. This function is attached to the websocket connection handler.

// invoked from API Gateway when a WebSocket connection is opened
func handler(ctx context.Context, request events.APIGatewayWebsocketProxyRequest) (events.APIGatewayProxyResponse, error) {

  err := initHost(ctx, request.RequestContext.DomainName, request.RequestContext.Stage)
  if nil != err {
    return handleError(ctx, err), nil
  err = recordConnection(ctx, request.RequestContext.ConnectionID)
  if nil != err {
    return handleError(ctx, err), nil
  return successResponse(ctx), nil

func initHost( ctx context.Context, endpointHost, endpointStage string ) error {
  // store the endpointHost and endpointStage for later use

func recordConnection( ctx context.Context, connectionID string ) error {
  // store the connectionID for later use

There are two interesting functions in this code initHost and recordConnection. I split them into different functions because the scope of the information is different. The endpoint information is the same across all websocket calls while the connectionID is unique for each websocket. I only store the endpoint host and stage the first time a connection is opened. The connectionID has to be stored for each connection and connected to the user account that opened the connection so that functions that want to message that user can look up their current connection. The details of how you want to store that information will vary depending on your app implementation. I store the information in DynamoDB, the endpoint host and stage in a single entry with a static key to make it easy to find from anywhere, while each connectionID is associated with a particular user identity in the DB.

The 403 error was caused because I was using the stage of the stack that was sending the message instead of the stage of the stack where the websocket connection was created. In particular the websocket handler was in a lambda called from a stage named dev while the message was originating from a lambda in a different stack with a stage named $default. When I switched the endpoint resolver to use the endpointHost and endpointStage that were stored when the connection was created the 403 went away.

As far as I can tell the only IAM permission required is that the connection handler have a Lambda websocket authorizer and that the policy returned by that authorizer provides permission to any stacks that are required. There are other ways to provide permissions including using IAM role based permissions but I’m currently using the Lambda authorizer.

Leave a Reply