diff --git a/src/routes/wxccRoutes.js b/src/routes/wxccRoutes.js index fc732f7..d03e2b3 100644 --- a/src/routes/wxccRoutes.js +++ b/src/routes/wxccRoutes.js @@ -1,22 +1,9 @@ import express from 'express'; -import axios from 'axios'; -import axiosRetry from 'axios-retry'; import { logger, webexLogger } from '../utilities/logger.js'; import * as jiraService from '../services/jiraService.js'; import grokService from '../services/grokService.js'; import config from '../config/index.js'; -// Configure retry for transient failures -axiosRetry(axios, { - retries: 3, - retryDelay: (retryCount) => Math.pow(2, retryCount) * 1000, - retryCondition: (error) => { - return axiosRetry.isNetworkOrIdempotentRequestError(error) || - error.response?.status === 429 || - error.response?.status >= 500; - } -}); - const router = express.Router(); // ======================== diff --git a/src/services/jiraService.js b/src/services/jiraService.js index a18a8bd..f70b1f5 100644 --- a/src/services/jiraService.js +++ b/src/services/jiraService.js @@ -30,9 +30,11 @@ const createJiraClient = () => { headers, }); - // Apply retry policy to this instance (keep in sync with the global axios-retry - // configured in wxccRoutes.js for the S3 downloads and any other raw calls). - // Using exponential backoff to match the current policy in routes. + // Retry policy scoped to this client only. We used to also mutate the + // global axios instance from wxccRoutes.js, which caused every bare axios + // call (S3 downloads, Assets AQL, etc.) to inherit these retries as a + // side-effect. That's been removed — anything that needs retries now uses + // an explicit instance (jiraClient here, downloadClient below). axiosRetry(client, { retries: 3, retryDelay: (retryCount) => Math.pow(2, retryCount) * 1000, @@ -48,6 +50,24 @@ const createJiraClient = () => { export const jiraClient = createJiraClient(); +// Dedicated instance for fetching pre-signed S3 URLs (audio + transcript +// files that Webex CC hands us). Kept separate from jiraClient because: +// 1. Different base URL (no baseURL — we always pass the full pre-signed URL). +// 2. No Authorization header (the S3 URL is already signed). +// 3. We want retries here — S3 pre-signed downloads are the flakiest thing +// in the pipeline (transient 5xx, TLS resets, TCP timeouts). +// Kept private (not exported); callers in this module use it directly. +const downloadClient = axios.create({ timeout: 20000 }); +axiosRetry(downloadClient, { + retries: 3, + retryDelay: (retryCount) => Math.pow(2, retryCount) * 1000, + retryCondition: (error) => { + return axiosRetry.isNetworkOrIdempotentRequestError(error) || + error.response?.status === 429 || + error.response?.status >= 500; + } +}); + // ======================== // Existing Functions (updated to use jiraClient where possible) // ======================== @@ -256,9 +276,8 @@ async function attachBufferToJira(jiraKey, fileBuffer, fileName) { export async function attachFileToJira(jiraKey, fileUrl, fileName) { let fileBuffer; try { - const dl = await axios.get(fileUrl, { - responseType: 'arraybuffer', - timeout: 20000 + const dl = await downloadClient.get(fileUrl, { + responseType: 'arraybuffer' }); fileBuffer = Buffer.from(dl.data); } catch (dlErr) { @@ -308,7 +327,7 @@ function formatTranscriptToHumanReadable(data) { async function fetchAndConvertTranscript(url) { try { - const resp = await axios.get(url, { timeout: 10000 }); + const resp = await downloadClient.get(url, { timeout: 10000 }); return formatTranscriptToHumanReadable(resp.data); } catch (e) { logger.warn(`Failed to fetch/convert transcript: ${e.message}`);