@gov-cy/govcy-express-services
Version:
An Express-based system that dynamically renders services using @gov-cy/govcy-frontend-renderer and posts data to a submission API.
125 lines (108 loc) • 5.09 kB
JavaScript
import axios from "axios";
import { logger } from "./govcyLogger.mjs";
/**
* Utility to handle API communication with retry logic
* @param {string} method - HTTP method (e.g., 'post', 'get', etc.)
* @param {string} url - API endpoint URL
* @param {object} inputData - Payload for the request (optional)
* @param {boolean} useAccessTokenAuth - Whether to use Authorization header with Bearer token
* @param {object} user - User object containing access_token (optional)
* @param {object} headers - Custom headers (optional)
* @param {number} retries - Number of retry attempts (default: 3)
* @param {boolean} allowSelfSignedCerts - Whether to allow self-signed certificates (default: false)
* @returns {Promise<object>} - API response
*/
export async function govcyApiRequest(
method,
url,
inputData = {},
useAccessTokenAuth = false,
user = null,
headers = {},
retries = 3,
allowSelfSignedCerts = false
) {
let attempt = 0;
// Clone headers to avoid mutation
let requestHeaders = { ...headers };
// Set authorization header if access token is provided
if (
useAccessTokenAuth &&
typeof user?.access_token === "string" &&
user.access_token.trim().length > 0
) {
requestHeaders['Authorization'] = `Bearer ${user.access_token}`;
}
while (attempt < retries) {
try {
logger.debug(`📤 Sending API request (Attempt ${attempt + 1})`, { method, url, inputData, requestHeaders });
// Build axios config
const axiosConfig = {
method,
url,
[method?.toLowerCase() === 'get' ? 'params' : 'data']: inputData,
headers: requestHeaders,
timeout: 10000, // 10 seconds timeout
};
// Add httpsAgent if NOT production to allow self-signed certificates
// Use per-call config for self-signed certs
if (allowSelfSignedCerts) {
axiosConfig.httpsAgent = new (await import('https')).Agent({ rejectUnauthorized: false });
}
const response = await axios(axiosConfig);
logger.debug(`📥 Received API response`, { status: response.status, data: response.data });
if (response.status !== 200) {
throw new Error(`Unexpected HTTP status: ${response.status}`);
}
// const { Succeeded, ErrorCode, ErrorMessage } = response.data;
// Normalize to PascalCase regardless of input case
const {
succeeded,
errorCode,
errorMessage,
data,
informationMessage,
Succeeded,
ErrorCode,
ErrorMessage,
Data,
InformationMessage
} = response.data;
const normalized = {
Succeeded: Succeeded !== undefined ? Succeeded : succeeded,
ErrorCode: ErrorCode !== undefined ? ErrorCode : errorCode,
ErrorMessage: ErrorMessage !== undefined ? ErrorMessage : errorMessage,
Data: Data !== undefined ? Data : data,
InformationMessage: InformationMessage !== undefined ? InformationMessage : informationMessage
};
// Merge any extra fields (like ReceivedAuthorization, etc.)
for (const key of Object.keys(response.data)) {
if (!(key in normalized)) {
normalized[key] = response.data[key];
}
}
// Validate the normalized response structure
if (typeof normalized.Succeeded !== "boolean") {
throw new Error("Invalid API response structure: Succeeded must be a boolean");
}
// Check if ErrorCode is a number when Succeeded is false
if (!normalized.Succeeded && typeof normalized.ErrorCode !== "number") {
throw new Error("Invalid API response structure: ErrorCode must be a number when Succeeded is false");
}
logger.info(`✅ API call succeeded: ${url}`, response.data);
return normalized; // Return normalized to pascal case the successful response
} catch (error) {
attempt++;
logger.debug(`🚨 API call failed (Attempt ${attempt})`, {
message: error.message,
status: error.response?.status,
data: error.response?.data,
});
if (attempt >= retries) {
logger.error(`🚨 API call failed after ${retries} attempts: ${url}`, error.message);
throw new Error(error.response?.data?.ErrorMessage || "API call failed after retries");
}
logger.info(`🔄 Retrying API request (Attempt ${attempt + 1})...`);
}
}
}