From c4a0a6934eb4f32b6ee02b62e5adfa4e5f4c75dd Mon Sep 17 00:00:00 2001 From: jmcqueen Date: Wed, 1 Jul 2026 16:28:47 -0400 Subject: [PATCH] Fix #4: remove global axios-retry side-effect; add dedicated downloadClient - wxccRoutes.js no longer mutates the default axios instance, which was causing every bare axios call in the codebase (S3 downloads, Assets diagnostics) to inherit retries as a side-effect of route file load order. - jiraService.js now defines a private downloadClient (own timeout, own retry policy) used by attachFileToJira and fetchAndConvertTranscript for fetching pre-signed S3 URLs. - Assets AQL/GET remain bare axios calls; they're one-shot diagnostics and should not auto-retry. Co-authored-by: Cursor --- src/routes/wxccRoutes.js | 13 ------------- src/services/jiraService.js | 33 ++++++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 20 deletions(-) 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}`);