fix(core): Restore AWS Secrets Manager connection reuse (#36426)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Irénée
2026-08-18 15:46:43 +00:00
committed by GitHub
co-authored by Claude Opus 4.8
parent c0b3a12a1c
commit 56bb5f593d
3 changed files with 44 additions and 1 deletions
@@ -74,6 +74,40 @@ describe('getNodeAgent', () => {
});
});
// ---------------------------------------------------------------------------
// getNodeAgent — connection-reuse options propagate across all routing paths
//
// Callers may pass connection-management options (e.g. `keepAlive`,
// `maxSockets`) to enable connection reuse. Regardless of how the request is
// routed, those options must reach the constructed agent — otherwise a proxied
// deployment would silently drop them.
// ---------------------------------------------------------------------------
describe('getNodeAgent connection-reuse options', () => {
// keep-alive/maxSockets are connection-management options, so Node's Agent (and
// the proxy agents that extend it) expose them as instance properties.
function getReuseOptions(agent: http.Agent | https.Agent): {
keepAlive: unknown;
maxSockets: unknown;
} {
const a = agent as { keepAlive?: unknown; maxSockets?: unknown };
return { keepAlive: a.keepAlive, maxSockets: a.maxSockets };
}
it.each([
['direct (proxy: false)', { proxy: false } as const],
['env proxy', { proxy: 'env' } as const],
['explicit proxy URL', { proxy: 'http://proxy.internal:3128' } as const],
])('forwards keepAlive/maxSockets on the %s path', (_label, transportOptions) => {
const { httpAgent, httpsAgent } = makeFacade()
.transport(transportOptions)
.getNodeAgent({ keepAlive: true, maxSockets: 50 });
expect(getReuseOptions(httpAgent)).toEqual({ keepAlive: true, maxSockets: 50 });
expect(getReuseOptions(httpsAgent)).toEqual({ keepAlive: true, maxSockets: 50 });
});
});
// ---------------------------------------------------------------------------
// getNodeAgent — SSRF lookup injection
// ---------------------------------------------------------------------------
@@ -105,6 +105,11 @@ describe('AwsSecretsManager', () => {
expect(outboundHttp.transport).toHaveBeenCalledWith({ ssrf: 'disabled' });
// Request connection reuse from our agents. Smithy treats a supplied agent as
// external and skips its own keep-alive defaults, so we pass them explicitly to
// restore the AWS SDK-equivalent behavior (Smithy NodeHttpHandler defaults).
expect(transport.getNodeAgent).toHaveBeenCalledWith({ keepAlive: true, maxSockets: 50 });
// The SDK client is built with our agents as its requestHandler, while the
// region and credentials it was already given are left untouched.
const SecretsManagerMock = SecretsManager as unknown as Mock;
@@ -136,11 +136,15 @@ export class AwsSecretsManager extends SecretsProvider {
// Drive the AWS SDK's HTTP transport through n8n's outbound client,
// so its calls reuse our agents (proxy + TLS) like every other outbound request.
// SigV4 signing and the credential chain stay with the SDK.
//
// Keep connections pooled so the sequential ListSecrets/BatchGetSecretValue refresh
// reuses sockets instead of paying a fresh TLS handshake per request. `maxSockets`
// caps concurrent sockets, matching the AWS SDK's own defaults.
clientConfig.requestHandler = this.outboundHttp
.transport({
ssrf: 'disabled', // fixed AWS-resolved Secrets Manager host, not user-controlled
})
.getNodeAgent();
.getNodeAgent({ keepAlive: true, maxSockets: 50 });
const { SecretsManager } = await import('@aws-sdk/client-secrets-manager');
this.client = new SecretsManager(clientConfig);