From 20175243da4c45dc0fc2e273ae5cb7611801fe39 Mon Sep 17 00:00:00 2001 From: Stephen Wright Date: Mon, 16 Feb 2026 16:04:40 +0000 Subject: [PATCH] fix: Credentials overrides no longer blocks UI (#25830) --- packages/cli/src/server.ts | 48 ++++++++++++------- packages/cli/src/services/frontend.service.ts | 45 ++++++++++++----- ...credentials-overwrites.integration.test.ts | 6 ++- .../features/credentials/credentials.api.ts | 22 ++++++++- 4 files changed, 90 insertions(+), 31 deletions(-) diff --git a/packages/cli/src/server.ts b/packages/cli/src/server.ts index 947d9067d9e..8eef7ec77b8 100644 --- a/packages/cli/src/server.ts +++ b/packages/cli/src/server.ts @@ -251,27 +251,43 @@ export class Server extends AbstractServer { this.app.post( `/${this.endpointPresetCredentials}`, async (req: express.Request, res: express.Response) => { - // If authentication is enforced we can allow multiple overwrites - if (!this.presetCredentialsLoaded || authenticationEnforced) { - const body = req.body as ICredentialsOverwrite; + try { + // If authentication is enforced we can allow multiple overwrites + if (!this.presetCredentialsLoaded || authenticationEnforced) { + const body = req.body as ICredentialsOverwrite; - if (req.contentType !== 'application/json') { + if (req.contentType !== 'application/json') { + ResponseHelper.sendErrorResponse( + res, + new Error( + 'Body must be a valid JSON, make sure the content-type is application/json', + ), + ); + return; + } + + await Container.get(CredentialsOverwrites).setData(body, true, true); + + this.presetCredentialsLoaded = true; + + // Send push event to notify frontend to refetch types + Container.get(Push).broadcast({ type: 'nodeDescriptionUpdated', data: {} }); + + ResponseHelper.sendSuccessResponse(res, { success: true }, true, 200); + } else { ResponseHelper.sendErrorResponse( res, - new Error( - 'Body must be a valid JSON, make sure the content-type is application/json', - ), + new Error('Preset credentials can be set once'), ); - return; } - - await Container.get(CredentialsOverwrites).setData(body, true, true); - - this.presetCredentialsLoaded = true; - - ResponseHelper.sendSuccessResponse(res, { success: true }, true, 200); - } else { - ResponseHelper.sendErrorResponse(res, new Error('Preset credentials can be set once')); + } catch (error) { + this.logger.error('Error handling credentials overwrite', { error }); + ResponseHelper.sendErrorResponse( + res, + new Error( + 'An error occurred while handling credentials overwrite, please check the logs for more details', + ), + ); } }, ); diff --git a/packages/cli/src/services/frontend.service.ts b/packages/cli/src/services/frontend.service.ts index 3f308c4cde4..ee5333f0e8c 100644 --- a/packages/cli/src/services/frontend.service.ts +++ b/packages/cli/src/services/frontend.service.ts @@ -376,16 +376,28 @@ export class FrontendService { } async generateTypes() { + /* + * If there are no types and credentials, they have been released from memory + * we trigger a re-initialization of the types to ensure they are loaded in memory, post processing triggers type regeneration + */ + const { types } = this.loadNodesAndCredentials; + if (types.nodes.length === 0 && types.credentials.length === 0) { + return await this.loadNodesAndCredentials.postProcessLoaders(); + } + this.overwriteCredentialsProperties(); const { staticCacheDir } = this.instanceSettings; // pre-render all the node and credential types as static json files await mkdir(path.join(staticCacheDir, 'types'), { recursive: true }); const { credentials, nodes } = this.loadNodesAndCredentials.types; - this.writeStaticJSON('nodes', nodes); + await this.writeStaticJSON('nodes', nodes); const nodeVersionIdentifiers = this.getNodeVersionIdentifiers(nodes); - this.writeStaticJSON('node-versions', nodeVersionIdentifiers); - this.writeStaticJSON('credentials', credentials); + await this.writeStaticJSON('node-versions', nodeVersionIdentifiers); + await this.writeStaticJSON('credentials', credentials); + + // release types to free memory + this.loadNodesAndCredentials.releaseTypes(); } async getSettings(): Promise { @@ -604,27 +616,36 @@ export class FrontendService { return Array.from(identifiers); } - private writeStaticJSON( + private async writeStaticJSON( name: string, data: Array, - ) { + ): Promise { const { staticCacheDir } = this.instanceSettings; const filePath = path.join(staticCacheDir, `types/${name}.json`); const stream = createWriteStream(filePath, 'utf-8'); - stream.write('[\n'); - data.forEach((entry, index) => { - stream.write(JSON.stringify(entry)); - if (index !== data.length - 1) stream.write(','); - stream.write('\n'); + + return await new Promise((resolve, reject) => { + stream.on('error', reject); + stream.on('finish', resolve); + + stream.write('[\n'); + data.forEach((entry, index) => { + stream.write(JSON.stringify(entry)); + if (index !== data.length - 1) stream.write(','); + stream.write('\n'); + }); + stream.write(']\n'); + stream.end(); }); - stream.write(']\n'); - stream.end(); } private overwriteCredentialsProperties() { const { credentials } = this.loadNodesAndCredentials.types; const credentialsOverwrites = this.credentialsOverwrites.getAll(); for (const credential of credentials) { + // Clear any existing overwritten properties to prevent stale data + delete credential.__overwrittenProperties; + const overwrittenProperties = []; this.credentialTypes .getParentTypes(credential.name) diff --git a/packages/cli/test/integration/credentials/credentials-overwrites.integration.test.ts b/packages/cli/test/integration/credentials/credentials-overwrites.integration.test.ts index b9cabf863d3..f82ad319d57 100644 --- a/packages/cli/test/integration/credentials/credentials-overwrites.integration.test.ts +++ b/packages/cli/test/integration/credentials/credentials-overwrites.integration.test.ts @@ -1,4 +1,4 @@ -import { testDb } from '@n8n/backend-test-utils'; +import { mockInstance, testDb } from '@n8n/backend-test-utils'; import { GlobalConfig } from '@n8n/config'; import { SettingsRepository } from '@n8n/db'; import { Container } from '@n8n/di'; @@ -7,6 +7,7 @@ import { Cipher } from 'n8n-core'; import { CredentialsOverwrites } from '@/credentials-overwrites'; import { CredentialTypes } from '@/credential-types'; import type { ICredentialsOverwrite } from '@/interfaces'; +import { FrontendService } from '@/services/frontend.service'; describe('CredentialsOverwrites - Integration Tests', () => { let credentialsOverwrites: CredentialsOverwrites; @@ -16,6 +17,9 @@ describe('CredentialsOverwrites - Integration Tests', () => { let credentialTypes: CredentialTypes; beforeAll(async () => { + // Mock FrontendService to prevent initialization issues during tests + mockInstance(FrontendService); + // Initialize real database await testDb.init(); diff --git a/packages/frontend/editor-ui/src/features/credentials/credentials.api.ts b/packages/frontend/editor-ui/src/features/credentials/credentials.api.ts index 7ba1a608457..cbfed813b75 100644 --- a/packages/frontend/editor-ui/src/features/credentials/credentials.api.ts +++ b/packages/frontend/editor-ui/src/features/credentials/credentials.api.ts @@ -9,11 +9,29 @@ import type { INodeCredentialTestResult, } from 'n8n-workflow'; import axios from 'axios'; +import { sleep } from 'n8n-workflow'; import type { CreateCredentialDto } from '@n8n/api-types'; +async function fetchCredentialTypesJsonWithRetry(url: string, retries = 5, delay = 500) { + for (let attempt = 0; attempt < retries; attempt++) { + const response = await axios.get(url, { withCredentials: true }); + + if ( + typeof response.data === 'object' && + response.data !== null && + Array.isArray(response.data) + ) { + return response.data; + } + + await sleep(delay * attempt); + } + + throw new Error('Could not fetch credential types'); +} + export async function getCredentialTypes(baseUrl: string): Promise { - const { data } = await axios.get(baseUrl + 'types/credentials.json', { withCredentials: true }); - return data; + return await fetchCredentialTypesJsonWithRetry(baseUrl + 'types/credentials.json'); } export async function getCredentialsNewName(