aws-sdk-js-v3: ApiGatewayManagementApi - Stage name not included in request path - ForbiddenException

Describe the bug When making request to the ApiGatewayManagementApi the stage name is not being included in the request path resulting in a 403 response.

This is because the path is currently just of the form:

/@connections/{ConnectionId}

https://github.com/aws/aws-sdk-js-v3/blob/64d22105691f286ad9accf1a137d7c1928378ad4/clients/client-apigatewaymanagementapi/protocols/Aws_restJson1.ts#L86

However, as per the documentation the path should be of the format:

/{Stage}/@connections/{ConnectionId}

https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-how-to-call-websocket-api-connections.html

Resulting in the following error:

(node:44238) UnhandledPromiseRejectionWarning: ForbiddenException: ForbiddenException
    at deserializeAws_restJson1PostToConnectionCommandError (<project_root>/node_modules/@aws-sdk/client-apigatewaymanagementapi/dist/cjs/protocols/Aws_restJson1.js:287:41)
    ...

SDK version number 3.0.0

Is the issue in the browser/Node.js/ReactNative? I’m testing in node.js

Details of the browser/Node.js/ReactNative version

$ node -v
v12.16.1

To Reproduce (observed behavior)

const {
  PostToConnectionCommand,
  ApiGatewayManagementApiClient,
} = require('@aws-sdk/client-apigatewaymanagementapi');

const API_ID = '';
const REGION = '';
const STAGE = '';
const CONNECTION_ID = '';

const apiGatewayManagementApiClient = new ApiGatewayManagementApiClient({
  endpoint: `https://${API_ID}.execute-api.${REGION}.amazonaws.com/${STAGE}`,
});

(async () => {
  const command = new PostToConnectionCommand({
    ConnectionId: CONNECTION_ID,
    Data: new TextEncoder().encode('test-message'),
  })

  const res = await apiGatewayManagementApiClient.send(command);
  console.log(res);
})();

Expected behavior Altering the request path with a middleware fixes this as a workaround:

apiGatewayManagementApiClient.middlewareStack.add(
  (next) =>
    async (args) => {
      args.request.path = STAGE + args.request.path;
      return await next(args);
    },
  { step: "build" },
);

Note that this seems to happen for all of the commands within the ApiGatewayManagementApi.

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 16
  • Comments: 15 (2 by maintainers)

Most upvoted comments

I just came across this issue too. AWS Support pointed me here (case 8373266661). Is there an ETA on when this will be resolved? I have started a new project using the v3 SDK thinking it was ready for production use but given this bug was opened five months ago it seems this may not be the case? Are we supposed to be sticking with v2 SDK for the time being?

I stumbled upon this issue as well, if it can help here is a workaround:

import { ApiGatewayManagementApiClient, ServiceInputTypes, ServiceOutputTypes } from '@aws-sdk/client-apigatewaymanagementapi';
import { HttpRequest } from '@aws-sdk/protocol-http';
import { FinalizeHandler, FinalizeHandlerArguments, HandlerExecutionContext } from '@aws-sdk/types';

const websocketApiStageName = 'ws';

const apiGatewayManagementApi = new ApiGatewayManagementApiClient({ 
    apiVersion: 'latest', 
    endpoint: 'BLABLA'
});
apiGatewayManagementApi.middlewareStack.addRelativeTo(
    (next: FinalizeHandler<ServiceInputTypes, ServiceOutputTypes>, context: HandlerExecutionContext) => {
        return async function (args: FinalizeHandlerArguments<ServiceInputTypes>) {
            if (!HttpRequest.isInstance(args.request)) return next(args);
            let prefixedRequest = args.request.clone();
            if (!prefixedRequest.path.startsWith(`/${websocketApiStageName}`)) {
                prefixedRequest.path = `/${websocketApiStageName}${args.request.path}`;
            }
            return await next({
                ...args,
                request: prefixedRequest
            });
        };
    },
    {
        relation: 'before',
        toMiddleware: 'awsAuthMiddleware'
    }
);

This appears to be fixed for me. (More specifically, after a recent update this stopped working again but after I dropped the middleware changes it works again.)

It appears to be due to #2543, specifically https://github.com/aws/aws-sdk-js-v3/commit/8ee8c9560098cc0ff7b877e857ba76d40ea571ae#diff-02accac44a940f23789960def1fcee3b352fe91e07e3e01a29aae545a7580ca2 as best I can tell, which would suggest anyone updating to 3.20.0 will need to drop the middleware changes they added.

For completeness sake and so this use case is taken into consideration, I tried setting up a custom domain (i.e. ws.mydomain.com) through CloudFront (to be able to use SSL Certificate with auto redirect to HTTPS/WSS), specifically setting the Origin Path to /production so as to circumvent the stage portion of the url altogether.

TLDR; the @aws-sdk/client-apigatewaymanagementapi package fails to post to a connection through API Gateway in WebSocket mode through CloudFront using a custom domain (i.e. posting to https://ws.mydomain.com/@connections/{ConnectionId} rather than https://xxxxx.execute-api.us-xxxx-n.amazonaws.com/production/@connections/{ConnectionId}). It fails regardless of wscat successfully connecting to it (i.e. wscat -c wss://ws.mydomain.com)

For more detail, I followed these steps:

  • Registered a domain through Route53 (i.e. ws.mydomain.com)
  • Issued an SSL certificate on ACM for the domain, including wildcard subdomain (i.e. both mydomain.com and *.mydomain.com), in us-east-1
  • Created a WebSocket API Gateway with corresponding $connect/$default/$disconnect integrations pointing to a pre-existing Lambda
  • Deployed a stage named production (i.e. xxxxx.execute-api.us-xxxx-n.amazonaws.com/production)
  • Created a CloudFront distribution pointing to Origin Domain xxxxx.execute-api.us-xxxx-n.amazonaws.com and Origin Path /production (with cache in legacy mode, virtually turned off), using a custom domain ws.mydomain.com and respective ACM certificate
  • Created ws.mydomain.com sub-domain in Route53 and pointed a simple A record to the aforementioned CloudFront distribution

As I mentioned in the TLDR; this setup works just fine using other WebSocket clients, but if on my code using aws-sdk-js-v3 I change:

const apiGatewayManagementApiClient = new ApiGatewayManagementApiClient({
  endpoint: `https://${API_ID}.execute-api.${REGION}.amazonaws.com/${STAGE}`,
});

to:

const apiGatewayManagementApiClient = new ApiGatewayManagementApiClient({
  endpoint: `https://ws.mydomain.com`, // also tested with a trailing slash (`/`)
});

Running PostToConnectionCommand generates the same ForbiddenException: ForbiddenException error.

As per the dist code, since the resolved path appends the following:

resolvedPath = "/@connections/{ConnectionId}";

This means that ApiGatewayManagementApiClient is then sending the request to https://ws.mydomain.com/@connections/{ConnectionId}, which CloudFront then maps to https://xxxxx.execute-api.us-xxxx-n.amazonaws.com/production/@connections/{ConnectionId}.

Furthermore, I figured maybe there’s some headers that ApiGatewayManagementApiClient might be sending to CloudFront which are not being forwarded to API Gateway, and sure thing, it is the content-type header, but even letting that one through made no difference. Also tried sending an empty body, again same error.

For the time being I have resolved to edit the Aws_restJson1.js file under dist and hard-coding the stage. I still do not suggest this as a workaround.

Please fix this issue, I have more projects depending on WebSocket API Gateway that need to be migrated to aws-sdk-js-v3.

This seems to be reported a lot now. There was some work done in https://github.com/aws/aws-sdk-js-v3/issues/2001 maybe join forces? @AllanZhengYP @birtles

Hi @samstradling I can confirm the stage is not displayed in Smithy model. I don’t think it would work in v2 SDK either, since the JSON model there doesn’t include the stage either.

Looks like the service doc does mention the correct uri, I will open a ticket to the service team internally to confirm if the model is correct.

Ref: P42821297