fix(core): Add timeout to external secrets provider refresh (#29679)

This commit is contained in:
Ali Elkhateeb
2026-05-04 13:10:05 +00:00
committed by GitHub
parent 45effb8959
commit e35042999f
3 changed files with 42 additions and 2 deletions
@@ -2,6 +2,7 @@ import { mockLogger } from '@n8n/backend-test-utils';
import { AnotherDummyProvider, DummyProvider } from '@test/external-secrets/utils';
import { EXTERNAL_SECRETS_REFRESH_TIMEOUT_MS } from '../constants';
import { ExternalSecretsProviderRegistry } from '../provider-registry.service';
import { ExternalSecretsSecretsCache } from '../secrets-cache.service';
@@ -53,6 +54,22 @@ describe('SecretsCache', () => {
await expect(cache.refreshProvider('dummy', dummyProvider)).resolves.not.toThrow();
});
it('should not hang when update exceeds refresh timeout', async () => {
jest.useFakeTimers();
try {
jest
.spyOn(dummyProvider, 'update')
.mockImplementation(async () => await new Promise(() => {}));
const refreshPromise = cache.refreshProvider('dummy', dummyProvider);
await jest.advanceTimersByTimeAsync(EXTERNAL_SECRETS_REFRESH_TIMEOUT_MS);
await expect(refreshPromise).resolves.toBeUndefined();
} finally {
jest.useRealTimers();
}
});
});
describe('refreshAll', () => {
@@ -3,6 +3,7 @@ import type { INodeProperties } from 'n8n-workflow';
export const EXTERNAL_SECRETS_DB_KEY = 'feature.externalSecrets';
export const EXTERNAL_SECRETS_INITIAL_BACKOFF = 10 * 1000;
export const EXTERNAL_SECRETS_MAX_BACKOFF = 5 * 60 * 1000;
export const EXTERNAL_SECRETS_REFRESH_TIMEOUT_MS = 20_000;
export const DOCS_HELP_NOTICE: INodeProperties = {
displayName:
@@ -1,7 +1,8 @@
import { Logger } from '@n8n/backend-common';
import { Service } from '@n8n/di';
import { ensureError } from 'n8n-workflow';
import { ensureError, OperationalError } from 'n8n-workflow';
import { EXTERNAL_SECRETS_REFRESH_TIMEOUT_MS } from './constants';
import { ExternalSecretsProviderRegistry } from './provider-registry.service';
import type { SecretsProvider } from './types';
@@ -41,7 +42,7 @@ export class ExternalSecretsSecretsCache {
}
try {
await provider.update();
await this.refreshProviderWithTimeout(provider);
this.logger.debug(`Refreshed secrets from provider ${name}`);
} catch (error) {
this.logger.error(`Error refreshing secrets from provider ${name}`, {
@@ -50,6 +51,27 @@ export class ExternalSecretsSecretsCache {
}
}
private async refreshProviderWithTimeout(provider: SecretsProvider): Promise<void> {
let timeoutId: NodeJS.Timeout | undefined;
const timeoutPromise = new Promise<never>((_, reject) => {
timeoutId = setTimeout(() => {
reject(
new OperationalError(
`Timed out refreshing secrets after ${EXTERNAL_SECRETS_REFRESH_TIMEOUT_MS}ms`,
),
);
}, EXTERNAL_SECRETS_REFRESH_TIMEOUT_MS);
});
try {
await Promise.race([provider.update(), timeoutPromise]);
} finally {
if (timeoutId !== undefined) {
clearTimeout(timeoutId);
}
}
}
/**
* Get a secret from a specific provider
*/