fix: Credentials overrides no longer blocks UI (#25830)

This commit is contained in:
Stephen Wright 2026-02-16 16:04:40 +00:00 committed by GitHub
parent f71bfff61c
commit 20175243da
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 90 additions and 31 deletions

View File

@ -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',
),
);
}
},
);

View File

@ -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<FrontendSettings> {
@ -604,27 +616,36 @@ export class FrontendService {
return Array.from(identifiers);
}
private writeStaticJSON(
private async writeStaticJSON(
name: string,
data: Array<INodeTypeBaseDescription | ICredentialType | string>,
) {
): Promise<void> {
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<void>((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)

View File

@ -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();

View File

@ -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<ICredentialType[]> {
const { data } = await axios.get(baseUrl + 'types/credentials.json', { withCredentials: true });
return data;
return await fetchCredentialTypesJsonWithRetry(baseUrl + 'types/credentials.json');
}
export async function getCredentialsNewName(