This guide provides a step-by-step walkthrough for building a production-ready bulk SMS messaging feature within a Next.js application using the Plivo communication platform. We'll cover everything from project setup and core implementation to error handling, security, deployment, and verification.
By the end of this guide, you will have a Next.js application with a secure API endpoint capable of accepting a list of phone numbers and a message, then efficiently sending that message to all recipients via Plivo's bulk messaging capabilities. This solves the common need for applications to send notifications, alerts, or marketing messages to multiple users simultaneously without overwhelming the API or managing individual requests inefficiently.
Project Overview and Goals
-
Goal: Build a Next.js application featuring an API endpoint (
/api/send-bulk
) that sends a single SMS message to multiple phone numbers using Plivo. -
Problem Solved: Provides a scalable and efficient way to broadcast messages, avoiding the complexity and potential rate-limiting issues of sending individual messages in a loop.
-
Technologies:
- Next.js: A popular React framework for building server-rendered and static web applications. We'll use its API routes feature.
- Plivo: A cloud communications platform providing SMS APIs. We'll use their Node.js SDK.
- Node.js: The runtime environment for Next.js and the Plivo SDK.
- (Optional) Prisma & PostgreSQL: For storing and retrieving contact lists (demonstrated but adaptable to other databases/ORMs).
-
Architecture:
+-----------------+ +-----------------------+ +---------------------+ +-------------+ | User / Frontend | ---> | Next.js API Route | ---> | Plivo Node.js SDK | ---> | Plivo API | | (e.g., Admin UI)| | (/api/send-bulk) | | (Bulk Send Logic) | | (SMS Service)| +-----------------+ +-----------------------+ +---------------------+ +-------------+ | ^ | ^ | (Trigger Send) | | (Optional: Fetch Numbers) | | | v | | +---------|-----------------------+ | | | v | +-----------------+ | | Database | | | (e.g., Prisma) | +---------------------------| (Contact Lists) | +-----------------+
(Note: An embedded image diagram would be more professional here if available.)
-
Prerequisites:
- Node.js (v18 or later recommended)
- npm or yarn package manager
- A Plivo account (Sign up at Plivo.com)
- A Plivo phone number capable of sending SMS.
- Basic understanding of JavaScript, React, and Next.js.
- Access to a terminal or command prompt.
1. Setting up the Project
Let's initialize a new Next.js project and install the necessary dependencies.
-
Create Next.js App: Open your terminal and run:
npx create-next-app@latest plivo-bulk-sms-app --typescript --eslint --tailwind --src-dir --app --import-alias ""@/*""
plivo-bulk-sms-app
: You can name your project differently.- We're using TypeScript, ESLint, Tailwind CSS, the
src/
directory, and the App Router for a modern setup, but you can adjust these flags if preferred. - Navigate into the project directory:
cd plivo-bulk-sms-app
-
Install Plivo SDK: Add the official Plivo Node.js helper library.
npm install plivo # or yarn add plivo
-
Install Prisma (Optional, for Contact Management): If you plan to manage contacts in a database, set up Prisma.
npm install prisma @prisma/client --save-dev # or yarn add prisma @prisma/client --dev npx prisma init --datasource-provider postgresql
- This initializes Prisma and configures it for PostgreSQL. Change
postgresql
if you use a different database (e.g.,mysql
,sqlite
). - Update the
DATABASE_URL
in the generated.env
file with your actual database connection string.
- This initializes Prisma and configures it for PostgreSQL. Change
-
Environment Variables: Create a file named
.env.local
in the root of your project. Never commit this file to Git. Add your Plivo credentials and other sensitive configurations:# .env.local # Plivo Credentials (Get from Plivo Console > API Keys: https://console.plivo.com/dashboard/) PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN # Plivo Source Number (Must be a Plivo number enabled for SMS: https://console.plivo.com/numbers/) # Use E.164 format, e.g., +14155551212 PLIVO_SOURCE_NUMBER=+1XXXXXXXXXX # Internal API Key (Generate a strong random string for securing your API endpoint) INTERNAL_API_KEY=YOUR_STRONG_SECRET_API_KEY # Database URL (If using Prisma) # Example for PostgreSQL: postgresql://user:password@host:port/database?schema=public DATABASE_URL=YOUR_DATABASE_CONNECTION_STRING
- Purpose:
PLIVO_AUTH_ID
/PLIVO_AUTH_TOKEN
: Your primary API credentials for authenticating your application with the Plivo API. Obtain these from your Plivo dashboard under ""API Keys"".PLIVO_SOURCE_NUMBER
: The Plivo phone number that will appear as the sender of the SMS messages. Must be SMS-enabled and in E.164 format. Find your numbers under ""Messaging"" > ""Numbers"" in the Plivo console.INTERNAL_API_KEY
: A secret key you'll use to protect your API endpoint from unauthorized access. Generate a secure, random string for this (e.g., usingopenssl rand -base64 32
in your terminal).DATABASE_URL
: The connection string for your database if you are using Prisma. Format depends on the database provider.
- Purpose:
-
Project Structure: Your
src
directory might look like this initially:src/ ├── app/ │ ├── api/ # API routes │ │ ├── send-bulk/ │ │ │ └── route.ts # Our bulk sending endpoint │ │ └── plivo-status-callback/ # Optional: For delivery reports │ │ └── route.ts │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx # Main frontend page (optional for this guide) ├── lib/ # Utility functions, Plivo client setup │ ├── plivo.ts │ ├── plivoService.ts # Core bulk sending logic │ ├── utils.ts # Helper functions (e.g., batching) │ ├── logger.ts # Optional: Structured logger │ └── prisma.ts # Optional: Prisma client instance ├── components/ # React components (optional for this guide) └── prisma/ # Prisma schema and migrations (if using) ├── migrations/ └── schema.prisma
- Architectural Decision: We place API logic in
app/api/
as per Next.js App Router conventions. Shared logic like Plivo client initialization goes intolib/
.
- Architectural Decision: We place API logic in
2. Implementing Core Functionality: The Bulk Send Logic
Plivo's API allows sending to multiple destinations in a single request by providing a <
-delimited string of numbers in the dst
parameter. However_ there's usually a limit on the number of recipients per request (often around 50-100_ check Plivo's current documentation for specifics – we'll assume 50 here). Therefore_ we need to batch our list of numbers.
-
Batching Utility: Create a helper function to split an array into chunks.
// src/lib/utils.ts /** * Splits an array into chunks of a specified size. * @param array The array to split. * @param chunkSize The maximum size of each chunk. * @returns An array of chunks. */ export function chunkArray<T>(array: T[], chunkSize: number): T[][] { const chunks: T[][] = []; for (let i = 0; i < array.length; i += chunkSize) { chunks.push(array.slice(i_ i + chunkSize)); } return chunks; }
- Why: This function allows us to break down a large list of recipients into smaller batches that comply with Plivo's API limits per request.
-
Plivo Client Setup: Initialize the Plivo client using environment variables.
// src/lib/plivo.ts import * as plivo from 'plivo'; const authId = process.env.PLIVO_AUTH_ID; const authToken = process.env.PLIVO_AUTH_TOKEN; if (!authId || !authToken) { throw new Error(""Plivo credentials (PLIVO_AUTH_ID_ PLIVO_AUTH_TOKEN) are not set in environment variables.""); } export const plivoClient = new plivo.Client(authId_ authToken); // console.log(""Plivo client initialized.""); // Optional: Log initialization (consider using a proper logger in production)
- Why: Centralizes Plivo client instantiation_ making it reusable across the application. Reads credentials securely from environment variables. Includes a check to ensure credentials are set.
-
Bulk Send Service Function: Create the core function that handles batching and sending.
// src/lib/plivoService.ts (Create this new file) import { plivoClient } from './plivo'; import { chunkArray } from './utils'; import { MessageCreateResponse } from 'plivo/dist/resources/message'; // Import specific Plivo response type // Potentially import MessageCreateParams if using stricter typing for params // import { MessageCreateParams } from 'plivo/dist/resources/message'; const PLIVO_SOURCE_NUMBER = process.env.PLIVO_SOURCE_NUMBER; const MAX_RECIPIENTS_PER_REQUEST = 50; // Adjust based on current Plivo limits if needed [check Plivo docs] if (!PLIVO_SOURCE_NUMBER) { throw new Error(""PLIVO_SOURCE_NUMBER is not set in environment variables.""); } interface SendBulkSmsResult { success: boolean; batchResults: { batch: string[]; response?: MessageCreateResponse; // Plivo's response type for success error?: any; // Error details if a batch failed }[]; error?: string; // General error message if validation fails early } /** * Sends an SMS message to multiple recipients in batches using Plivo. * @param recipientNumbers An array of phone numbers in E.164 format. * @param message The text message content. * @param deliveryReportUrl Optional URL for receiving delivery status callbacks. * @returns A promise resolving to the results of all batch sends. */ export async function sendBulkSms( recipientNumbers: string[]_ message: string_ deliveryReportUrl?: string ): Promise<SendBulkSmsResult> { if (!recipientNumbers || recipientNumbers.length === 0) { return { success: false, batchResults: [], error: ""Recipient list is empty."" }; } if (!message) { return { success: false, batchResults: [], error: ""Message content is empty."" }; } // Validate all numbers before starting the process const invalidNumbers = recipientNumbers.filter(num => !/^\+\d{10,15}$/.test(num)); if (invalidNumbers.length > 0) { console.error(""Invalid E.164 numbers found:"", invalidNumbers); return { success: false, batchResults: [], error: `Invalid E.164 numbers found: ${invalidNumbers.join(', ')}` }; } const numberBatches = chunkArray(recipientNumbers, MAX_RECIPIENTS_PER_REQUEST); const batchResults: SendBulkSmsResult['batchResults'] = []; let overallSuccess = true; console.log(`Sending message to ${recipientNumbers.length} recipients in ${numberBatches.length} batches.`); for (const batch of numberBatches) { const destinationString = batch.join('<'); // Plivo's delimiter for bulk numbers const batchResult: SendBulkSmsResult['batchResults'][0] = { batch }; try { console.log(`Sending batch to: ${destinationString.substring(0_ 50)}...`); // Log truncated dst // Using 'any' for flexibility_ especially when conditionally adding 'url'. // Refine with specific Plivo types (e.g._ MessageCreateParams from plivo/dist/resources/message) // if the structure is fixed and known. const params: any = { src: PLIVO_SOURCE_NUMBER_ dst: destinationString_ text: message_ }; if (deliveryReportUrl) { params.url = deliveryReportUrl; // Add callback URL if provided params.method = 'POST'; // Recommended method for callbacks } const response = await plivoClient.messages.create(params); console.log(`Batch sent successfully. Plivo Response:`_ response); batchResult.response = response; } catch (error: any) { console.error(`Error sending batch to: ${destinationString.substring(0_ 50)}...`_ error); batchResult.error = error.message || error; overallSuccess = false; // Mark overall success as false if any batch fails } batchResults.push(batchResult); // Optional: Add a small delay between batches to avoid hitting rate limits aggressively // await new Promise(resolve => setTimeout(resolve, 500)); // e.g., 500ms delay } console.log(`Bulk send process completed. Overall Success: ${overallSuccess}`); return { success: overallSuccess, batchResults }; }
- Why:
- Encapsulates the core logic for sending bulk SMS.
- Uses the
chunkArray
utility to divide recipients. - Formats the
dst
parameter correctly using the<
delimiter as required by Plivo's bulk feature. - Iterates through batches and calls
plivoClient.messages.create
for each. - Includes basic input validation and E.164 format check before batching.
- Provides structured results_ indicating success/failure for each batch.
- Includes logging for better observability.
- Handles potential errors during API calls gracefully for each batch.
- Adds an optional
deliveryReportUrl
parameter for status tracking (covered later). - Uses specific Plivo types (
MessageCreateResponse
) for better code safety where applicable.
- Why:
3. Building the API Layer
Now_ let's create the Next.js API route that will expose this functionality.
// src/app/api/send-bulk/route.ts
import { NextResponse } from 'next/server';
import { sendBulkSms } from '@/lib/plivoService';
// import { prisma } from '@/lib/prisma'; // Uncomment if using Prisma
const INTERNAL_API_KEY = process.env.INTERNAL_API_KEY;
// Define the expected structure of the request body
interface RequestBody {
numbers?: string[]; // List of E.164 phone numbers
// contactGroupId?: string; // Alternative: ID to fetch numbers from DB (requires Prisma setup)
message: string;
}
export async function POST(request: Request) {
console.log('Received request on /api/send-bulk');
// 1. Authentication
const authHeader = request.headers.get('Authorization');
if (!INTERNAL_API_KEY) {
console.error('INTERNAL_API_KEY is not set in environment variables.');
return NextResponse.json({ error: 'Internal Server Configuration Error' }_ { status: 500 });
}
if (!authHeader || !authHeader.startsWith('Bearer ') || authHeader.substring(7) !== INTERNAL_API_KEY) {
console.warn('Unauthorized attempt to access /api/send-bulk');
return NextResponse.json({ error: 'Unauthorized' }_ { status: 401 });
}
try {
// 2. Request Body Parsing and Validation
const body: RequestBody = await request.json();
const { numbers_ message /*_ contactGroupId */ } = body;
if (!message || typeof message !== 'string' || message.trim() === '') {
return NextResponse.json({ error: 'Invalid request: message is required and must be a non-empty string.' }_ { status: 400 });
}
let recipientNumbers: string[] = [];
// --- Option A: Direct Number List ---
if (numbers) {
if (!Array.isArray(numbers)) {
return NextResponse.json({ error: 'Invalid request: numbers must be an array.' }_ { status: 400 });
}
// Basic E.164 format check - more robust check happens in plivoService
const potentiallyInvalid = numbers.filter(num => typeof num !== 'string' || !num.startsWith('+'));
if (potentiallyInvalid.length > 0) {
return NextResponse.json({ error: 'Invalid request: numbers must be an array of strings potentially in E.164 format (e.g., +12223334444).' }, { status: 400 });
}
recipientNumbers = numbers;
}
// --- Option B: Fetch from Database (Example using Prisma - requires setup in Section 6) ---
/*
else if (contactGroupId) {
console.log(`Fetching numbers for contact group ID: ${contactGroupId}`);
// Ensure Prisma client is available
// if (!prisma) {
// console.error('Prisma client is not available.');
// return NextResponse.json({ error: 'Database configuration error.' }, { status: 500 });
// }
try {
const groupWithContacts = await prisma.contactGroup.findUnique({
where: { id: contactGroupId },
include: { contacts: { select: { phone_number: true } } },
});
if (!groupWithContacts) {
return NextResponse.json({ error: `Contact group with ID ${contactGroupId} not found.` }, { status: 404 });
}
// Ensure phone numbers are valid before adding
recipientNumbers = groupWithContacts.contacts
.map(c => c.phone_number)
.filter(num => /^\+\d{10,15}$/.test(num)); // Validate here too
console.log(`Fetched ${recipientNumbers.length} valid numbers for group ${contactGroupId}`);
} catch (dbError: any) {
console.error(""Database error fetching contact group:"", dbError);
return NextResponse.json({ error: 'Failed to retrieve contacts from database.' }, { status: 500 });
}
}
*/
else {
// If neither numbers nor contactGroupId is provided (and contactGroupId logic is enabled)
return NextResponse.json({ error: 'Invalid request: Either ""numbers"" array or ""contactGroupId"" must be provided.' }, { status: 400 });
}
if (recipientNumbers.length === 0) {
return NextResponse.json({ error: 'No valid recipient numbers found or provided.' }, { status: 400 });
}
// 3. Call the Bulk Send Service
console.log(`Initiating bulk send to ${recipientNumbers.length} numbers.`);
// Construct the absolute URL for the delivery report callback (if needed)
// Ensure VERCEL_URL is set in your Vercel project settings for production/preview
const baseUrl = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : 'http://localhost:3000';
const deliveryReportUrl = `${baseUrl}/api/plivo-status-callback`; // Define your callback endpoint
const result = await sendBulkSms(recipientNumbers, message.trim(), deliveryReportUrl);
// 4. Return Response
if (result.success) {
console.log('Bulk send successful.');
// Avoid sending detailed Plivo responses back to the client unless necessary
return NextResponse.json({ message: 'Bulk message sending process initiated successfully.', batchCount: result.batchResults.length }, { status: 200 });
} else {
console.error('Bulk send process completed with errors.');
// Log detailed batchResults errors for server-side debugging
// Consider what level of detail to return to the client
return NextResponse.json({
error: 'Bulk message sending process encountered errors.',
details: result.error, // General error or batch-specific errors summarized
failedBatchCount: result.batchResults.filter(r => r.error).length
}, { status: result.error?.includes(""Invalid E.164"") ? 400 : 500 }); // Return 400 for input errors, 500 otherwise
}
} catch (error: any) {
console.error('Error in /api/send-bulk handler:', error);
if (error instanceof SyntaxError) { // JSON parsing error
return NextResponse.json({ error: 'Invalid JSON payload.' }, { status: 400 });
}
return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 });
}
}
// Optional: Add GET or other methods if needed, otherwise they default to 405 Method Not Allowed
export async function GET() {
return NextResponse.json({ error: 'Method Not Allowed' }, { status: 405 });
}
-
Why:
- Authentication: Protects the endpoint using the
INTERNAL_API_KEY
via theAuthorization: Bearer <key>
header. Checks if the key is configured. - Validation: Checks the request body for required fields (
message
) and validates the format ofnumbers
(array of strings, basic format check). Provides clear error messages for bad requests. - Data Fetching (Optional): Includes commented-out example logic for fetching numbers from a database using Prisma based on a
contactGroupId
. Added comments about dependencies. - Service Call: Delegates the actual sending logic to the
sendBulkSms
function inplivoService.ts
. - Response Handling: Returns appropriate HTTP status codes (200, 400, 401, 500) and JSON responses based on the outcome. Returns less detailed success/error info to the client for security/simplicity.
- Callback URL: Constructs an absolute URL for Plivo delivery reports, essential for tracking status (using Vercel environment variables or localhost).
- Authentication: Protects the endpoint using the
-
Testing with
curl
:curl -X POST http://localhost:3000/api/send-bulk \ -H ""Content-Type: application/json"" \ -H ""Authorization: Bearer YOUR_STRONG_SECRET_API_KEY"" \ -d '{ ""numbers"": [""+12223334444"", ""+15556667777""], ""message"": ""Hello from our Next.js bulk sender! (Test)"" }'
(Replace
YOUR_STRONG_SECRET_API_KEY
and use valid E.164 phone numbers like +12223334444 for testing)Expected Success Response (JSON):
{ ""message"": ""Bulk message sending process initiated successfully."", ""batchCount"": 1 }
(Status Code: 200)
Expected Error Response (e.g., Unauthorized - JSON):
{ ""error"": ""Unauthorized"" }
(Status Code: 401)
Expected Error Response (e.g., Bad Request - JSON):
{ ""error"": ""Invalid request: message is required and must be a non-empty string."" }
(Status Code: 400)
4. Integrating with Plivo
This section focuses on the specifics of the Plivo integration itself.
-
Configuration: As covered in Section 1 (Setup), the essential configuration happens via environment variables (
.env.local
):PLIVO_AUTH_ID
: Your Plivo Account Auth ID.PLIVO_AUTH_TOKEN
: Your Plivo Account Auth Token.PLIVO_SOURCE_NUMBER
: Your Plivo SMS-enabled number.- How to Obtain:
- Log in to your Plivo Console (https://console.plivo.com/).
- Auth ID & Token: Navigate to the main Dashboard page. Your Auth ID and Token are displayed prominently at the top right. Click the ""eye"" icon to reveal the token.
- Source Number: Navigate to ""Messaging"" -> ""Numbers"". Ensure you have a number listed here. If not, you may need to rent one. Copy the number exactly as shown, including the
+
and country code (E.164 format).
- Security: Store these only in
.env.local
(or your deployment environment's secret management) and ensure.env.local
is listed in your.gitignore
file.
-
SDK Initialization: Covered in Section 2 (Core Functionality) in
src/lib/plivo.ts
. Theplivo.Client
is initialized using the environment variables. -
API Call: The core interaction occurs in
src/lib/plivoService.ts
within thesendBulkSms
function:// Inside sendBulkSms function const response = await plivoClient.messages.create({ src: PLIVO_SOURCE_NUMBER, dst: destinationString, // The '<'-delimited string text: message_ url: deliveryReportUrl_ // Optional callback URL method: 'POST'_ // Recommended for callbacks // Other potential params from Plivo SDK if needed });
src
: Your Plivo sending number.dst
: The crucial parameter for bulk sending – multiple E.164 numbers joined by<
.text
: The message content.url
: The publicly accessible endpoint in your application where Plivo will send status updates (delivery reports) for each message sent in the batch.method
: The HTTP method Plivo should use to call yoururl
(POST is recommended as it sends data in the body).
-
Fallback Mechanisms: The current implementation relies solely on Plivo. For critical messages_ true fallback would involve:
- Monitoring: Actively check Plivo's status page or API health endpoints.
- Alternative Provider: Have a secondary SMS provider configured.
- Logic: If
plivoClient.messages.create
fails consistently (e.g._ multiple retries fail_ or Plivo status indicates an outage)_ trigger sending via the alternative provider. This adds significant complexity and cost_ usually reserved for high-availability requirements. This guide does not implement a fallback provider.
5. Error Handling_ Logging_ and Retry Mechanisms
Robust error handling is crucial for a production system.
-
Error Handling Strategy:
- API Route (
route.ts
): Usetry...catch
blocks to capture errors during request processing_ validation_ database access (if applicable)_ and calls to thesendBulkSms
service. Return appropriate HTTP status codes (4xx for client errors_ 5xx for server errors) with informative JSON error messages. Check for specific error types (like JSON parsing errors). - Service Layer (
plivoService.ts
): Usetry...catch
within the loop for each batch send. This allows the process to continue even if one batch fails. Log errors for each failed batch and aggregate the results. Return a clear success/failure status along with detailed batch results. Perform input validation early. - Plivo Client (
plivo.ts
): Throw an error on initialization if credentials are missing.
- API Route (
-
Logging:
- Current: Uses basic
console.log
andconsole.error
. Sufficient for development. - Production: Integrate a structured logging library like
pino
orwinston
.Create a logger instance:npm install pino pino-pretty # pino-pretty for development formatting # or yarn add pino pino-pretty
Replace// src/lib/logger.ts (Example - create this file) import pino from 'pino'; const logger = pino({ level: process.env.LOG_LEVEL || 'info'_ transport: process.env.NODE_ENV !== 'production' ? { target: 'pino-pretty'_ options: { colorize: true } } // Pretty print in dev : undefined_ // Default JSON in prod for log ingestion systems }); export default logger;
console.log/error
withlogger.info/warn/error
throughout the application (e.g._ inroute.ts
_plivoService.ts
). Structured logs (JSON format in production) are easier to parse_ filter_ and analyze with log management tools (e.g._ Datadog_ Logtail_ Loki).- Log Levels: Use appropriate levels:
info
for routine operations (API calls_ batch starts)_warn
for potential issues (retries_ unexpected conditions)_error
for failures (API errors_ exceptions). - Log Content: Include relevant context like request IDs (if available)_ batch numbers_ error messages_ and Plivo API responses/errors.
- Log Levels: Use appropriate levels:
- Current: Uses basic
-
Retry Mechanisms:
- Concept: Network issues or temporary Plivo problems might cause API calls to fail. Retrying can improve reliability for transient errors.
- Simple Retry Implementation: Modify the
sendBulkSms
batch loop to include retries with backoff. (This replaces the simpletry/catch
block within the loop):// Inside the for...of loop in sendBulkSms (replace the simple try/catch) const maxAttempts = 3; // Max attempts per batch let success = false; let response: MessageCreateResponse | undefined; let lastError: any; for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { // logger.info({ batch_ attempt }_ `Attempt ${attempt} to send batch.`); // Use logger console.log(`Attempt ${attempt} for batch: ${destinationString.substring(0_ 50)}...`); const params: any = { /* ... as before ... */ }; if (deliveryReportUrl) { /* ... as before ... */ } response = await plivoClient.messages.create(params); success = true; batchResult.response = response; // logger.info({ batch_ attempt_ response }_ `Batch sent successfully on attempt ${attempt}.`); console.log(`Batch sent successfully on attempt ${attempt}. Plivo Response:`_ response); break; // Exit retry loop on success } catch (error: any) { lastError = error; // logger.warn({ batch_ attempt_ error: error.message }_ `Attempt ${attempt} failed for batch.`); console.warn(`Attempt ${attempt} failed for batch: ${destinationString.substring(0_ 50)}... Error: ${error.message}`); if (attempt < maxAttempts) { const delay = 1000 * Math.pow(2_ attempt - 1); // Exponential backoff (1s_ 2s) // logger.info({ batch_ attempt_ delay }_ `Waiting ${delay}ms before next attempt.`); console.log(`Waiting ${delay}ms before next attempt.`); await new Promise(resolve => setTimeout(resolve, delay)); } } } // End of retry loop if (!success) { // logger.error({ batch, error: lastError?.message || lastError }, `Batch failed after ${maxAttempts} attempts.`); console.error(`Batch failed after ${maxAttempts} attempts: ${destinationString.substring(0, 50)}... Last Error: ${lastError?.message}`); batchResult.error = lastError?.message || lastError; overallSuccess = false; } batchResults.push(batchResult); // Push result regardless of success/failure
- Exponential Backoff: The example uses simple exponential backoff (1s, 2s). Adding jitter (randomness) to the delay can be beneficial in high-concurrency scenarios. Libraries like
async-retry
can simplify complex retry logic. - Caveat: Be cautious with retries for sending messages. If a request partially succeeded or timed out but Plivo did process it, retrying could lead to duplicate messages. Check Plivo's API idempotency guarantees or implement your own request tracking using unique IDs if duplicates are critical to avoid. Plivo's
messageUuid
helps track results but doesn't prevent duplicates if the create call is retried after initial success but before the response is received.
-
Testing Error Scenarios:
- Invalid Credentials: Temporarily change
PLIVO_AUTH_ID
/PLIVO_AUTH_TOKEN
in.env.local
. Expect Plivo errors (likely HTTP 401) caught inplivoService.ts
. - Invalid Source Number: Use a non-Plivo number or incorrectly formatted number for
PLIVO_SOURCE_NUMBER
. Expect Plivo errors. - Invalid Destination Number: Include poorly formatted numbers (e.g.,
+123
) or numbers known to be invalid in thenumbers
array. Expect a 400 error from the API route due to validation, or specific Plivo errors/failure statuses in delivery reports if they pass initial validation but fail at Plivo. - Insufficient Funds: If your Plivo account balance is too low. Expect Plivo API errors.
- Network Issues: Simulate network drops locally (e.g., disconnect Wi-Fi briefly during a
curl
request) or use tools likeiptables
(Linux) or network link conditioners (macOS) to introduce packet loss or latency. Test if retries handle transient failures. - Rate Limiting: Send many requests in quick succession (you might need a script for this). Observe if Plivo returns 429 Too Many Requests errors and how your application handles them (ideally, the retry logic with backoff should help).
- Invalid Credentials: Temporarily change