Two-factor authentication (2FA) adds a critical layer of security to user accounts by requiring a second form of verification beyond just a password. Typically, this involves sending a one-time password (OTP) to the user's registered device, often via SMS. This guide provides a complete walkthrough for implementing SMS-based 2FA/OTP in a Node.js application using the Express framework and the Vonage Verify API.
This tutorial will guide you through building a simple web application with three core pages: one to input a phone number, one to enter the received OTP code, and a final page confirming success or indicating failure. We'll cover everything from initial project setup to error handling, security considerations, and deployment suggestions, resulting in a robust and secure 2FA implementation.
Project Overview and Goals
Goal: To integrate SMS-based Two-Factor Authentication into a Node.js Express web application.
Problem Solved: Enhances application security by verifying user identity through a secondary channel (SMS OTP), mitigating risks associated with compromised passwords.
Technologies Used:
- Node.js: JavaScript runtime environment for the backend server.
- Express.js: Minimalist web framework for Node.js, used to handle routing and requests.
- Vonage Verify API: Manages the complexities of OTP generation, delivery (SMS, voice), and verification. We use this to avoid building and maintaining the OTP lifecycle logic ourselves.
- EJS (Embedded JavaScript templates): Simple templating engine to render HTML pages dynamically. (Nunjucks or others could also be used).
- dotenv: Module to load environment variables from a
.env
file.
System Architecture:
+-------------+ +-----------------+ +---------------+ +-------------+ +--------------+
| User |------>| Node.js/Express |------>| Vonage Verify |------>| SMS Network |------>| User's Phone |
| (Browser) | | App | | API | | | | (Receives OTP)|
+-------------+ +-----------------+ +---------------+ +-------------+ +--------------+
^ | ^ |
| (Enters OTP) | (Sends Phone #) | (Sends OTP) |
| | | |
+---------------------+ (Verifies OTP) -----------+ |
| |
+-------------------------------------------------+
(Verification Result)
(Note: This ASCII diagram provides a basic overview. Tools like MermaidJS could create clearer diagrams if the platform supports them.)
Final Outcome: A functional web application where a user can:
- Enter their phone number.
- Receive an SMS OTP via Vonage.
- Enter the received OTP.
- Get confirmation of successful verification or an error message.
Prerequisites:
- Node.js and npm (or yarn) installed. Install Node.js
- A Vonage API account. Sign up for free and note your API Key and API Secret from the dashboard.
- Basic understanding of Node.js, Express, and web concepts (HTTP requests, forms).
- A text editor or IDE (e.g., VS Code).
1. Setting Up the Project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal or command prompt and create a new directory for the project, then navigate into it.
mkdir node-vonage-2fa cd node-vonage-2fa
-
Initialize Node.js Project: This command creates a
package.json
file to manage project dependencies and scripts.npm init -y
-
Install Dependencies: We need Express for the web server, EJS for templating,
body-parser
to handle form submissions,@vonage/server-sdk
for the Vonage API, anddotenv
for managing credentials securely.npm install express ejs body-parser @vonage/server-sdk dotenv
(Note: While
body-parser
works perfectly fine, modern versions of Express (4.16+) include built-in middleware:express.json()
andexpress.urlencoded({ extended: true })
. We usebody-parser
here for clarity, but you could use the built-in versions instead.) -
Create Project Structure: Set up a basic directory structure for clarity.
node-vonage-2fa/ ├── views/ │ ├── index.ejs │ ├── verify.ejs │ └── success.ejs ├── .env ├── .gitignore ├── server.js ├── package.json ├── package-lock.json └── node_modules/
-
Configure Environment Variables: Create a file named
.env
in the project root. Add your Vonage API Key and Secret. Never commit this file to version control.# .env VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRET PORT=3000
Replace
YOUR_API_KEY
andYOUR_API_SECRET
with the actual credentials from your Vonage dashboard. -
Create
.gitignore
: Create a.gitignore
file in the project root to prevent sensitive files and unnecessary directories from being committed to Git.# .gitignore node_modules/ .env npm-debug.log* yarn-debug.log* yarn-error.log*
-
Basic Express Server Setup (
server.js
): Createserver.js
and set up the initial Express application, load environment variables, and configure middleware.// server.js require('dotenv').config(); // Load environment variables from .env file const express = require('express'); const bodyParser = require('body-parser'); const { Vonage } = require('@vonage/server-sdk'); // Input Validation (using express-validator - install later in Section 7) // const { body, validationResult } = require('express-validator'); // Rate Limiting (using express-rate-limit - install later in Section 7) // const rateLimit = require('express-rate-limit'); const app = express(); const port = process.env.PORT || 3000; // --- Middleware --- // Parse URL-encoded bodies (as sent by HTML forms) app.use(bodyParser.urlencoded({ extended: true })); // Parse JSON bodies (as sent by API clients) app.use(bodyParser.json()); // Serve static files (CSS, JS, images) if needed // app.use(express.static('public')); // Set EJS as the view engine app.set('view engine', 'ejs'); // --- Vonage Client Initialization --- // IMPORTANT: Check for API Key/Secret presence if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET) { console.error('Error: VONAGE_API_KEY or VONAGE_API_SECRET not found in .env file.'); console.error('Please ensure you have a .env file with your Vonage credentials.'); process.exit(1); // Exit if credentials are not set } const vonage = new Vonage({ apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET }); // --- Routes (will be defined in Section 2) --- // ... routes go here ... // --- Start Server --- app.listen(port, () => { console.log(`Server listening on port ${port}`); console.log(`Access the app at http://localhost:${port}`); });
Why these choices?
dotenv
: Standard practice for securely managing credentials and configuration outside the codebase.body-parser
: Essential middleware for Express to understand incoming request bodies, especially from HTML forms (urlencoded
).ejs
: A simple and popular choice for server-side templating in Express.@vonage/server-sdk
: The official Vonage SDK for Node.js, providing convenient methods to interact with the Verify API. We explicitly check for credentials before initializing to provide clear feedback if they are missing.
2. Implementing Core Functionality (Verify API Integration)
Now, let's build the routes and logic for the 2FA flow using the Vonage Verify API.
-
Create Views (EJS Templates):
-
views/index.ejs
(Phone Number Input Form)<!-- views/index.ejs --> <!DOCTYPE html> <html lang=""en""> <head> <meta charset=""UTF-8""> <meta name=""viewport"" content=""width=device-width_ initial-scale=1.0""> <title>Enter Phone Number</title> <style> /* Basic styling */ body { font-family: sans-serif; padding: 20px; } .error { color: red; margin-bottom: 10px; } label, input, button { display: block; margin-bottom: 10px; } input { padding: 8px; width: 250px; } button { padding: 10px 15px; cursor: pointer; } </style> </head> <body> <h1>Enter Your Phone Number for 2FA</h1> <% if (typeof error !== 'undefined' && error) { %> <p class=""error""><%= error %></p> <% } %> <form method=""POST"" action=""/request-verification""> <label for=""phoneNumber"">Phone Number (with country code, e.g., 14155551212):</label> <input type=""tel"" id=""phoneNumber"" name=""phoneNumber"" required placeholder=""14155551212""> <button type=""submit"">Send Verification Code</button> </form> </body> </html>
-
views/verify.ejs
(Code Entry Form)<!-- views/verify.ejs --> <!DOCTYPE html> <html lang=""en""> <head> <meta charset=""UTF-8""> <meta name=""viewport"" content=""width=device-width_ initial-scale=1.0""> <title>Enter Verification Code</title> <style> /* Basic styling */ body { font-family: sans-serif; padding: 20px; } .error { color: red; margin-bottom: 10px; } label, input, button { display: block; margin-bottom: 10px; } input { padding: 8px; width: 150px; } button { padding: 10px 15px; cursor: pointer; } </style> </head> <body> <h1>Enter the Verification Code</h1> <p>A code was sent to your phone.</p> <% if (typeof error !== 'undefined' && error) { %> <p class=""error""><%= error %></p> <% } %> <form method=""POST"" action=""/check-verification""> <input type=""hidden"" name=""requestId"" value=""<%= requestId %>""> <label for=""code"">Verification Code:</label> <input type=""text"" id=""code"" name=""code"" required pattern=""\d{4_6}"" title=""Enter the 4 or 6 digit code""> <button type=""submit"">Verify Code</button> </form> </body> </html>
-
views/success.ejs
(Success Page)<!-- views/success.ejs --> <!DOCTYPE html> <html lang=""en""> <head> <meta charset=""UTF-8""> <meta name=""viewport"" content=""width=device-width_ initial-scale=1.0""> <title>Verification Successful</title> <style> /* Basic styling */ body { font-family: sans-serif; padding: 20px; } h1 { color: green; } </style> </head> <body> <h1>Verification Successful!</h1> <p>Your identity has been confirmed.</p> <a href="" />Start Over</a> </body> </html>
-
-
Define Routes in
server.js
: Add the following route handlers within yourserver.js
file, replacing the// ... routes go here ...
comment.// server.js (add these inside the file, after Vonage init) // --- Routes --- // GET Route: Display the initial phone number input form app.get('/', (req, res) => { // Render index.ejs, optionally passing an error message if redirected here res.render('index', { error: req.query.error }); }); // POST Route: Start the verification process app.post('/request-verification', async (req, res) => { const phoneNumber = req.body.phoneNumber; // Basic validation: Assumes digits only, 10-15 length. Doesn't strictly enforce E.164. // Consider more robust validation (e.g., using a library like 'libphonenumber-js' or 'express-validator' as shown later). // Section 8 clarifies E.164 format is recommended for Vonage. if (!phoneNumber || !/^\d{10,15}$/.test(phoneNumber)) { return res.render('index', { error: 'Invalid phone number format. Please include country code (digits only, 10-15 length).' }); } // Log only the last 4 digits for privacy console.log(`Requesting verification for phone number ending: ...${phoneNumber.slice(-4)}`); try { const resp = await vonage.verify.start({ number: phoneNumber, brand: ""MyApp"", // Customize this to your app name // workflow_id: 1 // Optional: Specify workflow (1: SMS -> TTS -> TTS, default) // See Vonage docs for other workflow IDs (e.g., SMS only) // code_length: 6 // Optional: Default is 4, can be set to 6 }); if (resp.status === '0') { // Status '0' indicates success const requestId = resp.request_id; console.log(`Verification request sent. Request ID: ${requestId}`); // Redirect to the verification code entry page, passing the request ID res.render('verify', { requestId: requestId }); } else { // Handle Vonage API errors (e.g., invalid number, throttling) console.error(`Vonage verification request failed: ${resp.error_text} (Status: ${resp.status})`); // Pass the specific Vonage error back to the user res.render('index', { error: `Verification failed: ${resp.error_text}` }); } } catch (error) { // Handle network errors or SDK issues console.error('Error starting verification:', error); res.render('index', { error: 'An unexpected error occurred. Please try again.' }); } }); // POST Route: Check the submitted verification code app.post('/check-verification', async (req, res) => { const requestId = req.body.requestId; const code = req.body.code; // Basic input validation if (!requestId || !code || !/^\d{4,6}$/.test(code)) { return res.render('verify', { requestId: requestId, error: 'Invalid input. Please enter the code.' }); } console.log(`Checking verification code. Request ID: ${requestId}, Code: ****`); // Avoid logging the code try { const resp = await vonage.verify.check(requestId, code); if (resp.status === '0') { // Status '0' indicates successful verification console.log(`Verification successful. Request ID: ${requestId}`); // Verification successful, render the success page res.render('success'); } else { // Handle Vonage API errors (e.g., wrong code, expired code) console.error(`Vonage verification check failed: ${resp.error_text} (Status: ${resp.status})`); // Render the verify page again with the specific error // Common statuses: '16' (Wrong code), '6' (Request expired or not found) res.render('verify', { requestId: requestId, error: `Verification failed: ${resp.error_text}` }); } } catch (error) { // Handle network errors or SDK issues console.error('Error checking verification:', error); res.render('verify', { requestId: requestId, // Pass requestId back so user can retry if needed error: 'An unexpected error occurred while checking the code. Please try again.' }); } }); // Optional: GET route to cancel a verification request (if needed) // Add a cancel button to verify.ejs pointing to /cancel-verification?requestId=<%= requestId %> app.get('/cancel-verification', async (req, res) => { const requestId = req.query.requestId; if (!requestId) { return res.redirect('/?error=Missing request ID for cancellation.'); } console.log(`Attempting to cancel verification. Request ID: ${requestId}`); try { const resp = await vonage.verify.cancel(requestId); if (resp.status === '0') { console.log(`Verification cancelled successfully. Request ID: ${requestId}`); res.redirect('/?error=Verification cancelled.'); // Redirect with a message } else { console.error(`Vonage cancel request failed: ${resp.error_text} (Status: ${resp.status})`); // Might fail if already verified or expired, which is often acceptable res.redirect(`/verify?requestId=${requestId}&error=Could not cancel: ${resp.error_text}`); } } catch(error) { console.error('Error cancelling verification:', error); res.redirect(`/verify?requestId=${requestId}&error=Error cancelling verification.`); } }); // Fallback route for unmatched paths (optional) app.use((req, res) => { res.status(404).send(""Sorry, can't find that!""); });
Why these choices?
async/await
: Used for cleaner handling of the asynchronous Vonage API calls compared to callbacks.try...catch
: Essential for catching potential network errors or unexpected issues during the API calls.vonage.verify.start
: Initiates the 2FA process. Sends the SMS/voice call with the code. Requires the target phone number and abrand
name (shown in the SMS message). It returns arequest_id
.vonage.verify.check
: Verifies the user-submitted code against therequest_id
. Requires both therequest_id
and thecode
.vonage.verify.cancel
: Allows explicitly cancelling an in-progress verification (useful if the user navigates away or requests a new code).- Error Handling: We check the
status
code in the Vonage response ('0'
means success). Non-zero statuses indicate specific errors (wrong code, expired, etc.), and theerror_text
provides a human-readable message which we display to the user. We redirect back to the appropriate form (index
orverify
) on error, passing the error message andrequestId
(for retries) where applicable.
-
Run the Application: Open your terminal in the project root and run:
node server.js
Open your web browser and navigate to
http://localhost:3000
(or the port you configured). You should see the phone number input form. Test the full flow by entering your phone number, receiving the SMS, and entering the code.
3. Building a Complete API Layer
In this specific example, we've built a server-rendered web application rather than a separate API layer consumed by a frontend framework (like React/Vue/Angular). The Express routes (/request-verification
, /check-verification
) act as the ""API endpoints"" for the HTML forms.
If building a separate API:
-
Endpoints: You would expose similar endpoints (
POST /api/v1/auth/2fa/request
,POST /api/v1/auth/2fa/check
). -
Authentication/Authorization: Crucially, these API endpoints would need protection. They should typically only be accessible by an already authenticated user (e.g., identified via a session or a JWT token sent in the
Authorization
header). This ensures you're associating the 2FA attempt with the correct, logged-in user account before proceeding with sending SMS messages or verifying codes. -
Request Validation: Use libraries like
express-validator
more rigorously on the API endpoints to validate input formats (phone number, code, request ID). -
Responses: Return JSON responses instead of rendering HTML templates.
- Success (
/request-verification
):{""status"": ""success"", ""requestId"": ""...""}
- Success (
/check-verification
):{""status"": ""success"", ""message"": ""Verification complete""}
- Error:
{""status"": ""error"", ""message"": ""Invalid code"", ""details"": ""...""}
with appropriate HTTP status codes (400 Bad Request, 401 Unauthorized, 429 Too Many Requests, 500 Server Error).
- Success (
-
Testing: Use tools like Postman or
curl
to test the API endpoints directly.# Example curl for requesting verification (assuming JWT auth for a logged-in user) curl -X POST http://localhost:3000/api/v1/auth/2fa/request \ -H ""Content-Type: application/json"" \ -H ""Authorization: Bearer YOUR_JWT_TOKEN"" \ -d '{""phoneNumber"": ""14155551212""}' # Or phone number might be derived from logged-in user server-side # Example curl for checking the code curl -X POST http://localhost:3000/api/v1/auth/2fa/check \ -H ""Content-Type: application/json"" \ -H ""Authorization: Bearer YOUR_JWT_TOKEN"" \ -d '{""requestId"": ""YOUR_REQUEST_ID"", ""code"": ""1234""}'
4. Integrating with Vonage (Credentials and Configuration)
-
Obtaining Credentials:
- Go to the Vonage API Dashboard.
- Your API Key and API Secret are displayed prominently at the top of the ""Getting started"" page or under ""API settings"".
- Copy these values carefully.
-
Secure Storage (
.env
): As done in Section 1, store these credentials in a.env
file in your project root:# .env VONAGE_API_KEY=YOUR_API_KEY VONAGE_API_SECRET=YOUR_API_SECRET PORT=3000
VONAGE_API_KEY
: Your public Vonage API key.VONAGE_API_SECRET
: Your private Vonage API secret. Keep this confidential.PORT
: The port your Express server will run on.
-
Loading Credentials (
dotenv
): Ensure the first line in yourserver.js
loads these variables:// server.js (first line) require('dotenv').config();
-
Using Credentials (SDK Initialization): The Vonage SDK automatically uses these environment variables if they are named correctly (
VONAGE_API_KEY
,VONAGE_API_SECRET
). However, explicitly passing them during initialization (as shown inserver.js
) is clearer and allows for alternative configuration methods if needed.// server.js const vonage = new Vonage({ apiKey: process.env.VONAGE_API_KEY, apiSecret: process.env.VONAGE_API_SECRET });
-
Vonage Dashboard Settings: For the Verify API specifically, there usually aren't extensive dashboard configurations required beyond obtaining the key/secret. However, be aware of:
- Account Balance: Ensure you have sufficient credit. Verification attempts cost a small amount. You get free credit when signing up.
- Allowed IPs (Optional): For increased security, you can restrict API calls to specific IP addresses in the dashboard settings, but this is less common for server-side applications deployed in cloud environments with dynamic IPs.
- Verify API Settings: Under ""Verify"" in the dashboard, you might find options to configure default settings like allowed countries or SMS sender IDs (where applicable and supported).
-
Fallback Mechanisms: The Vonage Verify API itself handles retries (e.g., SMS -> Voice Call) based on the selected
workflow_id
. For application-level resilience:- Network Errors: Implement retries with exponential backoff in your own code when calling
vonage.verify.start
orvonage.verify.check
if you encounter network-level errors (e.g., timeouts, DNS issues) before reaching the Vonage API. Libraries likeasync-retry
can help (see Section 5). - Vonage Service Issues: Monitor Vonage's status page (status.vonage.com). In a critical application, you might consider a secondary 2FA provider as a fallback, though this adds significant complexity. For most use cases, relying on Vonage's high availability is sufficient. Handle API errors gracefully by informing the user there's a temporary issue.
- Network Errors: Implement retries with exponential backoff in your own code when calling
5. Error Handling, Logging, and Retry Mechanisms
Robust error handling and logging are crucial for diagnosing issues in production.
-
Consistent Error Handling Strategy:
- Catch Errors: Use
try...catch
blocks around all external API calls (Vonage SDK) and potentially problematic synchronous code. - Vonage API Errors: Check the
status
property in the Vonage response. If non-zero, log thestatus
anderror_text
, and provide a user-friendly message based on theerror_text
or status code. - Network/SDK Errors: Catch errors in the
catch
block. Log the detailed error stack trace for debugging. Provide a generic ""unexpected error"" message to the user. - User Input Errors: Validate user input before calling the API (see Section 7) and return specific error messages to the user immediately.
- Catch Errors: Use
-
Logging: Replace basic
console.log
andconsole.error
with a more structured logging library for production.-
Install a Logger: (e.g., Winston)
npm install winston
-
Configure Logger: Create a simple logger configuration.
// logger.js (example) const winston = require('winston'); const logger = winston.createLogger({ level: process.env.LOG_LEVEL || 'info', // Control log level via env var format: winston.format.combine( winston.format.timestamp(), winston.format.json() // Log in JSON format for easier parsing ), transports: [ // Log to console new winston.transports.Console({ format: winston.format.combine( winston.format.colorize(), // Add colors for readability in console winston.format.simple() ) }), // Optionally log to a file in production // new winston.transports.File({ filename: 'error.log', level: 'error' }), // new winston.transports.File({ filename: 'combined.log' }) ], }); module.exports = logger;
-
Use Logger in
server.js
:// server.js // ... other requires const logger = require('./logger'); // Assuming logger.js is created // --- Vonage Client Initialization --- if (!process.env.VONAGE_API_KEY || !process.env.VONAGE_API_SECRET) { logger.error('FATAL: VONAGE_API_KEY or VONAGE_API_SECRET not found in .env file.'); process.exit(1); } // ... // --- Routes --- app.post('/request-verification', async (req, res) => { // ... validation ... const phoneNumber = req.body.phoneNumber; // Get phone number after validation // Log only the last 4 digits for privacy logger.info(`Requesting verification for phone number ending: ...${phoneNumber.slice(-4)}`); try { // ... vonage.verify.start call ... const resp = await vonage.verify.start({ number: phoneNumber, brand: ""MyApp"" }); // Example call if (resp.status === '0') { logger.info(`Verification request sent successfully.`, { requestId: resp.request_id }); // ... render verify page ... res.render('verify', { requestId: resp.request_id }); // Example render } else { logger.warn(`Vonage verification request failed.`, { status: resp.status, error: resp.error_text, requestId: resp.request_id }); // ... render index with error ... res.render('index', { error: `Verification failed: ${resp.error_text}` }); // Example render } } catch (error) { logger.error('Error starting verification:', { errorMessage: error.message, stack: error.stack }); // ... render index with generic error ... res.render('index', { error: 'An unexpected error occurred. Please try again.' }); // Example render } }); app.post('/check-verification', async (req, res) => { // ... validation ... const { requestId, code } = req.body; // Get variables after validation logger.info(`Checking verification code.`, { requestId }); // Avoid logging the code itself try { // ... vonage.verify.check call ... const resp = await vonage.verify.check(requestId, code); // Example call if (resp.status === '0') { logger.info(`Verification successful.`, { requestId }); // ... render success page ... res.render('success'); // Example render } else { logger.warn(`Vonage verification check failed.`, { status: resp.status, error: resp.error_text, requestId }); // ... render verify with error ... res.render('verify', { requestId: requestId, error: `Verification failed: ${resp.error_text}` }); // Example render } } catch (error) { logger.error('Error checking verification:', { errorMessage: error.message, stack: error.stack, requestId }); // ... render verify with generic error ... res.render('verify', { requestId: requestId, error: 'An unexpected error occurred while checking the code. Please try again.' }); // Example render } }); // ... rest of server.js including other routes and app.listen ... app.listen(port, () => { logger.info(`Server listening on port ${port}`); });
-
-
Retry Mechanisms (Application Level): While Vonage handles retries for delivery, you might retry API calls from your server if they fail due to transient network issues.
-
Install Retry Library:
npm install async-retry
-
Wrap API Calls:
// server.js const retry = require('async-retry'); // ... other requires ... const logger = require('./logger'); // Make sure logger is available // Example wrapping vonage.verify.start app.post('/request-verification', async (req, res) => { // ... validation ... const phoneNumber = req.body.phoneNumber; logger.info(`Requesting verification for phone number ending: ...${phoneNumber.slice(-4)}`); try { const resp = await retry(async bail => { // bail is a function to stop retrying if the error is permanent (like invalid credentials) try { const apiResponse = await vonage.verify.start({ number: phoneNumber, brand: ""MyApp"" }); // Decide which Vonage statuses are definitive and shouldn't be retried by *our* application. // Statuses '0' (success), '3' (invalid number), '4' (invalid creds), '6' (invalid request_id), // '9' (quota exceeded), '10' (concurrent verifications), '15' (unsupported network), // '16' (wrong code), '17' (too many wrong codes) are generally final for *this* specific call attempt. // Status '1' (throttled) or '5' (internal error) *might* be retryable, but check Vonage docs/best practices. // Let's assume only SDK/network errors are retryable here for simplicity. if (apiResponse.status && apiResponse.status !== '0') { // Use bail for errors we know shouldn't be retried from our side. // Example: Bailing on invalid credentials or invalid number format reported by Vonage. if (apiResponse.status === '4' || apiResponse.status === '3') { bail(new Error(`Vonage non-retryable error: ${apiResponse.error_text} (Status: ${apiResponse.status})`)); return; // Important: return after calling bail with an error } // For other Vonage errors, we might not retry either, just let the outer handler deal with them. // Throwing a regular error here would cause async-retry to retry (unless bailed). // If we don't want to retry other Vonage API errors, we can just return the response. // However, the outer try/catch expects a successful response structure or an error. // Let's throw an error for *any* non-zero status to be handled outside the retry block. throw new Error(`Vonage API Error: ${apiResponse.error_text} (Status: ${apiResponse.status})`); } return apiResponse; // Return successful response } catch (sdkOrNetworkError) { // Log the attempt failure logger.warn('Vonage API call attempt failed, may retry.', { error: sdkOrNetworkError.message }); // If the error is definitely non-retryable (e.g., specific SDK error), use bail // if (sdkOrNetworkError.isNonRetryable) { bail(sdkOrNetworkError); return; } // Otherwise, throw the error to trigger a retry by async-retry throw sdkOrNetworkError; } }, { retries: 3, // Number of retries factor: 2, // Exponential backoff factor minTimeout: 1000, // Minimum time between retries (ms) onRetry: (error, attempt) => { logger.warn(`Retrying Vonage API call (Attempt ${attempt}) due to error: ${error.message}`); } }); // Process the successful response (resp will have status '0' if we reached here) logger.info(`Verification request sent successfully after retries (if any).`, { requestId: resp.request_id }); res.render('verify', { requestId: resp.request_id }); } catch (error) { // This catches errors from retry (if it finally failed) or non-retryable errors (bailed) // or errors thrown inside the retry block for non-zero statuses. logger.error('Failed to start verification after retries or due to non-retryable error:', { errorMessage: error.message, stack: error.stack }); // Determine error message based on the caught error const userErrorMessage = error.message.startsWith('Vonage') ? error.message : 'An unexpected error occurred. Please try again.'; res.render('index', { error: userErrorMessage }); } }); // Similar wrapping could be applied to vonage.verify.check and vonage.verify.cancel // ... other routes ...
-