aws / aws-sdk-js-v3

Modularized AWS SDK for JavaScript.
Apache License 2.0
2.95k stars 553 forks source link

TypeError: SendMediaMessageCommand is not a constructor (@aws-sdk/client-pinpoint-sms-voice-v2) #6197

Closed JUSTINMKAUFMAN closed 1 week ago

JUSTINMKAUFMAN commented 1 week ago

Checkboxes for prior research

Describe the bug

The error in the title (TypeError: SendMediaMessageCommand is not a constructor) is raised when trying to send an MMS message using the @aws-sdk/client-pinpoint-sms-voice-v2 dependency.

import { PinpointSMSVoiceV2Client, SendMediaMessageCommand, SendMediaMessageCommandInput } from '@aws-sdk/client-pinpoint-sms-voice-v2';

const input: SendMediaMessageCommandInput = { 
  DestinationPhoneNumber: phone,
  OriginationIdentity: originationPhone,
  MessageBody: body,
  MediaUrls: [s3Url]
};

const command = new SendMediaMessageCommand(input);  // Error here

I am following the example in the SendMediaMessageCommand documentation to the letter.

Similar code in my stack for using @aws-sdk/client-sns to send a regular SMS works just fine (e.g. const command = new PublishCommand({ PhoneNumber: phone, Message: body })).

SDK version number

@aws-sdk/client-pinpoint-sms-voice-v2@3.596.0

Which JavaScript Runtime is this issue in?

Node.js

Details of the browser/Node.js/ReactNative version

v18.15.0

Reproduction Steps

// `sendMMS.ts` (a lambda handler inside a CDK app)
import { PinpointSMSVoiceV2Client, SendMediaMessageCommand } from '@aws-sdk/client-pinpoint-sms-voice-v2';

const client = new PinpointSMSVoiceV2Client({ region: 'us-west-2' });

export async function sendMMS(phone: string, s3Url: string, body: string, originationPhone: string): Promise<boolean> {
  try {
    const input = { 
      DestinationPhoneNumber: phone,
      OriginationIdentity: originationPhone,
      MessageBody: body,
      MediaUrls: [s3Url]
    };

    const command = new SendMediaMessageCommand(input);    
    const response = await client.send(command);

    if (!response) { 
      console.error(`Error sending MMS to phone ${phone} with ${s3Url} and body ${body}: no response`);
    }
  } catch (error) {
    console.error(`Error sending MMS to phone ${phone} with ${s3Url} and body ${body}: ${error}`);
  }

  return true;
}

Observed Behavior

TypeError: SendMediaMessageCommand is not a constructor

Expected Behavior

The code to use the documented constructor for SendMediaMessageCommand with no errors.

Possible Solution

I don't know what is causing this issue so it's hard to suggest a solution, but I'd start by comparing the source code for instantiating other Command classes (such as the @aws-sdk/client-sns PublishCommand, which works fine for me) to the SendMediaMessageCommand to find potential discrepancies.

Additional Information/Context

This code comes from my AWS CDK app which is written in Typescript.

Here is my tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "moduleResolution": "node",
    "lib": ["es2020"],
    "declaration": true,
    "strict": true,
    "esModuleInterop": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "noUnusedLocals": false,
    "noUnusedParameters": false,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": false,
    "inlineSourceMap": true,
    "inlineSources": true,
    "experimentalDecorators": true,
    "strictPropertyInitialization": false,
    "allowSyntheticDefaultImports": true,
    "typeRoots": ["./node_modules/@types"]
  }  
}

...and here is my package.json (in relevant part):

{
    "name": "backend",
    "version": "1.0.0",
    "bin": {
        "backend": "bin/backend.js"
    },
    "devDependencies": {
        "@graphql-codegen/cli": "^5.0.2",
        "@graphql-codegen/typescript": "^4.0.6",
        "@graphql-codegen/typescript-operations": "^4.2.0",
        "@graphql-codegen/typescript-resolvers": "^4.0.6",
        "@graphql-tools/merge": "^9.0.4",
        "@types/aws-lambda": "8.10.138",
        "@types/node": "^20.14.2",
        "@types/uuid": "^9.0.8",
        "@typescript-eslint/eslint-plugin": "^7.6.0",
        "@typescript-eslint/parser": "^7.6.0",
        "aws-cdk": "^2.146.0",
        "esbuild": "^0.21.5",
        "eslint": "^8.56.0",
        "ts-node": "^10.9.2",
        "typescript": "~5.4.5"
    },
    "dependencies": {
        "@aws-appsync/utils": "^1.8.0",
        "@aws-cdk/aws-cognito-identitypool-alpha": "^2.146.0-alpha.0",
        "@aws-sdk/client-cloudwatch-logs": "^3.596.0",
        "@aws-sdk/client-cognito-identity-provider": "^3.596.0",
        "@aws-sdk/client-dynamodb": "^3.596.0",
        "@aws-sdk/client-lightsail": "^3.596.0",
        "@aws-sdk/client-pinpoint-sms-voice-v2": "3.596.0",
        "@aws-sdk/client-s3": "^3.596.0",
        "@aws-sdk/client-secrets-manager": "^3.596.0",
        "@aws-sdk/client-ses": "^3.596.0",
        "@aws-sdk/client-sesv2": "^3.596.0",
        "@aws-sdk/client-sns": "^3.596.0",
        "@aws-sdk/client-sqs": "^3.596.0",
        "@aws-sdk/lib-dynamodb": "^3.596.0",
        "@aws-sdk/s3-request-presigner": "^3.596.0",
        "@aws-sdk/util-dynamodb": "^3.596.0",
        "aws-cdk-lib": "^2.146.0",
        "axios": "^1.6.8",
        "constructs": "^10.3.0",
        "dotenv": "^16.4.5",
        "moment": "^2.30.1",
        "moment-timezone": "^0.5.45",
        "runtypes": "^6.7.0",
        "source-map-support": "^0.5.21",
        "stream-buffers": "^3.0.2",
        "url": "^0.11.3",
        "uuidv4": "^6.2.13"
    }
}
kuhe commented 1 week ago

is your CDK removing the AWS SDK from your application bundle?

If so, you should turn that option off. This operation may have not made it into the AWS Lambda provided version of the AWS SDK for JavaScript (v3) yet.

https://docs.aws.amazon.com/lambda/latest/operatorguide/sdks-functions.html beyond simple testing you should configure the CDK or any other tool to use your own pinned version of the AWS SDK.

You should be able check what exports are available like this:

import * as namespace from "@aws-sdk/client-pinpoint-sms-voice-v2";

console.log(Object.keys(namespace));

Or, using CJS to check the version number:

console.log(
  require("@aws-sdk/client-pinpoint-sms-voice-v2/package.json").version
);
JUSTINMKAUFMAN commented 1 week ago

Thank you very much for the quick reply!

In my cdk.json I have (in relevant part) { "watch": { "exclude": ["node_modules"] } }, is that the option you're referring to?

In my "context" object (in the same file) I have:

 "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true,
    "@aws-cdk/core:stackRelativeExports": true,
    "@aws-cdk/aws-rds:lowercaseDbIdentifier": true,
    "@aws-cdk/aws-lambda:recognizeVersionProps": true,
    "@aws-cdk/aws-lambda:recognizeLayerVersion": true,
    "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true,
    "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
    "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
    "@aws-cdk/core:checkSecretUsage": true,
    "@aws-cdk/aws-iam:minimizePolicies": true,
    "@aws-cdk/core:validateSnapshotRemovalPolicy": true,
    "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
    "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
    "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
    "@aws-cdk/core:target-partitions": [
      "aws",
      "aws-cn"
    ]

...not sure any of those look right though.

Finally, the code to create the NodeJsFunction has the following props:

{
    functionName: 'sendMMS',
    runtime: Runtime.NODEJS_18_X,
    handler: 'handler',
    entry: `functions/sendMMSHandler.ts`,
    architecture: Architecture.ARM_64,
    timeout: Duration.minutes(15),
    awsSdkConnectionReuse: true,
    bundling: {
      minify: false,
      sourceMap: true,
      target: 'es2020'
    },
    environment: {
      'NODE_OPTIONS': '--enable-source-maps'
    }
  }

If you could point me to the option you are referencing (that would cause CDK to remove the AWS SDK from my application bundle), that'd be much appreciated. Thanks again.

JUSTINMKAUFMAN commented 1 week ago

I took a shot at adding the library to the 'externalModules' array in my NodeJsFunction bundling props, and now I get this error: TypeError: import_client_pinpoint_sms_voice_v2.SendMediaMessageCommand is not a constructor (similar).

RanVaknin commented 1 week ago

Hi @JUSTINMKAUFMAN ,

Just to add to what @kuhe said, this issue is likely happening because when you deploy your Lambda using CDK, the Lambda function comes pre-installed with the lambda provided SDK version that is not @latest. Looking at the SendMediaMessageCommand API command, it was added 2 months ago, and Lambda only updates their provided SDK version 1-2 times a year which means this operation doesn't exist on the SDK version running from your lambda function.

In normal SDK-land, fixing this would mean bundling and minifying your application and deploying to lambda, or providing the desired SDK version as a Lambda Layer. In CDK-land you'll likely need to do CDK specific things to get it to work. Ideally CDK questions would go to the CDK repo since that team deals with CDK specific issues and will be better equipped to assist you.

That being said, I found this issue on their repo that seems to be directly related to the issue you are having. The suggestion is to specify:

bundling: { 
    externalModules: [],
   // more options,
}

Can you give the workaround there a try and let us know if it solves the issue?

Thanks, Ran~

JUSTINMKAUFMAN commented 1 week ago

Thanks for the follow-up @RanVaknin. I did manage to solve this after trying a number of different things. My learnings:

  1. I wasn't able to resolve this simply by changing the configuration of the lambda construct itself (including, but not limited to, your suggestion around the externalModules property).

  2. My understanding is there are 2 ways to tackle this in a CDK app.

    • The first is to bundle all deps inside the lambda, tell the lambda to treat @aws-sdk/* as an external module, and deploy the lambda as a dockerized image. I did not wind up doing this.
    • The second (my approach) is to create a Lambda Layer that contains the necessary libraries and import that layer into the lambda.

This took a bunch of tinkering to get working (I couldn't find any specific documentation on how to build/consume a layer that simply exposes the aws-sdk modules you need), so here is what I wound up with in case it helps someone else:

The stack for the lambda layer:

import { Construct } from 'constructs'
import { Stack, StackProps } from 'aws-cdk-lib'
import { StringParameter } from 'aws-cdk-lib/aws-ssm'
import { Architecture, Code, LayerVersion, Runtime } from 'aws-cdk-lib/aws-lambda'

export class LambdaStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props)

    const awsSdkLayer = new LayerVersion(this, 'AwsSdkLayer', {
      layerVersionName: 'AwsSdkLayer',
      code: Code.fromAsset('lambda/layers/awsSdkLayer'),
      compatibleRuntimes: [Runtime.NODEJS_18_X],
      compatibleArchitectures: [Architecture.ARM_64]
    })

    new StringParameter(this, 'aws-sdk-layer-arn', { parameterName: `/Lambda/awsSdkLayerArn`, stringValue: awkSdkLayer.layerVersionArn })
  }
}

Then I created a folder structure like: ./lambda/layers/awsSdkLayer/nodejs ...and inside nodejs, I created 2 files:

package.json :

{
    "name": "aws-sdk-layer",
    "version": "0.1.0",
    "dependencies": {
        "@aws-sdk/client-cloudwatch-logs": "^3.596.0",
        "@aws-sdk/client-cognito-identity-provider": "^3.596.0",
        "@aws-sdk/client-dynamodb": "^3.596.0",
        "@aws-sdk/client-lightsail": "^3.596.0",
        "@aws-sdk/client-pinpoint-sms-voice-v2": "^3.596.0",
        "@aws-sdk/client-s3": "^3.596.0",
        "@aws-sdk/client-secrets-manager": "^3.596.0",
        "@aws-sdk/client-ses": "^3.596.0",
        "@aws-sdk/client-sesv2": "^3.596.0",
        "@aws-sdk/client-sns": "^3.596.0",
        "@aws-sdk/client-sqs": "^3.596.0",
        "@aws-sdk/lib-dynamodb": "^3.596.0",
        "@aws-sdk/s3-request-presigner": "^3.596.0",
        "@aws-sdk/util-dynamodb": "^3.596.0"
    }
}

tsconfig.json

{ "extends": "../../../../tsconfig.json" }

(Given that I ultimately stripped everything out of it, I probably didn't need the tsconfig after all).

And in the stack where the lambda gets created:

// Getting the layer via SSM
const parameter = StringParameter.fromStringParameterName(scope, `aws-sdk-layer-arn`, '/Lambda/awsSdkLayerArn')
const awsSdkLayer = LayerVersion.fromLayerVersionArn(scope, `aws-sdk-layer`, parameter.stringValue)

// Creating the lambda
new NodejsFunction(scope, `myLambda`, {
  functionName: 'myLambda',
  runtime: Runtime.NODEJS_18_X,
  handler: 'handler',
  entry: `functions/myLambda.ts`,
  architecture: Architecture.ARM_64,
  timeout: Duration.minutes(15),
  layers: [awsSdkLayer],  // ---> IMPORT THE LAYER
  bundling: {
    minify: false,
    sourceMap: true,
    target: 'es2020',
    externalModules: ['@aws-sdk/client-pinpoint-sms-voice-v2'] // ---> DECLARE EXTERNAL MODULES
  },
  environment: {
    'NODE_OPTIONS': '--enable-source-maps'
  }
})

I am confident this could be further optimized, or even generally approached in a more sophisticated way (and I'm all ears if you have suggestions!), but I was just relieved to finally see the runtime errors disappear.

RanVaknin commented 1 week ago

Great to hear that you worked through this.

I don't have any suggestions since Im not super familiar with CDK, but like you said there are multiple ways to go about this. Im going to close this issue but your answer will remain discoverable.

Thanks again, Ran~