Leverage the power of Fastify's performance and Plivo's efficient bulk messaging API to build a scalable service capable of sending a single SMS message to thousands of recipients with a single API request. This guide provides a complete walkthrough, from project setup to deployment and monitoring.
We'll build a simple but robust API endpoint that accepts a list of phone numbers and a message, then utilizes Plivo to broadcast the SMS. This approach is significantly more efficient than sending individual messages in a loop, reducing latency and API call overhead.
Project Overview and Goals
-
Problem: Sending the same SMS message to numerous recipients individually is slow, inefficient, and can easily hit API rate limits.
-
Solution: Create a Fastify API endpoint (
POST /broadcast
) that accepts a list of destination phone numbers and a message body. This endpoint will use the Plivo Node.js SDK to send the message to all recipients via Plivo's bulk messaging feature (a single API call). -
Technologies:
- Node.js: The runtime environment.
- Fastify: A high-performance, low-overhead Node.js web framework. Chosen for its speed, extensibility, and built-in validation.
- Plivo Node.js SDK: Simplifies interaction with the Plivo REST API.
dotenv
&fastify-env
: For managing environment variables securely and reliably.
-
Architecture:
+-----------------+ +---------------------+ +-------------+ +-----------------+ | Client (e.g. UI,| ---> | Fastify API Server | ---> | Plivo API | ---> | SMS Recipients | | Curl, Postman) | | (POST /broadcast) | | (Bulk Send) | | (Mobile Phones) | +-----------------+ +---------------------+ +-------------+ +-----------------+ | | Uses Plivo SDK | Reads Env Vars (.env) | Logs Events/Errors
-
Outcome: A functional Fastify application with a single endpoint (
/broadcast
) capable of accepting bulk SMS requests and dispatching them via Plivo. -
Prerequisites:
- Node.js (v18 or later recommended) and npm/yarn.
- A Plivo account (Sign up at Plivo).
- A Plivo phone number capable of sending SMS (purchased or verified within your Plivo console).
- Your Plivo Auth ID and Auth Token (found in the Plivo console).
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies. These instructions are OS-agnostic.
-
Create Project Directory:
mkdir fastify-plivo-broadcaster cd fastify-plivo-broadcaster
-
Initialize npm Project:
npm init -y
This creates a
package.json
file. -
Install Dependencies:
fastify
: The core web framework.plivo
: The official Plivo Node.js SDK.dotenv
: Loads environment variables from a.env
file.fastify-env
: Schema-based environment variable validation for Fastify.
npm install fastify plivo dotenv fastify-env
-
Install Development Dependencies (Optional but Recommended):
nodemon
: Automatically restarts the server on file changes during development.pino-pretty
: Formats Pino logs for readability during development.
npm install --save-dev nodemon pino-pretty
-
Configure
package.json
Scripts: Openpackage.json
and add/modify thescripts
section:{ ""name"": ""fastify-plivo-broadcaster"", ""version"": ""1.0.0"", ""description"": """", ""main"": ""src/server.js"", ""scripts"": { ""start"": ""node src/server.js"", ""dev"": ""nodemon src/server.js | pino-pretty"", ""test"": ""echo \""Error: no test specified\"" && exit 1"" }, ""keywords"": [], ""author"": """", ""license"": ""ISC"", ""dependencies"": { ""dotenv"": ""^..."", ""fastify"": ""^..."", ""fastify-env"": ""^..."", ""plivo"": ""^..."" }, ""devDependencies"": { ""nodemon"": ""^..."", ""pino-pretty"": ""^..."" } }
(Note: The
dev
script pipes output topino-pretty
for readable logs during development). -
Create Project Structure: Organize your code for clarity and maintainability.
mkdir src mkdir src/routes mkdir src/services touch src/server.js touch src/routes/broadcast.js touch src/services/plivoService.js touch .env touch .gitignore
src/
: Contains all source code.src/routes/
: Holds route definitions.src/services/
: Contains business logic interacting with external services (like Plivo).src/server.js
: The main application entry point..env
: Stores sensitive credentials and configuration (DO NOT commit this file)..gitignore
: Specifies files/directories to be ignored by Git.
-
Configure
.gitignore
: Add the following lines to your.gitignore
file to prevent committing sensitive information and unnecessary files:# .gitignore node_modules .env npm-debug.log *.log
-
Set Up Environment Variables (
.env
): Open the.env
file and add your Plivo credentials and server configuration.PLIVO_AUTH_ID
: Your Plivo Account Auth ID. Find this on the overview page of your Plivo Console.PLIVO_AUTH_TOKEN
: Your Plivo Account Auth Token. Also on the overview page.PLIVO_SOURCE_NUMBER
: A Plivo phone number you own (in E.164 format, e.g.,+14155552671
) enabled for SMS. Find this under Phone Numbers -> Your Numbers in the console.HOST
: The host address the server should listen on (e.g.,0.0.0.0
to listen on all available network interfaces, or127.0.0.1
for localhost only).PORT
: The port number for the server (e.g.,3000
).LOG_LEVEL
: Controls logging verbosity (e.g.,info
,debug
,warn
,error
). Fastify uses Pino logger.
# .env PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN PLIVO_SOURCE_NUMBER=+1XXXXXXXXXX HOST=0.0.0.0 PORT=3000 LOG_LEVEL=info
Replace the placeholder values with your actual Plivo credentials and desired settings.
-
Initialize Basic Fastify Server (
src/server.js
): Set up the basic server structure and configurefastify-env
to load and validate our environment variables.// src/server.js 'use strict'; // Load environment variables from .env file first require('dotenv').config(); const Fastify = require('fastify'); const fastifyEnv = require('fastify-env'); const broadcastRoutes = require('./routes/broadcast'); // Define schema for environment variables const envSchema = { type: 'object', required: [ 'PORT', 'HOST', 'PLIVO_AUTH_ID', 'PLIVO_AUTH_TOKEN', 'PLIVO_SOURCE_NUMBER', ], properties: { PORT: { type: 'string', default: 3000 }, HOST: { type: 'string', default: '0.0.0.0' }, PLIVO_AUTH_ID: { type: 'string' }, PLIVO_AUTH_TOKEN: { type: 'string' }, PLIVO_SOURCE_NUMBER: { type: 'string', pattern: '^\\+[1-9]\\d{1,14}$' // E.164 format regex }, LOG_LEVEL: { type: 'string', default: 'info' }, // Add other env vars here if needed }, }; // Determine logger options based on NODE_ENV // Use pino-pretty only in development const loggerConfig = process.env.NODE_ENV === 'production' ? { level: process.env.LOG_LEVEL || 'info' } // Production: JSON logs : { // Development: Pretty logs level: process.env.LOG_LEVEL || 'info', transport: { target: 'pino-pretty', options: { translateTime: 'HH:MM:ss Z', ignore: 'pid,hostname', }, }, }; // Initialize Fastify with logging options const fastify = Fastify({ logger: loggerConfig }); const startServer = async () => { try { // Register fastify-env plugin await fastify.register(fastifyEnv, { schema: envSchema, dotenv: true, // Load .env file using dotenv }); // Make Plivo credentials available across the app via decoration // This prevents needing to pass `fastify.config` everywhere fastify.decorate('plivoConfig', { authId: fastify.config.PLIVO_AUTH_ID, authToken: fastify.config.PLIVO_AUTH_TOKEN, sourceNumber: fastify.config.PLIVO_SOURCE_NUMBER, }); // Register application routes fastify.register(broadcastRoutes, { prefix: '/api/v1' }); // Add a version prefix // Basic health check route fastify.get('/health', async (request, reply) => { return { status: 'ok', timestamp: new Date().toISOString() }; }); // Start the server await fastify.listen({ port: parseInt(fastify.config.PORT, 10), host: fastify.config.HOST, }); fastify.log.info(`Server dependencies loaded successfully.`); } catch (err) { fastify.log.error(err); process.exit(1); } }; startServer();
- We use
dotenv
to load the.env
file before Fastify initializes fully. fastify-env
validates the loaded environment variables againstenvSchema
. This ensures critical configuration is present and correctly formatted (like the E.164 pattern for the phone number).- We decorate the
fastify
instance withplivoConfig
to make credentials easily accessible in services/routes without explicit passing. - A simple
/health
endpoint is added for monitoring. - Routes are registered with a
/api/v1
prefix for better API versioning. - Logging is configured using Pino. Crucially,
pino-pretty
is conditionally used only whenNODE_ENV
is notproduction
, ensuring readable logs in development and efficient JSON logs in production.
- We use
-
Run the Server (Development):
npm run dev
You should see pretty-printed output indicating the server is listening on the configured host and port (e.g.,
http://0.0.0.0:3000
). Iffastify-env
encounters missing or invalid variables, it will throw an error. To run in production mode (for JSON logs):NODE_ENV=production npm start
.
2. Implementing Core Functionality (Plivo Service)
Now, let's create the service responsible for interacting with the Plivo API.
-
Create Plivo Service (
src/services/plivoService.js
): This service initializes the Plivo client and exposes a function to send bulk messages.// src/services/plivoService.js 'use strict'; const plivo = require('plivo'); let plivoClient; // Singleton client instance /** * Initializes the Plivo client using configuration from the Fastify instance. * Should be called once during application startup or on first use. * @param {object} config - Plivo configuration object { authId, authToken, sourceNumber } * @param {object} logger - Fastify logger instance */ function initializePlivoClient(config, logger) { if (!plivoClient) { if (!config || !config.authId || !config.authToken) { logger.error('Plivo Auth ID or Auth Token is missing in config.'); throw new Error('Plivo credentials are required for initialization.'); } try { plivoClient = new plivo.Client(config.authId, config.authToken); logger.info('Plivo client initialized successfully.'); } catch (error) { logger.error({ err: error }, 'Failed to initialize Plivo client'); throw error; // Re-throw to prevent application start if client fails } } return plivoClient; } /** * Sends a single SMS message to multiple recipients using Plivo's bulk messaging. * @param {string[]} destinations - An array of destination phone numbers in E.164 format. * @param {string} message - The text message content. * @param {object} config - Plivo configuration object { authId, authToken, sourceNumber } * @param {object} logger - Fastify logger instance * @returns {Promise<object>} - The Plivo API response object. * @throws {Error} - If Plivo API call fails or input is invalid. */ async function sendBulkSms(destinations, message, config, logger) { // Ensure client is initialized (idempotent) initializePlivoClient(config, logger); if (!Array.isArray(destinations) || destinations.length === 0) { logger.warn('sendBulkSms called with invalid or empty destinations array.'); throw new Error('Destinations array cannot be empty.'); } if (!message || typeof message !== 'string' || message.trim() === '') { logger.warn('sendBulkSms called with invalid or empty message.'); throw new Error('Message content cannot be empty.'); } if (!config.sourceNumber) { logger.error('Plivo source number is missing from configuration.'); throw new Error('Plivo source number is required.'); } // Plivo expects destination numbers separated by '<' // Filter out potential duplicates before joining const uniqueDestinations = [...new Set(destinations)]; const plivoDestinationString = uniqueDestinations.join('<'); // Basic check for Plivo's potential limit (check their docs for current limits) const MAX_RECIPIENTS_PER_REQUEST = 100; // Example limit - ADJUST BASED ON PLIVO DOCS if (uniqueDestinations.length > MAX_RECIPIENTS_PER_REQUEST) { logger.warn(`Attempted to send to ${uniqueDestinations.length} recipients, exceeding the example limit of ${MAX_RECIPIENTS_PER_REQUEST}. The current implementation does NOT automatically batch requests. Request may fail if Plivo enforces this limit.`); // Depending on requirements, you might throw an error or implement batching logic here. // throw new Error(`Cannot send to more than ${MAX_RECIPIENTS_PER_REQUEST} recipients in a single request without batching.`); } logger.info( `Attempting to send bulk SMS via Plivo to ${uniqueDestinations.length} unique recipients.`, ); try { const response = await plivoClient.messages.create( config.sourceNumber, // src: Your Plivo number plivoDestinationString, // dst: Recipients separated by '<' message_ // text: The message content // Optional parameters (e.g._ method_ url for status callbacks) // { // method: 'POST'_ // Method for status callbacks // url: 'https://your-app.com/plivo/status-callback' // Your callback URL // } ); logger.info( { message_uuid: response.messageUuid_ api_id: response.apiId_ recipient_count: uniqueDestinations.length_ }_ 'Plivo bulk message request successful.'_ ); // The response contains message UUIDs for tracking_ but not individual delivery status here. // Use Webhooks for detailed status updates. return response; } catch (error) { logger.error( { err: error_ plivoError: error.message_ // Plivo SDK often puts useful info here statusCode: error.statusCode_ // Plivo SDK might add statusCode }_ 'Plivo API error during bulk message send.'_ ); // Re-throw a generic error or a custom application error throw new Error(`Failed to send bulk SMS via Plivo: ${error.message}`); } } module.exports = { initializePlivoClient_ // Export initializer if needed elsewhere_ though route will handle it sendBulkSms_ };
- We use a module-level variable
plivoClient
to act as a singleton – the client is initialized only once. - The
initializePlivoClient
function handles the creation_ taking config and logger from Fastify. sendBulkSms
performs input validation (non-empty destinations array_ non-empty message).- It filters duplicate numbers using
new Set()
. Crucially_ it joins the unique destination numbers with the<
delimiter as required by Plivo's bulk API. - It includes a check against a hypothetical recipient limit per request. You must consult the official Plivo documentation for the current accurate limit and adjust the
MAX_RECIPIENTS_PER_REQUEST
constant. Note: The warning message now explicitly states that automatic batching is not implemented in this code. - It calls
plivoClient.messages.create
with the source number_ the<
-separated destination string_ and the message text. - Successful responses and errors from the Plivo API are logged appropriately.
- Errors are caught and re-thrown to be handled by the route.
- We comment out the optional
url
parameter for status callbacks – implementing webhooks is a vital next step for production monitoring but beyond this initial setup scope.
- We use a module-level variable
3. Building the API Layer (Broadcast Route)
Let's create the Fastify route that exposes our bulk sending functionality.
-
Define Route and Schema (
src/routes/broadcast.js
): This file defines thePOST /broadcast
endpoint_ validates the incoming request body_ and calls the Plivo service.// src/routes/broadcast.js 'use strict'; const { sendBulkSms_ initializePlivoClient } = require('../services/plivoService'); // Define the schema for the request body and response const broadcastSchema = { body: { type: 'object'_ required: ['destinations'_ 'message']_ properties: { destinations: { type: 'array'_ minItems: 1_ items: { type: 'string'_ // Basic E.164 format validation pattern: '^\\+[1-9]\\d{1_14}$'_ }_ description: 'Array of recipient phone numbers in E.164 format (e.g._ +14155552671)'_ }_ message: { type: 'string'_ minLength: 1_ maxLength: 1600_ // Standard SMS limit consideration (Plivo handles segmentation) description: 'The text message content to send.'_ }_ }_ }_ response: { 200: { // Successful response type: 'object'_ properties: { message: { type: 'string' }_ message_uuid: { type: 'array'_ items: { type: 'string' } }_ // Plivo returns UUIDs array api_id: { type: 'string' }_ recipient_count: { type: 'integer' }_ }_ }_ 400: { // Bad request (validation failed) type: 'object'_ properties: { statusCode: { type: 'integer' }_ error: { type: 'string' }_ message: { type: 'string' }_ }_ }_ 500: { // Server error (e.g._ Plivo API failed) type: 'object'_ properties: { statusCode: { type: 'integer' }_ error: { type: 'string' }_ message: { type: 'string' }_ }_ }_ // Add other status codes like 429 for Rate Limiting if implemented }_ }; async function broadcastRoutes(fastify_ options) { // Ensure Plivo client is initialized when this route plugin is loaded // Access config decorated onto fastify instance in server.js initializePlivoClient(fastify.plivoConfig_ fastify.log); fastify.post('/broadcast'_ { schema: broadcastSchema }_ async (request_ reply) => { const { destinations, message } = request.body; const requestId = request.id; // Fastify generates a unique ID for each request fastify.log.info( `[${requestId}] Received broadcast request for ${destinations.length} destinations.`, ); try { // Call the Plivo service function const plivoResponse = await sendBulkSms( destinations, message, fastify.plivoConfig, // Pass Plivo config from fastify instance request.log // Pass request-specific logger for better context ); // Send success response back to the client reply.code(200).send({ message: plivoResponse.message, // e.g., ""message(s) queued"" message_uuid: plivoResponse.messageUuid, api_id: plivoResponse.apiId, recipient_count: destinations.length, // Or use unique count if preferred }); fastify.log.info( `[${requestId}] Broadcast request processed successfully. Plivo API ID: ${plivoResponse.apiId}`, ); } catch (error) { fastify.log.error( `[${requestId}] Error processing broadcast request: ${error.message}`, { err: error } // Log the full error object ); // Determine appropriate status code (could refine based on error type) // If it's a known Plivo client-side error (e.g., validation) maybe 400? // Otherwise, assume 500 for server/Plivo issues. // For now, defaulting to 500 for simplicity. reply.code(500).send({ statusCode: 500, error: 'Internal Server Error', message: `Failed to send broadcast: ${error.message}`, // Provide some context }); } }); // You can add more routes here if needed } module.exports = broadcastRoutes;
- We define a
broadcastSchema
using Fastify's schema validation capabilities. This automatically validates therequest.body
against the defined structure, types, and constraints (likeminItems
,minLength
, and the E.164pattern
). If validation fails, Fastify automatically sends a 400 Bad Request response. - The schema also defines expected response formats for different status codes (200, 400, 500).
- The route handler extracts
destinations
andmessage
from the validatedrequest.body
. - It calls the
sendBulkSms
service function, passing the necessary data, the Plivo configuration (fastify.plivoConfig
), and the request-specific logger (request.log
) for contextual logging. - It handles success by sending a 200 response with relevant details from the Plivo API response.
- It handles errors caught from the service layer by logging them and sending a 500 Internal Server Error response.
- We define a
-
Register Route in Server: Ensure the route is registered in
src/server.js
(already done in Step 1.9). The linefastify.register(broadcastRoutes, { prefix: '/api/v1' });
handles this. -
Test the Endpoint: Restart your server (
npm run dev
). You can now test the endpoint usingcurl
or a tool like Postman.Using
curl
: Replace placeholders with your actual server address, valid E.164 numbers (use your own test numbers!), and a message.curl -X POST http://localhost:3000/api/v1/broadcast \ -H ""Content-Type: application/json"" \ -d '{ ""destinations"": [""+14155551234"", ""+14155555678"", ""+14155551234""], ""message"": ""Hello from the Fastify Bulk Broadcaster! Test message."" }'
Expected Success Response (Example):
{ ""message"": ""message(s) queued"", ""message_uuid"": [ ""e8f3a0ee-a1d7-11eb-8f9c-0242ac110002"", ""e8f3a3a8-a1d7-11eb-8f9c-0242ac110002"" ], ""api_id"": ""f9b3aabc-a1d7-11eb-b3e0-0242ac110003"", ""recipient_count"": 3 }
(Note: Plivo's actual response structure for
message_uuid
might vary slightly; consult their documentation. The service filters duplicates before sending, so only 2 unique numbers are sent here).Example Error Response (Validation Failure): If you send invalid data (e.g., missing
message
or invalid phone number format):{ ""statusCode"": 400, ""error"": ""Bad Request"", ""message"": ""body/destinations/0 must match pattern \""^\\\\+[1-9]\\\\d{1,14}$\"""" }
Example Error Response (Server/Plivo Error): If Plivo credentials are wrong or the API call fails:
{ ""statusCode"": 500, ""error"": ""Internal Server Error"", ""message"": ""Failed to send broadcast: Error: Authentication credentials invalid. Please check your auth_id and auth_token."" }
4. Integrating with Plivo (Credentials & Setup)
We've already integrated the SDK, but let's explicitly cover obtaining and securing credentials.
-
Obtain Plivo Auth ID and Auth Token:
- Log in to your Plivo Console (https://console.plivo.com/).
- On the main Dashboard/Overview page, you will find your Auth ID and Auth Token.
- Copy these values carefully.
-
Obtain Plivo Source Number:
- Navigate to Messaging -> Phone Numbers in the Plivo Console sidebar.
- If you don't have a number, you'll need to buy one capable of sending SMS messages to your target regions.
- Copy the Number in E.164 format (e.g.,
+14155552671
).
-
Secure Storage:
- As done in Step 1.8, place these credentials only in your
.env
file:# .env PLIVO_AUTH_ID=YOUR_PLIVO_AUTH_ID PLIVO_AUTH_TOKEN=YOUR_PLIVO_AUTH_TOKEN PLIVO_SOURCE_NUMBER=+1XXXXXXXXXX # ... other vars
- Crucially: Ensure
.env
is listed in your.gitignore
file to prevent accidentally committing secrets to version control. - In production environments, use your hosting provider's mechanism for managing secrets (e.g., AWS Secrets Manager, Kubernetes Secrets, environment variables injected by the platform).
- As done in Step 1.8, place these credentials only in your
-
Fallback Mechanisms:
- The current implementation relies directly on Plivo. For high availability, consider:
- Retry Logic: Implement application-level retries with exponential backoff (see Section 5) for transient network errors before calling Plivo.
- Secondary Provider: Abstract the SMS sending logic further. If Plivo fails consistently (e.g., platform outage), you could potentially switch to a different SMS provider API. This adds significant complexity. For this guide, we focus on robust error handling with Plivo.
- The current implementation relies directly on Plivo. For high availability, consider:
5. Error Handling, Logging, and Retry Mechanisms
We've built basic error handling and logging. Let's enhance it.
-
Consistent Error Handling:
- Service Layer (
plivoService.js
): Catches specific Plivo API errors, logs detailed information (including Plivo error messages/codes if available), and throws a standardizedError
object. - Route Layer (
broadcast.js
): Catches errors propagated from the service layer. Logs the error with request context (request.id
,request.log
). Sends an appropriate HTTP status code (4xx for client errors it can detect, 5xx for server/dependency errors) and a generic error message to the client, avoiding leaking internal details. Fastify's schema validation automatically handles 400 errors for invalid input. - Global Error Handler (Optional): For unhandled exceptions, Fastify allows setting a global error handler using
fastify.setErrorHandler((error, request, reply) => { ... })
. This can catch unexpected errors and ensure a consistent error response format.
- Service Layer (
-
Logging:
- Levels: Use
LOG_LEVEL
env var (info
,debug
,warn
,error
,fatal
) to control verbosity.info
is good for production,debug
for development. - Context: Fastify automatically injects
reqId
into logs. We passrequest.log
to the service layer (sendBulkSms
) so service-level logs also contain the request ID, making it easy to trace a single request's lifecycle through logs. - Format: As configured in Section 1,
pino-pretty
is used for development readability. In production (NODE_ENV=production
), output defaults to JSON logs for easier parsing by log aggregation systems (e.g., ELK stack, Datadog, Loki). - What to Log:
- Request received (
info
) - Validation errors (
warn
orinfo
, Fastify handles response) - Calls to external services (Plivo) (
info
/debug
) - Successful external service responses (
info
) - Errors from external services (
error
) - Application logic errors (
error
) - Key decision points or state changes (
debug
)
- Request received (
- Levels: Use
-
Retry Mechanisms:
- Plivo Internal Retries: Plivo may have internal retries for certain transient issues, but don't rely solely on this.
- Application-Level Retries: For network errors or specific Plivo error codes indicating a temporary issue (e.g., rate limit exceeded, temporary gateway error), you can implement retries before calling
plivoClient.messages.create
. Libraries likeasync-retry
orp-retry
are excellent for this.
Example using
async-retry
(Conceptual):First, install the library:
npm install async-retry
Then, modify the service conceptually:
// src/services/plivoService.js (Conceptual modification with async-retry) const retry = require('async-retry'); const plivo = require('plivo'); // Assuming plivo is already required let plivoClient; function initializePlivoClient(config, logger) { /* ... as before ... */ } async function sendBulkSms(destinations, message, config, logger) { initializePlivoClient(config, logger); // ... existing validation ... const uniqueDestinations = [...new Set(destinations)]; const plivoDestinationString = uniqueDestinations.join('<'); // ... MAX_RECIPIENTS_PER_REQUEST check ... logger.info( `Attempting to send bulk SMS via Plivo to ${uniqueDestinations.length} unique recipients (with retry logic).` ); try { // Wrap the Plivo call in retry logic const response = await retry( async (bail_ attemptNumber) => { // bail is a function to stop retrying (e.g., for auth errors) // attemptNumber is the current retry attempt logger.debug(`Plivo API call attempt #${attemptNumber}`); try { const result = await plivoClient.messages.create( config.sourceNumber, plivoDestinationString, message // Optional params... ); logger.info(`Plivo API attempt #${attemptNumber} successful.`); return result; // Success, return result } catch (error) { logger.warn(`Plivo API attempt #${attemptNumber} failed: ${error.message}`); // Decide if the error is retryable // Example: Don't retry authentication errors (401) if (error.statusCode === 401 || (error.message && error.message.includes('Authentication credentials invalid'))) { logger.error('Authentication error. Not retrying.'); bail(new Error(`Plivo authentication failed: ${error.message}`)); // Stop retrying return; // Necessary after bail } // Example: Retry potentially transient errors (rate limit 429, server errors 5xx) if (error.statusCode === 429 || error.statusCode >= 500) { logger.warn(`Retryable error detected (status: ${error.statusCode}). Throwing to trigger retry.`); throw error; // Throw error to trigger retry by async-retry } // For other client-side errors (e.g., invalid number 400), don't retry logger.error(`Non-retryable Plivo error (status: ${error.statusCode || 'N/A'}). Not retrying.`); bail(new Error(`Non-retryable Plivo error: ${error.message}`)); return; // Necessary after bail } }, { retries: 3, // Number of retries (total attempts = retries + 1) factor: 2, // Exponential backoff factor minTimeout: 1000, // Initial delay ms maxTimeout: 5000, // Max delay ms onRetry: (error, attempt) => { logger.warn(`Retrying Plivo API call (attempt ${attempt}) due to: ${error.message}`); }, } ); logger.info( { message_uuid: response.messageUuid, api_id: response.apiId, recipient_count: uniqueDestinations.length, }, 'Plivo bulk message request successful after potential retries.', ); return response; } catch (error) { // This catches errors after all retries fail or if bail was called logger.error( { err: error, plivoError: error.message, statusCode: error.statusCode, }, 'Plivo API error during bulk message send after all retries.', ); throw new Error(`Failed to send bulk SMS via Plivo after retries: ${error.message}`); } } module.exports = { initializePlivoClient, sendBulkSms };
- This adds complexity but improves resilience against temporary Plivo issues or network glitches. Carefully define which errors are retryable.