This guide provides a complete walkthrough for integrating AWS Pinpoint's MMS capabilities into a RedwoodJS application. We'll cover everything from setting up your AWS resources and RedwoodJS project to implementing the sending logic, handling errors, testing, and deployment. By the end, you'll have a functional service capable of sending text messages with media attachments (images, videos, etc.) via AWS.
Target Audience: Developers familiar with RedwoodJS and basic AWS concepts. Goal: Build a production-ready RedwoodJS service to send MMS messages using AWS Pinpoint and S3.
Project Overview and Goals
Sending rich media content like images or videos alongside text messages (MMS) can significantly enhance user engagement compared to standard SMS. However, integrating MMS sending requires specific cloud infrastructure setup and careful handling of media files.
This guide solves the problem of reliably sending MMS messages from a RedwoodJS backend by leveraging AWS services.
Specific Goals:
- Set up necessary AWS resources: IAM permissions, an S3 bucket for media storage, and an AWS Pinpoint origination identity (phone number) capable of sending MMS.
- Configure a RedwoodJS project with the AWS SDK v3.
- Implement a RedwoodJS service function to send MMS messages using the AWS Pinpoint SMS and Voice v2 API.
- Expose this functionality via a GraphQL mutation.
- Handle media uploads to S3 (though the upload mechanism itself is out of scope for this guide, we'll assume files are already in S3).
- Implement basic error handling and logging.
- Provide guidance on testing, security, and deployment.
Technologies Involved:
- RedwoodJS: A full-stack JavaScript/TypeScript framework for building modern web applications. Chosen for its integrated structure (API, Web, Prisma) and developer experience features (generators, cells, services).
- AWS Pinpoint (SMS and Voice v2 API): AWS's communication service, used here specifically for its
SendMediaMessage
API action to dispatch MMS messages. Chosen for its scalability and integration with other AWS services. Note: Pinpoint MMS sending is primarily available for US and Canadian destinations.- Important Limitation: While Pinpoint can send MMS (media), it currently cannot receive inbound media messages (like images sent to your Pinpoint number). It can typically receive inbound text (SMS) replies on MMS-capable numbers if configured.
- AWS S3 (Simple Storage Service): Used to store the media files (images, videos, PDFs) that will be attached to the MMS messages. Chosen for its durability, availability, and seamless integration with Pinpoint.
- AWS IAM (Identity and Access Management): Manages permissions, ensuring our RedwoodJS backend has the necessary rights to interact with Pinpoint and S3 securely.
- AWS SDK for JavaScript v3: The official library for interacting with AWS services from Node.js applications.
- TypeScript: For type safety and improved developer experience within RedwoodJS.
- GraphQL: RedwoodJS's default API layer, used to expose the MMS sending functionality.
System Architecture:
(Note: This text-based diagram illustrates the flow. For complex systems, a visual diagram (e.g., using tools like draw.io, Lucidchart, or Mermaid) is recommended.)
+-----------------+ +---------------------+ +-----------------------+ +-----------------+
| RedwoodJS | ---> | RedwoodJS API | ---> | AWS SDK v3 | ---> | AWS Pinpoint |
| Frontend (Web) | | (GraphQL Mutation) | | (Pinpoint Client) | | SendMediaMessage|
+-----------------+ +---------------------+ +-----------------------+ +-------+---------+
^ | |
| User Interaction | Calls Service Function | Reads Media
v v v
+-----------------+ +---------------------+ +-----------------+
| User | | RedwoodJS Service | | AWS S3 Bucket |
| (Initiates Send)| | (mmsService.ts) | | (Media Files) |
+-----------------+ +---------------------+ +-----------------+
| Uses Credentials
v
+-----------------------+
| AWS IAM Permissions |
+-----------------------+
Prerequisites:
- Node.js (LTS version recommended) and Yarn installed.
- An AWS account with appropriate permissions to create IAM users/roles, S3 buckets, and manage Pinpoint resources.
- AWS CLI installed and configured locally (for initial setup/verification).
- Basic understanding of RedwoodJS project structure and services.
- Familiarity with command-line interfaces.
Expected Outcome:
A RedwoodJS application with a GraphQL mutation sendMms
that accepts a destination phone number, message body, and an array of S3 URIs for media files. Calling this mutation will trigger an MMS message send via AWS Pinpoint.
1. Setting up AWS Resources
Before writing any RedwoodJS code, we need the necessary AWS infrastructure.
Step 1: Create an IAM User/Role for RedwoodJS Backend
It's crucial to follow the principle of least privilege. Create an IAM user (or role, if deploying to EC2/ECS/Lambda) specifically for your RedwoodJS application.
-
Navigate to the IAM console in your AWS account.
-
Go to ""Users"" and click ""Create user"".
-
Provide a username (e.g.,
redwoodjs-mms-app-user
). -
Choose ""Provide user access to the AWS Management Console"" - Optional. This is only necessary if a human needs to log into the AWS console with this user's credentials. It's not required for the application's programmatic access via access keys.
-
Select ""Attach policies directly"".
-
Click ""Create policy"".
-
Switch to the ""JSON"" editor.
-
Paste the following policy, replacing
YOUR_S3_BUCKET_NAME
andYOUR_AWS_REGION
:{ ""Version"": ""2012-10-17"", ""Statement"": [ { ""Sid"": ""AllowPinpointMMS"", ""Effect"": ""Allow"", ""Action"": ""sms-voice:SendMediaMessage"", ""Resource"": ""*"" // Ideally, restrict this to the ARN of your specific Pinpoint application or origination identity if possible, following the principle of least privilege. Example: ""arn:aws:mobiletargeting:REGION:ACCOUNT_ID:apps/PINPOINT_APP_ID/*"" or similar based on the specific identity type. }, { ""Sid"": ""AllowReadFromMMSBucket"", ""Effect"": ""Allow"", ""Action"": ""s3:GetObject"", ""Resource"": ""arn:aws:s3:::YOUR_S3_BUCKET_NAME/*"" }, { ""Sid"": ""AllowListMMSBucket"", ""Effect"": ""Allow"", ""Action"": ""s3:ListBucket"", ""Resource"": ""arn:aws:s3:::YOUR_S3_BUCKET_NAME"", ""Condition"": { ""StringLike"": { ""s3:prefix"": [ ""*"" ] } } } ] }
- Explanation:
sms-voice:SendMediaMessage
: Allows sending MMS via the Pinpoint SMS/Voice v2 API.s3:GetObject
: Allows reading the media files from your designated S3 bucket. Pinpoint needs this to fetch the media.s3:ListBucket
: While not strictly required bySendMediaMessage
itself, it's often useful for debugging or listing objects if your app needs it. You can remove it if onlyGetObject
is needed. EnsureYOUR_S3_BUCKET_NAME
is correctly replaced.
- Explanation:
-
Click ""Next"".
-
Give the policy a name (e.g.,
RedwoodJSMMSSendingPolicy
). -
Click ""Create policy"".
-
Go back to the user creation tab, refresh the policy list, and select the
RedwoodJSMMSSendingPolicy
you just created. -
Click ""Next"".
-
Review and click ""Create user"".
-
Crucially: Navigate to the newly created user, go to the ""Security credentials"" tab, and under ""Access keys"", click ""Create access key"".
-
Select ""Application running outside AWS"" (or the appropriate option).
-
Click ""Next"". Add a description tag if desired.
-
Click ""Create access key"".
-
Immediately copy the ""Access key ID"" and ""Secret access key"". You won't be able to see the secret key again. Store these securely. We'll use them in our RedwoodJS environment variables later.
Step 2: Create an S3 Bucket for Media Files
Pinpoint needs access to your media files stored in S3.
- Navigate to the S3 console.
- Click ""Create bucket"".
- Enter a globally unique bucket name (e.g.,
your-company-mms-media-unique-id
). Remember this name. - Select the same AWS Region where you plan to register your Pinpoint phone number. This is mandatory. MMS sending requires the S3 bucket and the Pinpoint originator to be in the same region.
- Block Public Access: Keep ""Block all public access"" checked. We are granting access via the IAM policy, which is more secure.
- Leave other settings as default unless you have specific requirements (versioning, encryption, etc.).
- Click ""Create bucket"".
Step 3: Obtain an MMS-Capable Pinpoint Origination Identity
You need a phone number (Short Code, Toll-Free Number, or 10DLC for the US; Long Code or Short Code for Canada) registered in Pinpoint and enabled for MMS.
- Navigate to the Pinpoint console. Switch to the same AWS Region used for your S3 bucket.
- In the left navigation, under ""SMS and voice"", click ""Phone numbers"".
- Click ""Request phone number"".
- Select the target country (US or Canada for MMS).
- Choose the number type (Toll-Free, 10DLC, Short Code). Toll-Free numbers often have the simplest setup for testing, but 10DLC is standard for application-to-person (A2P) messaging in the US. Short codes require a separate, more involved application process.
- Follow the on-screen instructions for registration. This may involve providing business information and use case details, especially for 10DLC and Short Codes. Approval times vary.
- Verification: Once the number is provisioned and Active, check its capabilities. Click on the phone number in the Pinpoint console. Look for ""MMS"" listed under ""Capabilities"". If it only shows ""SMS"", the number cannot send MMS, and you might need to request a new one specifically ensuring MMS support (most numbers requested after May 2024 should include it automatically if available for the type/region).
- Note down the full E.164 formatted phone number (e.g.,
+12065550100
). This is yourOriginationIdentity
.
2. Setting up the RedwoodJS Project
Now, let's configure our RedwoodJS application.
Step 1: Create or Use Existing RedwoodJS App
If you don't have one, create a new RedwoodJS project:
yarn create redwood-app ./redwood-mms-app --typescript
cd redwood-mms-app
Step 2: Install AWS SDK v3
We need the specific client for the Pinpoint SMS and Voice v2 API.
# Navigate to the API side
cd api
# Install the AWS SDK v3 client for Pinpoint SMS/Voice
yarn add @aws-sdk/client-pinpoint-sms-voice-v2
# Navigate back to the project root (optional)
cd ..
Step 3: Configure Environment Variables
Store your AWS credentials and configuration securely. Create or edit the .env
file in the root of your RedwoodJS project. Never commit this file to version control if it contains secrets. Use a .env.example
and .gitignore
.
# .env
# AWS Credentials for MMS Sending
# Replace with the keys generated in AWS Setup Step 1
AWS_ACCESS_KEY_ID="YOUR_ACCESS_KEY_ID"
AWS_SECRET_ACCESS_KEY="YOUR_SECRET_ACCESS_KEY"
# AWS Region where Pinpoint number and S3 bucket reside
# Example: us-east-1
AWS_REGION="YOUR_AWS_REGION"
# Pinpoint Origination Identity (MMS-capable phone number)
# Example: +12065550100
PINPOINT_ORIGINATION_NUMBER="YOUR_PINPOINT_PHONE_NUMBER_E164"
# S3 Bucket Name for Media Files
# Example: your-company-mms-media-unique-id
S3_MMS_BUCKET_NAME="YOUR_S3_BUCKET_NAME"
- Explanation:
AWS_ACCESS_KEY_ID
,AWS_SECRET_ACCESS_KEY
: Credentials for the IAM user created earlier. The SDK will automatically pick these up.AWS_REGION
: Essential for the SDK to target the correct AWS region where your Pinpoint number and S3 bucket exist.PINPOINT_ORIGINATION_NUMBER
: The E.164 formatted phone number registered in Pinpoint that will send the MMS.S3_MMS_BUCKET_NAME
: The name of the S3 bucket created for media storage.
Important: Ensure your root .gitignore
file includes .env
.
3. Implementing Core Functionality (RedwoodJS Service)
We'll create a RedwoodJS service to encapsulate the logic for sending MMS messages.
Step 1: Generate the Service
Use the RedwoodJS CLI to generate a service for MMS operations.
yarn rw g service mms
This creates api/src/services/mms/mms.ts
and api/src/services/mms/mms.test.ts
.
Step 2: Implement the sendMms
Service Function
Open api/src/services/mms/mms.ts
and add the following code:
// api/src/services/mms/mms.ts
import {
PinpointSMSVoiceV2Client,
SendMediaMessageCommand,
SendMediaMessageCommandInput,
SendMediaMessageCommandOutput,
} from '@aws-sdk/client-pinpoint-sms-voice-v2'
import { logger } from 'src/lib/logger' // Redwood's built-in logger
// Initialize the Pinpoint client
// The SDK automatically picks up credentials and region from environment variables
// (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION)
const pinpointClient = new PinpointSMSVoiceV2Client({})
interface SendMmsArgs {
destinationPhoneNumber: string // E.164 format (e.g., +14155550101)
mediaUrls?: string[] // Array of S3 URIs (e.g., s3://bucket-name/image.jpg)
messageBody?: string // The text part of the message
}
export const sendMms = async ({
destinationPhoneNumber,
mediaUrls,
messageBody,
}: SendMmsArgs): Promise<SendMediaMessageCommandOutput> => {
// Validate essential inputs
if (!destinationPhoneNumber) {
throw new Error('Destination phone number is required.')
}
if (!mediaUrls?.length && !messageBody) {
throw new Error('Either mediaUrls or messageBody must be provided.')
}
// Retrieve configuration from environment variables
const originationNumber = process.env.PINPOINT_ORIGINATION_NUMBER
const s3BucketName = process.env.S3_MMS_BUCKET_NAME // Used for validation
if (!originationNumber) {
logger.error('PINPOINT_ORIGINATION_NUMBER environment variable is not set.')
throw new Error('MMS sending configuration is incomplete.')
}
if (!s3BucketName) {
logger.error('S3_MMS_BUCKET_NAME environment variable is not set.')
throw new Error('MMS sending configuration is incomplete.')
}
// Basic validation for S3 URIs (recommended)
if (mediaUrls) {
for (const url of mediaUrls) {
if (!url.startsWith(`s3://${s3BucketName}/`)) {
logger.error(
{ url, expectedBucket: s3BucketName },
'Provided media URL does not match the configured S3 bucket or format.'
)
// For production readiness, throw an error for invalid S3 URLs
throw new Error(`Invalid S3 URL format or bucket: ${url}`);
}
}
}
const commandInput: SendMediaMessageCommandInput = {
DestinationPhoneNumber: destinationPhoneNumber,
OriginationIdentity: originationNumber,
MessageBody: messageBody,
MediaUrls: mediaUrls,
// ConfigurationSetName: 'YourOptionalConfigurationSet', // Optional: For event streaming
}
logger.info({ commandInput }, 'Attempting to send MMS via Pinpoint')
try {
const command = new SendMediaMessageCommand(commandInput)
const response = await pinpointClient.send(command)
logger.info({ response }, 'Successfully sent MMS command to Pinpoint')
// The response contains a MessageId if the request was accepted by Pinpoint.
// This *does not* guarantee delivery to the handset.
// Delivery status tracking requires setting up event streaming (e.g., via SNS or Kinesis).
if (!response.MessageId) {
logger.warn({ response }, 'Pinpoint accepted the request but did not return a MessageId.')
}
return response
} catch (error) {
logger.error({ error, commandInput }, 'Failed to send MMS via Pinpoint')
// Re-throw the error so it can be handled by the GraphQL layer or calling function
// You might want to wrap specific AWS errors for cleaner handling upstream
throw new Error(`MMS sending failed: ${error.message || 'Unknown AWS error'}`)
// Consider checking error.name for specific AWS exceptions like
// 'ValidationException', 'AccessDeniedException', 'ResourceNotFoundException', etc.
}
}
// You can add other MMS related functions here if needed
// export const getMmsStatus = async (messageId: string) => { ... } // Requires event streaming setup
- Explanation:
- We import the necessary client and command from the AWS SDK v3.
- The
PinpointSMSVoiceV2Client
is instantiated. It automatically finds credentials (via environment variables, EC2 instance profile, etc.) and the region (AWS_REGION
). - The
sendMms
function takes the destination number, an optional array of S3 URIs (s3://bucket/object.ext
), and an optional message body. - It performs basic input validation and retrieves necessary config from
.env
. - It includes S3 URI validation that throws an error if the format or bucket is incorrect.
- It constructs the
SendMediaMessageCommandInput
object, mapping our arguments to the API parameters.OriginationIdentity
is set to our configured sending number.MediaUrls
expects an array of strings in the formats3://YOUR_BUCKET_NAME/path/to/your/file.jpg
. Pinpoint fetches these files.
- The
pinpointClient.send()
method dispatches the command to AWS. - Basic logging is included using Redwood's built-in
logger
. - Error handling uses a
try...catch
block, logs the error, and re-throws it. - The successful response from AWS includes a
MessageId
, which confirms AWS accepted the request, not that the message was delivered.
4. Building a Complete API Layer (GraphQL Mutation)
Let's expose the sendMms
service function through Redwood's GraphQL API.
Step 1: Define the GraphQL Schema
Open or create api/src/graphql/mms.sdl.ts
:
// api/src/graphql/mms.sdl.ts
export const schema = gql`
type MmsResponse {
messageId: String
# Add other relevant fields from SendMediaMessageCommandOutput if needed
# Consider adding fields like 'Message' (containing status info) if useful,
# though detailed status often requires event streaming setup.
}
type Mutation {
"""
Sends an MMS message with optional media attachments via AWS Pinpoint.
Requires destinationPhoneNumber and at least one of mediaUrls or messageBody.
"""
sendMms(
destinationPhoneNumber: String!
mediaUrls: [String!]
messageBody: String
): MmsResponse! @requireAuth # Or @skipAuth depending on your needs
}
`
- Explanation:
- We define a
MmsResponse
type to represent the data returned (primarily theMessageId
). Added a comment about potentially including other fields. - We define a
sendMms
mutation within theMutation
type. - It accepts
destinationPhoneNumber
,mediaUrls
(an array of strings), andmessageBody
as arguments.!
denotes required fields. @requireAuth
ensures only authenticated users can call this mutation. Change to@skipAuth
if authentication is not needed for this specific action, but be cautious about potential abuse.
- We define a
Step 2: Link Service to GraphQL (Implicit)
RedwoodJS automatically maps GraphQL resolvers to service functions based on naming conventions. Since our mutation is named sendMms
and our service file (api/src/services/mms/mms.ts
) exports a function named sendMms
, Redwood handles the connection. No explicit resolver code is needed in api/src/functions/graphql.ts
unless you need custom logic before or after calling the service.
Step 3: Testing the Mutation
-
Ensure you have uploaded a test file (e.g.,
test.jpg
) to your S3 bucket (s3://YOUR_S3_BUCKET_NAME/test.jpg
). Make sure the IAM user hass3:GetObject
permission for this file. -
Start your RedwoodJS development server:
yarn rw dev
. -
Navigate to the GraphQL Playground, usually
http://localhost:8911/graphql
. -
If using
@requireAuth
, ensure you configure theAuthorization: Bearer <token>
header appropriately based on your auth setup. -
Execute the following mutation (replace placeholders):
mutation SendTestMms { sendMms( destinationPhoneNumber: "+1YOUR_TEST_RECIPIENT_NUMBER" # E.164 format messageBody: "Hello from RedwoodJS MMS! Check the image." mediaUrls: ["s3://YOUR_S3_BUCKET_NAME/test.jpg"] ) { messageId } }
-
Check the response in the Playground. You should see a
messageId
if successful. -
Check the recipient phone; the MMS message should arrive shortly.
-
Check the API server logs (
yarn rw dev
console output) for log messages from the service.
Example cURL Request:
curl 'http://localhost:8911/graphql' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer YOUR_AUTH_TOKEN' \ # Include ONLY if using @requireAuth
--data-binary '{"query":"mutation SendTestMms($dest: String!, $body: String, $media: [String!]) {\n sendMms(destinationPhoneNumber: $dest, messageBody: $body, mediaUrls: $media) {\n messageId\n }\n}","variables":{"dest":"+1YOUR_TEST_RECIPIENT_NUMBER","body":"Hello via cURL MMS!","media":["s3://YOUR_S3_BUCKET_NAME/test.jpg"]}}' \
--compressed
Example JSON Response:
{
"data": {
"sendMms": {
"messageId": "pinpoint-message-id-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
}
}
5. Integrating with Necessary Third-Party Services (AWS)
This section summarizes the critical integration points covered in the setup sections.
- AWS Pinpoint:
- Configuration: Requires an active, MMS-capable Origination Identity (phone number) in the same region as the S3 bucket.
- Credentials: Handled via IAM User Access Key ID and Secret Access Key stored in
.env
(AWS_ACCESS_KEY_ID
,AWS_SECRET_ACCESS_KEY
). - Region: Specified in
.env
(AWS_REGION
). - Integration: Via
@aws-sdk/client-pinpoint-sms-voice-v2
in the RedwoodJS service.
- AWS S3:
- Configuration: Requires an S3 bucket in the same region as the Pinpoint Origination Identity. Bucket access should not be public.
- Credentials: Access granted via the IAM policy attached to the IAM user (
s3:GetObject
). The same AWS keys are used. - Integration: Media files are referenced by their S3 URI (
s3://BUCKET_NAME/OBJECT_KEY
) in theMediaUrls
parameter of theSendMediaMessageCommand
.
- AWS IAM:
- Configuration: A dedicated IAM user/role with a least-privilege policy allowing
sms-voice:SendMediaMessage
ands3:GetObject
on the specific bucket. - Credentials: Access Key ID and Secret stored securely.
- Integration: The AWS SDK automatically uses these credentials to sign API requests.
- Configuration: A dedicated IAM user/role with a least-privilege policy allowing
Fallback Mechanisms: AWS services are generally reliable, but for critical messaging:
- Consider implementing retry logic (see Section 6).
- Monitor AWS Health Dashboard for service outages.
- For extreme high availability, you might consider multi-region setups or alternative providers, but this significantly increases complexity.
6. Implementing Error Handling, Logging, and Retry Mechanisms
Robust applications need proper error handling.
Error Handling Strategy:
- Service Layer: The
sendMms
service function includes atry...catch
block. It catches errors from the AWS SDK, logs detailed information (including the input parameters and the error itself), and then re-throws a generic error. This prevents leaking sensitive AWS error details directly to the client while still signaling failure. - GraphQL Layer: RedwoodJS automatically handles errors thrown from services. By default, it will return a GraphQL error response to the client, including the message from the thrown error. You can customize this behavior using Redwood's error handling capabilities if needed.
- Specific AWS Errors: You can enhance the
catch
block in the service to check for specific AWS error codes (e.g.,error.name === 'ValidationException'
) to provide more context or potentially trigger different actions (like alerting onAccessDeniedException
).
Logging:
- We used RedwoodJS's built-in
logger
(import { logger } from 'src/lib/logger'
). - Log Levels:
logger.info
: Used for successful operations or key steps (e.g., attempting send, successful send).logger.warn
: Used for potential issues that don't stop the process (e.g., missing MessageId in response).logger.error
: Used for critical failures (e.g., failed AWS SDK call, invalid S3 URL).
- Log Format: Redwood's default logger provides structured JSON logging, which is ideal for log aggregation tools (like Datadog, Logtail, AWS CloudWatch Logs). Include relevant context (like
commandInput
orresponse
) in the log objects.
Retry Mechanisms:
Network issues or temporary AWS glitches can occur. Implementing retries can improve reliability.
// Example modification within api/src/services/mms/mms.ts
// Simple retry function (consider using a library like 'async-retry' for more features)
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
async function retryOperation<T>(
operation: () => Promise<T>,
maxRetries = 3,
delayMs = 500, // Initial delay
backoffFactor = 2
): Promise<T> {
let attempt = 0;
while (attempt < maxRetries) {
try {
return await operation();
} catch (error) {
attempt++;
if (attempt >= maxRetries) {
logger.error({ error, attempt }, `Operation failed after ${maxRetries} attempts.`);
throw error; // Rethrow after final attempt
}
// Check if error is retryable (e.g., network errors, throttling)
// Avoid retrying on validation errors or permission errors.
// **IMPORTANT CAVEAT:** This check is very basic. Developers MUST carefully
// identify which specific AWS SDK v3 errors (by name or code) are genuinely
// transient and safe to retry for Pinpoint's SendMediaMessage API.
// Consult AWS documentation on error handling and retries for the specific service.
const isRetryable = error.name === 'ThrottlingException' || error.message.includes('Network Error'); // Example checks - **Refine this based on AWS docs and testing!**
if (!isRetryable) {
logger.error({ error, attempt }, `Non-retryable error encountered. Failing operation.`); // Added more info
throw error;
}
const waitTime = delayMs * Math.pow(backoffFactor, attempt - 1);
logger.warn({ error, attempt, waitTime }, `Attempt ${attempt} failed. Retrying in ${waitTime}ms...`);
await sleep(waitTime);
}
}
// Should not be reached if maxRetries > 0, but satisfies TypeScript
throw new Error('Retry logic failed unexpectedly.');
}
// --- Inside the sendMms function ---
// Replace the direct call:
// try {
// const command = new SendMediaMessageCommand(commandInput);
// const response = await pinpointClient.send(command);
// ...
// With the retry logic wrapped around the send command inside the try block:
try {
const response = await retryOperation(async () => {
const command = new SendMediaMessageCommand(commandInput);
return await pinpointClient.send(command);
});
// --- Rest of the try...catch block ---
logger.info({ response }, 'Successfully sent MMS command to Pinpoint')
// ... (rest of the success handling) ...
return response
} catch (error) {
// ... (existing error logging and re-throw) ...
logger.error({ error, commandInput }, 'Failed to send MMS via Pinpoint')
throw new Error(`MMS sending failed: ${error.message || 'Unknown AWS error'}`)
}
// } // This closing brace was missing in the original snippet, assuming it belongs here to close the sendMms function
- Explanation:
- A basic
retryOperation
helper is added. - It takes the operation (the
pinpointClient.send
call) and retry parameters. - It uses exponential backoff (
delayMs * Math.pow(backoffFactor, attempt - 1)
) to avoid overwhelming the service. - Crucially: It includes a basic check (
isRetryable
) with a strong caveat that this check must be refined based on AWS documentation and testing to only retry genuinely transient errors. Do not retry on errors likeValidationException
orAccessDeniedException
. - Libraries like
async-retry
offer more sophisticated features (jitter, custom retry conditions).
- A basic
Testing Error Scenarios:
- Mocking: In unit tests, configure the AWS SDK mock to throw specific exceptions (
ValidationException
,ThrottlingException
,AccessDeniedException
) and verify that your error handling and retry logic behave as expected. - Manual: Temporarily revoke IAM permissions, provide invalid phone numbers, or incorrect S3 URIs to trigger real errors during development testing.
7. Database Schema and Data Layer
While not strictly required just to send an MMS, a real-world application would likely store information about sent messages.
Potential Schema (using Prisma in schema.prisma
):
// schema.prisma
model MmsMessage {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
destinationPhoneNumber String
originationIdentity String
messageBody String?
mediaUrls String[] // Array of S3 URIs sent
status MmsStatus @default(PENDING) // PENDING, SENT_TO_PROVIDER, DELIVERED, FAILED, etc.
providerMessageId String? @unique // The MessageId from Pinpoint response
errorMessage String? // Store error details if sending failed initially
// Optional: Link to a User or Contact model
// userId String?
// user User? @relation(fields: [userId], references: [id])
}
enum MmsStatus {
PENDING // Request initiated in our system
SENT_TO_PROVIDER // Request accepted by AWS Pinpoint (got MessageId)
DELIVERED // Confirmed delivery (Requires Event Streaming setup)
FAILED // Sending failed (either initial API call or later delivery failure)
// Add other relevant statuses
}
Implementation:
- Add the model to your
schema.prisma
. - Run
yarn rw prisma migrate dev
to apply the changes to your database. - Modify the
sendMms
service function:- Create an
MmsMessage
record in the database before callingpinpointClient.send()
, setting the status toPENDING
. - If the
pinpointClient.send()
call (or retry operation) is successful, update the record with theproviderMessageId
and set the status toSENT_TO_PROVIDER
. - If the call fails, update the record with the
errorMessage
and set the status toFAILED
.
- Create an
// Example snippet within api/src/services/mms/mms.ts
import { db } from 'src/lib/db' // Import Redwood's Prisma client
// ... inside sendMms, before the try block ...
let dbMessage;
try {
dbMessage = await db.mmsMessage.create({
data: {
destinationPhoneNumber: destinationPhoneNumber,
originationIdentity: originationNumber, // Ensure this is available here
messageBody: messageBody,
mediaUrls: mediaUrls,
status: 'PENDING', // Assuming MmsStatus enum is available or use string 'PENDING'
},
});
// ... existing try block containing the pinpointClient.send call ...
// Inside the success path of the try block (after getting response):
await db.mmsMessage.update({
where: { id: dbMessage.id },
data: {
providerMessageId: response.MessageId,
status: 'SENT_TO_PROVIDER', // Assuming MmsStatus enum or use string
},
});
// Inside the catch block:
if (dbMessage) { // Check if initial create succeeded
await db.mmsMessage.update({
where: { id: dbMessage.id },
data: {
status: 'FAILED', // Assuming MmsStatus enum or use string
errorMessage: error.message || 'Unknown AWS error',
},
});
}
// ... rest of catch block (logging, re-throwing error) ...
} catch (error) { // This outer catch handles the initial DB create error
logger.error({ error }, 'Failed to create initial MmsMessage record in DB');
// Decide how to handle this - maybe throw, maybe return specific error
throw new Error(`Database operation failed: ${error.message}`);
}