🔌 fix: Isolate Code-Server HTTP Agents to Prevent Socket Pool Contamination (#12311)

* 🔧 fix: Isolate HTTP agents for code-server axios requests

Prevents socket hang up after 5s on Node 19+ when code executor has
file attachments. follow-redirects (axios dep) leaks `socket.destroy`
as a timeout listener on TCP sockets; with Node 19+ defaulting to
keepAlive: true, tainted sockets re-enter the global pool and destroy
active node-fetch requests in CodeExecutor after the idle timeout.

Uses dedicated http/https agents with keepAlive: false for all axios
calls targeting CODE_BASEURL in crud.js and process.js.

Closes #12298

* ♻️ refactor: Extract code-server HTTP agents to shared module

- Move duplicated agent construction from crud.js and process.js into
  a shared agents.js module to eliminate DRY violation
- Switch process.js from raw `require('axios')` to `createAxiosInstance()`
  for proxy configuration parity with crud.js
- Fix import ordering in process.js (agent constants no longer split imports)
- Add 120s timeout to uploadCodeEnvFile (was the only code-server call
  without a timeout)

*  test: Add regression tests for code-server socket isolation

- Add crud.spec.js covering getCodeOutputDownloadStream and
  uploadCodeEnvFile (agent options, timeout, URL, error handling)
- Add socket pool isolation tests to process.spec.js asserting
  keepAlive:false agents are forwarded to axios
- Update process.spec.js mocks for createAxiosInstance() migration

* ♻️ refactor: Move code-server agents to packages/api

Relocate agents.js from api/server/services/Files/Code/ to
packages/api/src/utils/code.ts per workspace conventions. Consumers
now import codeServerHttpAgent/codeServerHttpsAgent from @librechat/api.
This commit is contained in:
Danny Avila 2026-03-19 16:16:57 -04:00 committed by GitHub
parent 7e74165c3c
commit 39f5f83a8a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 273 additions and 45 deletions

View file

@ -1,9 +1,15 @@
const path = require('path');
const { v4 } = require('uuid');
const axios = require('axios');
const { logger } = require('@librechat/data-schemas');
const { getCodeBaseURL } = require('@librechat/agents');
const { logAxiosError, getBasePath, sanitizeFilename } = require('@librechat/api');
const {
getBasePath,
logAxiosError,
sanitizeFilename,
createAxiosInstance,
codeServerHttpAgent,
codeServerHttpsAgent,
} = require('@librechat/api');
const {
Tools,
megabyte,
@ -23,6 +29,8 @@ const { getStrategyFunctions } = require('~/server/services/Files/strategies');
const { convertImage } = require('~/server/services/Files/images/convert');
const { determineFileType } = require('~/server/utils');
const axios = createAxiosInstance();
/**
* Creates a fallback download URL response when file cannot be processed locally.
* Used when: file exceeds size limit, storage strategy unavailable, or download error occurs.
@ -102,6 +110,8 @@ const processCodeOutput = async ({
'User-Agent': 'LibreChat/1.0',
'X-API-Key': apiKey,
},
httpAgent: codeServerHttpAgent,
httpsAgent: codeServerHttpsAgent,
timeout: 15000,
});
@ -300,6 +310,8 @@ async function getSessionInfo(fileIdentifier, apiKey) {
'User-Agent': 'LibreChat/1.0',
'X-API-Key': apiKey,
},
httpAgent: codeServerHttpAgent,
httpsAgent: codeServerHttpsAgent,
timeout: 5000,
});
@ -448,5 +460,6 @@ const primeFiles = async (options, apiKey) => {
module.exports = {
primeFiles,
getSessionInfo,
processCodeOutput,
};