refactor(core): Port endpoints config (no-changelog) (#10268)

This commit is contained in:
Iván Ovejero 2024-07-31 17:45:11 +02:00 committed by GitHub
parent d91eb2cdd5
commit 1608d2527b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 275 additions and 228 deletions

View file

@ -0,0 +1,102 @@
import { Config, Env, Nested } from '../decorators';
@Config
class PrometheusMetricsConfig {
/** Whether to enable the `/metrics` endpoint to expose Prometheus metrics. */
@Env('N8N_METRICS')
readonly enable: boolean = false;
/** Prefix for Prometheus metric names. */
@Env('N8N_METRICS_PREFIX')
readonly prefix: string = 'n8n_';
/** Whether to expose system and Node.js metrics. See: https://www.npmjs.com/package/prom-client */
@Env('N8N_METRICS_INCLUDE_DEFAULT_METRICS')
readonly includeDefaultMetrics = true;
/** Whether to include a label for workflow ID on workflow metrics. */
@Env('N8N_METRICS_INCLUDE_WORKFLOW_ID_LABEL')
readonly includeWorkflowIdLabel: boolean = false;
/** Whether to include a label for node type on node metrics. */
@Env('N8N_METRICS_INCLUDE_NODE_TYPE_LABEL')
readonly includeNodeTypeLabel: boolean = false;
/** Whether to include a label for credential type on credential metrics. */
@Env('N8N_METRICS_INCLUDE_CREDENTIAL_TYPE_LABEL')
readonly includeCredentialTypeLabel: boolean = false;
/** Whether to expose metrics for API endpoints. See: https://www.npmjs.com/package/express-prom-bundle */
@Env('N8N_METRICS_INCLUDE_API_ENDPOINTS')
readonly includeApiEndpoints: boolean = false;
/** Whether to include a label for the path of API endpoint calls. */
@Env('N8N_METRICS_INCLUDE_API_PATH_LABEL')
readonly includeApiPathLabel: boolean = false;
/** Whether to include a label for the HTTP method of API endpoint calls. */
@Env('N8N_METRICS_INCLUDE_API_METHOD_LABEL')
readonly includeApiMethodLabel: boolean = false;
/** Whether to include a label for the status code of API endpoint calls. */
@Env('N8N_METRICS_INCLUDE_API_STATUS_CODE_LABEL')
readonly includeApiStatusCodeLabel: boolean = false;
/** Whether to include metrics for cache hits and misses. */
@Env('N8N_METRICS_INCLUDE_CACHE_METRICS')
readonly includeCacheMetrics: boolean = false;
/** Whether to include metrics derived from n8n's internal events */
@Env('N8N_METRICS_INCLUDE_MESSAGE_EVENT_BUS_METRICS')
readonly includeMessageEventBusMetrics: boolean = false;
}
@Config
export class EndpointsConfig {
/** Max payload size in MiB */
@Env('N8N_PAYLOAD_SIZE_MAX')
readonly payloadSizeMax: number = 16;
@Nested
readonly metrics: PrometheusMetricsConfig;
/** Path segment for REST API endpoints. */
@Env('N8N_ENDPOINT_REST')
readonly rest: string = 'rest';
/** Path segment for form endpoints. */
@Env('N8N_ENDPOINT_FORM')
readonly form: string = 'form';
/** Path segment for test form endpoints. */
@Env('N8N_ENDPOINT_FORM_TEST')
readonly formTest: string = 'form-test';
/** Path segment for waiting form endpoints. */
@Env('N8N_ENDPOINT_FORM_WAIT')
readonly formWaiting: string = 'form-waiting';
/** Path segment for webhook endpoints. */
@Env('N8N_ENDPOINT_WEBHOOK')
readonly webhook: string = 'webhook';
/** Path segment for test webhook endpoints. */
@Env('N8N_ENDPOINT_WEBHOOK_TEST')
readonly webhookTest: string = 'webhook-test';
/** Path segment for waiting webhook endpoints. */
@Env('N8N_ENDPOINT_WEBHOOK_WAIT')
readonly webhookWaiting: string = 'webhook-waiting';
/** Whether to disable n8n's UI (frontend). */
@Env('N8N_DISABLE_UI')
readonly disableUi: boolean = false;
/** Whether to disable production webhooks on the main process, when using webhook-specific processes. */
@Env('N8N_DISABLE_PRODUCTION_MAIN_PROCESS')
readonly disableProductionWebhooksOnMainProcess: boolean = false;
/** Colon-delimited list of additional endpoints to not open the UI on. */
@Env('N8N_ADDITIONAL_NON_UI_ROUTES')
readonly additionalNonUIRoutes: string = '';
}

View file

@ -10,6 +10,7 @@ import { EventBusConfig } from './configs/event-bus';
import { NodesConfig } from './configs/nodes'; import { NodesConfig } from './configs/nodes';
import { ExternalStorageConfig } from './configs/external-storage'; import { ExternalStorageConfig } from './configs/external-storage';
import { WorkflowsConfig } from './configs/workflows'; import { WorkflowsConfig } from './configs/workflows';
import { EndpointsConfig } from './configs/endpoints';
@Config @Config
class UserManagementConfig { class UserManagementConfig {
@ -71,4 +72,7 @@ export class GlobalConfig {
/** HTTP Protocol via which n8n can be reached */ /** HTTP Protocol via which n8n can be reached */
@Env('N8N_PROTOCOL') @Env('N8N_PROTOCOL')
readonly protocol: 'http' | 'https' = 'http'; readonly protocol: 'http' | 'https' = 'http';
@Nested
readonly endpoints: EndpointsConfig;
} }

View file

@ -145,12 +145,44 @@ describe('GlobalConfig', () => {
onboardingFlowDisabled: false, onboardingFlowDisabled: false,
callerPolicyDefaultOption: 'workflowsFromSameOwner', callerPolicyDefaultOption: 'workflowsFromSameOwner',
}, },
endpoints: {
metrics: {
enable: false,
prefix: 'n8n_',
includeWorkflowIdLabel: false,
includeDefaultMetrics: true,
includeMessageEventBusMetrics: false,
includeNodeTypeLabel: false,
includeCacheMetrics: false,
includeApiEndpoints: false,
includeApiPathLabel: false,
includeApiMethodLabel: false,
includeCredentialTypeLabel: false,
includeApiStatusCodeLabel: false,
},
additionalNonUIRoutes: '',
disableProductionWebhooksOnMainProcess: false,
disableUi: false,
form: 'form',
formTest: 'form-test',
formWaiting: 'form-waiting',
payloadSizeMax: 16,
rest: 'rest',
webhook: 'webhook',
webhookTest: 'webhook-test',
webhookWaiting: 'webhook-waiting',
},
}; };
it('should use all default values when no env variables are defined', () => { it('should use all default values when no env variables are defined', () => {
process.env = {}; process.env = {};
const config = Container.get(GlobalConfig); const config = Container.get(GlobalConfig);
expect(config).toEqual(defaultConfig);
// deepCopy for diff to show plain objects
// eslint-disable-next-line n8n-local-rules/no-json-parse-json-stringify
const deepCopy = <T>(obj: T): T => JSON.parse(JSON.stringify(obj));
expect(deepCopy(config)).toEqual(defaultConfig);
expect(mockFs.readFileSync).not.toHaveBeenCalled(); expect(mockFs.readFileSync).not.toHaveBeenCalled();
}); });

View file

@ -34,7 +34,7 @@ export abstract class AbstractServer {
protected externalHooks: ExternalHooks; protected externalHooks: ExternalHooks;
protected protocol = Container.get(GlobalConfig).protocol; protected globalConfig = Container.get(GlobalConfig);
protected sslKey: string; protected sslKey: string;
@ -74,15 +74,15 @@ export abstract class AbstractServer {
this.sslKey = config.getEnv('ssl_key'); this.sslKey = config.getEnv('ssl_key');
this.sslCert = config.getEnv('ssl_cert'); this.sslCert = config.getEnv('ssl_cert');
this.restEndpoint = config.getEnv('endpoints.rest'); this.restEndpoint = this.globalConfig.endpoints.rest;
this.endpointForm = config.getEnv('endpoints.form'); this.endpointForm = this.globalConfig.endpoints.form;
this.endpointFormTest = config.getEnv('endpoints.formTest'); this.endpointFormTest = this.globalConfig.endpoints.formTest;
this.endpointFormWaiting = config.getEnv('endpoints.formWaiting'); this.endpointFormWaiting = this.globalConfig.endpoints.formWaiting;
this.endpointWebhook = config.getEnv('endpoints.webhook'); this.endpointWebhook = this.globalConfig.endpoints.webhook;
this.endpointWebhookTest = config.getEnv('endpoints.webhookTest'); this.endpointWebhookTest = this.globalConfig.endpoints.webhookTest;
this.endpointWebhookWaiting = config.getEnv('endpoints.webhookWaiting'); this.endpointWebhookWaiting = this.globalConfig.endpoints.webhookWaiting;
this.uniqueInstanceId = generateHostInstanceId(instanceType); this.uniqueInstanceId = generateHostInstanceId(instanceType);
@ -134,7 +134,8 @@ export abstract class AbstractServer {
} }
async init(): Promise<void> { async init(): Promise<void> {
const { app, protocol, sslKey, sslCert } = this; const { app, sslKey, sslCert } = this;
const { protocol } = this.globalConfig;
if (protocol === 'https' && sslKey && sslCert) { if (protocol === 'https' && sslKey && sslCert) {
const https = await import('https'); const https = await import('https');
@ -261,14 +262,16 @@ export abstract class AbstractServer {
return; return;
} }
this.logger.debug(`Shutting down ${this.protocol} server`); const { protocol } = this.globalConfig;
this.logger.debug(`Shutting down ${protocol} server`);
this.server.close((error) => { this.server.close((error) => {
if (error) { if (error) {
this.logger.error(`Error while shutting down ${this.protocol} server`, { error }); this.logger.error(`Error while shutting down ${protocol} server`, { error });
} }
this.logger.debug(`${this.protocol} server shut down`); this.logger.debug(`${protocol} server shut down`);
}); });
} }
} }

View file

@ -6,7 +6,6 @@ import { promisify } from 'util';
import cookieParser from 'cookie-parser'; import cookieParser from 'cookie-parser';
import express from 'express'; import express from 'express';
import helmet from 'helmet'; import helmet from 'helmet';
import { GlobalConfig } from '@n8n/config';
import { InstanceSettings } from 'n8n-core'; import { InstanceSettings } from 'n8n-core';
import type { IN8nUISettings } from 'n8n-workflow'; import type { IN8nUISettings } from 'n8n-workflow';
@ -81,17 +80,16 @@ export class Server extends AbstractServer {
private readonly loadNodesAndCredentials: LoadNodesAndCredentials, private readonly loadNodesAndCredentials: LoadNodesAndCredentials,
private readonly orchestrationService: OrchestrationService, private readonly orchestrationService: OrchestrationService,
private readonly postHogClient: PostHogClient, private readonly postHogClient: PostHogClient,
private readonly globalConfig: GlobalConfig,
private readonly eventService: EventService, private readonly eventService: EventService,
) { ) {
super('main'); super('main');
this.testWebhooksEnabled = true; this.testWebhooksEnabled = true;
this.webhooksEnabled = !config.getEnv('endpoints.disableProductionWebhooksOnMainProcess'); this.webhooksEnabled = !this.globalConfig.endpoints.disableProductionWebhooksOnMainProcess;
} }
async start() { async start() {
if (!config.getEnv('endpoints.disableUi')) { if (!this.globalConfig.endpoints.disableUi) {
const { FrontendService } = await import('@/services/frontend.service'); const { FrontendService } = await import('@/services/frontend.service');
this.frontendService = Container.get(FrontendService); this.frontendService = Container.get(FrontendService);
} }
@ -133,7 +131,7 @@ export class Server extends AbstractServer {
await import('@/controllers/mfa.controller'); await import('@/controllers/mfa.controller');
} }
if (!config.getEnv('endpoints.disableUi')) { if (!this.globalConfig.endpoints.disableUi) {
await import('@/controllers/cta.controller'); await import('@/controllers/cta.controller');
} }
@ -167,7 +165,7 @@ export class Server extends AbstractServer {
} }
async configure(): Promise<void> { async configure(): Promise<void> {
if (config.getEnv('endpoints.metrics.enable')) { if (this.globalConfig.endpoints.metrics.enable) {
const { PrometheusMetricsService } = await import('@/metrics/prometheus-metrics.service'); const { PrometheusMetricsService } = await import('@/metrics/prometheus-metrics.service');
await Container.get(PrometheusMetricsService).init(this.app); await Container.get(PrometheusMetricsService).init(this.app);
} }
@ -307,7 +305,8 @@ export class Server extends AbstractServer {
this.app.use('/icons/@:scope/:packageName/*/*.(svg|png)', serveIcons); this.app.use('/icons/@:scope/:packageName/*/*.(svg|png)', serveIcons);
this.app.use('/icons/:packageName/*/*.(svg|png)', serveIcons); this.app.use('/icons/:packageName/*/*.(svg|png)', serveIcons);
const isTLSEnabled = this.protocol === 'https' && !!(this.sslKey && this.sslCert); const isTLSEnabled =
this.globalConfig.protocol === 'https' && !!(this.sslKey && this.sslCert);
const isPreviewMode = process.env.N8N_PREVIEW_MODE === 'true'; const isPreviewMode = process.env.N8N_PREVIEW_MODE === 'true';
const securityHeadersMiddleware = helmet({ const securityHeadersMiddleware = helmet({
contentSecurityPolicy: false, contentSecurityPolicy: false,
@ -341,7 +340,7 @@ export class Server extends AbstractServer {
this.restEndpoint, this.restEndpoint,
this.endpointPresetCredentials, this.endpointPresetCredentials,
isApiEnabled() ? '' : publicApiEndpoint, isApiEnabled() ? '' : publicApiEndpoint,
...config.getEnv('endpoints.additionalNonUIRoutes').split(':'), ...this.globalConfig.endpoints.additionalNonUIRoutes.split(':'),
].filter((u) => !!u); ].filter((u) => !!u);
const nonUIRoutesRegex = new RegExp(`^/(${nonUIRoutes.join('|')})/?.*$`); const nonUIRoutesRegex = new RegExp(`^/(${nonUIRoutes.join('|')})/?.*$`);
const historyApiHandler: express.RequestHandler = (req, res, next) => { const historyApiHandler: express.RequestHandler = (req, res, next) => {

View file

@ -1002,23 +1002,19 @@ export async function getBase(
): Promise<IWorkflowExecuteAdditionalData> { ): Promise<IWorkflowExecuteAdditionalData> {
const urlBaseWebhook = Container.get(UrlService).getWebhookBaseUrl(); const urlBaseWebhook = Container.get(UrlService).getWebhookBaseUrl();
const formWaitingBaseUrl = urlBaseWebhook + config.getEnv('endpoints.formWaiting'); const globalConfig = Container.get(GlobalConfig);
const webhookBaseUrl = urlBaseWebhook + config.getEnv('endpoints.webhook');
const webhookTestBaseUrl = urlBaseWebhook + config.getEnv('endpoints.webhookTest');
const webhookWaitingBaseUrl = urlBaseWebhook + config.getEnv('endpoints.webhookWaiting');
const variables = await WorkflowHelpers.getVariables(); const variables = await WorkflowHelpers.getVariables();
return { return {
credentialsHelper: Container.get(CredentialsHelper), credentialsHelper: Container.get(CredentialsHelper),
executeWorkflow, executeWorkflow,
restApiUrl: urlBaseWebhook + config.getEnv('endpoints.rest'), restApiUrl: urlBaseWebhook + globalConfig.endpoints.rest,
instanceBaseUrl: urlBaseWebhook, instanceBaseUrl: urlBaseWebhook,
formWaitingBaseUrl, formWaitingBaseUrl: globalConfig.endpoints.formWaiting,
webhookBaseUrl, webhookBaseUrl: globalConfig.endpoints.webhook,
webhookWaitingBaseUrl, webhookWaitingBaseUrl: globalConfig.endpoints.webhookWaiting,
webhookTestBaseUrl, webhookTestBaseUrl: globalConfig.endpoints.webhookTest,
currentNodeParameters, currentNodeParameters,
executionTimeoutTimestamp, executionTimeoutTimestamp,
userId, userId,

View file

@ -1,4 +1,4 @@
import { Service } from 'typedi'; import Container, { Service } from 'typedi';
import type { NextFunction, Response } from 'express'; import type { NextFunction, Response } from 'express';
import { createHash } from 'crypto'; import { createHash } from 'crypto';
import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken'; import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
@ -14,6 +14,7 @@ import { Logger } from '@/Logger';
import type { AuthenticatedRequest } from '@/requests'; import type { AuthenticatedRequest } from '@/requests';
import { JwtService } from '@/services/jwt.service'; import { JwtService } from '@/services/jwt.service';
import { UrlService } from '@/services/url.service'; import { UrlService } from '@/services/url.service';
import { GlobalConfig } from '@n8n/config';
interface AuthJwtPayload { interface AuthJwtPayload {
/** User Id */ /** User Id */
@ -33,7 +34,7 @@ interface PasswordResetToken {
hash: string; hash: string;
} }
const restEndpoint = config.get('endpoints.rest'); const restEndpoint = Container.get(GlobalConfig).endpoints.rest;
// The browser-id check needs to be skipped on these endpoints // The browser-id check needs to be skipped on these endpoints
const skipBrowserIdCheckEndpoints = [ const skipBrowserIdCheckEndpoints = [
// we need to exclude push endpoint because we can't send custom header on websocket requests // we need to exclude push endpoint because we can't send custom header on websocket requests

View file

@ -44,7 +44,7 @@ export abstract class BaseCommand extends Command {
protected license: License; protected license: License;
private globalConfig = Container.get(GlobalConfig); protected globalConfig = Container.get(GlobalConfig);
/** /**
* How long to wait for graceful shutdown before force killing the process. * How long to wait for graceful shutdown before force killing the process.

View file

@ -124,8 +124,8 @@ export class Start extends BaseCommand {
private async generateStaticAssets() { private async generateStaticAssets() {
// Read the index file and replace the path placeholder // Read the index file and replace the path placeholder
const n8nPath = Container.get(GlobalConfig).path; const n8nPath = this.globalConfig.path;
const restEndpoint = config.getEnv('endpoints.rest');
const hooksUrls = config.getEnv('externalFrontendHooksUrls'); const hooksUrls = config.getEnv('externalFrontendHooksUrls');
let scriptsString = ''; let scriptsString = '';
@ -151,7 +151,9 @@ export class Start extends BaseCommand {
]; ];
if (filePath.endsWith('index.html')) { if (filePath.endsWith('index.html')) {
streams.push( streams.push(
replaceStream('{{REST_ENDPOINT}}', restEndpoint, { ignoreCase: false }), replaceStream('{{REST_ENDPOINT}}', this.globalConfig.endpoints.rest, {
ignoreCase: false,
}),
replaceStream(closingTitleTag, closingTitleTag + scriptsString, { replaceStream(closingTitleTag, closingTitleTag + scriptsString, {
ignoreCase: false, ignoreCase: false,
}), }),
@ -201,7 +203,7 @@ export class Start extends BaseCommand {
this.initWorkflowHistory(); this.initWorkflowHistory();
this.logger.debug('Workflow history init complete'); this.logger.debug('Workflow history init complete');
if (!config.getEnv('endpoints.disableUi')) { if (!this.globalConfig.endpoints.disableUi) {
await this.generateStaticAssets(); await this.generateStaticAssets();
} }
} }

View file

@ -355,149 +355,6 @@ export const schema = {
}, },
}, },
endpoints: {
payloadSizeMax: {
format: Number,
default: 16,
env: 'N8N_PAYLOAD_SIZE_MAX',
doc: 'Maximum payload size in MB.',
},
metrics: {
enable: {
format: Boolean,
default: false,
env: 'N8N_METRICS',
doc: 'Enable /metrics endpoint. Default: false',
},
prefix: {
format: String,
default: 'n8n_',
env: 'N8N_METRICS_PREFIX',
doc: 'An optional prefix for metric names. Default: n8n_',
},
includeDefaultMetrics: {
format: Boolean,
default: true,
env: 'N8N_METRICS_INCLUDE_DEFAULT_METRICS',
doc: 'Whether to expose default system and node.js metrics. Default: true',
},
includeWorkflowIdLabel: {
format: Boolean,
default: false,
env: 'N8N_METRICS_INCLUDE_WORKFLOW_ID_LABEL',
doc: 'Whether to include a label for the workflow ID on workflow metrics. Default: false',
},
includeNodeTypeLabel: {
format: Boolean,
default: false,
env: 'N8N_METRICS_INCLUDE_NODE_TYPE_LABEL',
doc: 'Whether to include a label for the node type on node metrics. Default: false',
},
includeCredentialTypeLabel: {
format: Boolean,
default: false,
env: 'N8N_METRICS_INCLUDE_CREDENTIAL_TYPE_LABEL',
doc: 'Whether to include a label for the credential type on credential metrics. Default: false',
},
includeApiEndpoints: {
format: Boolean,
default: false,
env: 'N8N_METRICS_INCLUDE_API_ENDPOINTS',
doc: 'Whether to expose metrics for API endpoints. Default: false',
},
includeApiPathLabel: {
format: Boolean,
default: false,
env: 'N8N_METRICS_INCLUDE_API_PATH_LABEL',
doc: 'Whether to include a label for the path of API invocations. Default: false',
},
includeApiMethodLabel: {
format: Boolean,
default: false,
env: 'N8N_METRICS_INCLUDE_API_METHOD_LABEL',
doc: 'Whether to include a label for the HTTP method (GET, POST, ...) of API invocations. Default: false',
},
includeApiStatusCodeLabel: {
format: Boolean,
default: false,
env: 'N8N_METRICS_INCLUDE_API_STATUS_CODE_LABEL',
doc: 'Whether to include a label for the HTTP status code (200, 404, ...) of API invocations. Default: false',
},
includeCacheMetrics: {
format: Boolean,
default: false,
env: 'N8N_METRICS_INCLUDE_CACHE_METRICS',
doc: 'Whether to include metrics for cache hits and misses. Default: false',
},
includeMessageEventBusMetrics: {
format: Boolean,
default: true,
env: 'N8N_METRICS_INCLUDE_MESSAGE_EVENT_BUS_METRICS',
doc: 'Whether to include metrics for events. Default: false',
},
},
rest: {
format: String,
default: 'rest',
env: 'N8N_ENDPOINT_REST',
doc: 'Path for rest endpoint',
},
form: {
format: String,
default: 'form',
env: 'N8N_ENDPOINT_FORM',
doc: 'Path for form endpoint',
},
formTest: {
format: String,
default: 'form-test',
env: 'N8N_ENDPOINT_FORM_TEST',
doc: 'Path for test form endpoint',
},
formWaiting: {
format: String,
default: 'form-waiting',
env: 'N8N_ENDPOINT_FORM_WAIT',
doc: 'Path for waiting form endpoint',
},
webhook: {
format: String,
default: 'webhook',
env: 'N8N_ENDPOINT_WEBHOOK',
doc: 'Path for webhook endpoint',
},
webhookWaiting: {
format: String,
default: 'webhook-waiting',
env: 'N8N_ENDPOINT_WEBHOOK_WAIT',
doc: 'Path for waiting-webhook endpoint',
},
webhookTest: {
format: String,
default: 'webhook-test',
env: 'N8N_ENDPOINT_WEBHOOK_TEST',
doc: 'Path for test-webhook endpoint',
},
disableUi: {
format: Boolean,
default: false,
env: 'N8N_DISABLE_UI',
doc: 'Disable N8N UI (Frontend).',
},
disableProductionWebhooksOnMainProcess: {
format: Boolean,
default: false,
env: 'N8N_DISABLE_PRODUCTION_MAIN_PROCESS',
doc: 'Disable production webhooks from main process. This helps ensures no http traffic load to main process when using webhook-specific processes.',
},
additionalNonUIRoutes: {
doc: 'Additional endpoints to not open the UI on. Multiple endpoints can be separated by colon (":")',
format: String,
default: '',
env: 'N8N_ADDITIONAL_NON_UI_ROUTES',
},
},
workflowTagsDisabled: { workflowTagsDisabled: {
format: Boolean, format: Boolean,
default: false, default: false,

View file

@ -5,7 +5,6 @@ import { Credentials } from 'n8n-core';
import type { ICredentialDataDecryptedObject, IWorkflowExecuteAdditionalData } from 'n8n-workflow'; import type { ICredentialDataDecryptedObject, IWorkflowExecuteAdditionalData } from 'n8n-workflow';
import { jsonParse, ApplicationError } from 'n8n-workflow'; import { jsonParse, ApplicationError } from 'n8n-workflow';
import config from '@/config';
import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; import type { CredentialsEntity } from '@db/entities/CredentialsEntity';
import type { User } from '@db/entities/User'; import type { User } from '@db/entities/User';
import { CredentialsRepository } from '@db/repositories/credentials.repository'; import { CredentialsRepository } from '@db/repositories/credentials.repository';
@ -20,6 +19,7 @@ import { ExternalHooks } from '@/ExternalHooks';
import { UrlService } from '@/services/url.service'; import { UrlService } from '@/services/url.service';
import { BadRequestError } from '@/errors/response-errors/bad-request.error'; import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error'; import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { GlobalConfig } from '@n8n/config';
export interface CsrfStateParam { export interface CsrfStateParam {
cid: string; cid: string;
@ -37,10 +37,11 @@ export abstract class AbstractOAuthController {
private readonly credentialsRepository: CredentialsRepository, private readonly credentialsRepository: CredentialsRepository,
private readonly sharedCredentialsRepository: SharedCredentialsRepository, private readonly sharedCredentialsRepository: SharedCredentialsRepository,
private readonly urlService: UrlService, private readonly urlService: UrlService,
private readonly globalConfig: GlobalConfig,
) {} ) {}
get baseUrl() { get baseUrl() {
const restUrl = `${this.urlService.getInstanceBaseUrl()}/${config.getEnv('endpoints.rest')}`; const restUrl = `${this.urlService.getInstanceBaseUrl()}/${this.globalConfig.endpoints.rest}`;
return `${restUrl}/oauth${this.oauthVersion}-credential`; return `${restUrl}/oauth${this.oauthVersion}-credential`;
} }

View file

@ -4,7 +4,6 @@ import type { Application, Request, Response, RequestHandler } from 'express';
import { rateLimit as expressRateLimit } from 'express-rate-limit'; import { rateLimit as expressRateLimit } from 'express-rate-limit';
import { AuthService } from '@/auth/auth.service'; import { AuthService } from '@/auth/auth.service';
import config from '@/config';
import { UnauthenticatedError } from '@/errors/response-errors/unauthenticated.error'; import { UnauthenticatedError } from '@/errors/response-errors/unauthenticated.error';
import { inProduction, RESPONSE_ERROR_MESSAGES } from '@/constants'; import { inProduction, RESPONSE_ERROR_MESSAGES } from '@/constants';
import type { BooleanLicenseFeature } from '@/Interfaces'; import type { BooleanLicenseFeature } from '@/Interfaces';
@ -12,7 +11,7 @@ import { License } from '@/License';
import type { AuthenticatedRequest } from '@/requests'; import type { AuthenticatedRequest } from '@/requests';
import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file import { send } from '@/ResponseHelper'; // TODO: move `ResponseHelper.send` to this file
import { userHasScope } from '@/permissions/checkAccess'; import { userHasScope } from '@/permissions/checkAccess';
import { GlobalConfig } from '@n8n/config';
import type { import type {
AccessScope, AccessScope,
Controller, Controller,
@ -52,6 +51,7 @@ export class ControllerRegistry {
constructor( constructor(
private readonly license: License, private readonly license: License,
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly globalConfig: GlobalConfig,
) {} ) {}
activate(app: Application) { activate(app: Application) {
@ -64,7 +64,7 @@ export class ControllerRegistry {
const metadata = registry.get(controllerClass)!; const metadata = registry.get(controllerClass)!;
const router = Router({ mergeParams: true }); const router = Router({ mergeParams: true });
const prefix = `/${config.getEnv('endpoints.rest')}/${metadata.basePath}` const prefix = `/${this.globalConfig.endpoints.rest}/${metadata.basePath}`
.replace(/\/+/g, '/') .replace(/\/+/g, '/')
.replace(/\/$/, ''); .replace(/\/$/, '');
app.use(prefix, router); app.use(prefix, router);

View file

@ -5,6 +5,8 @@ import { mock } from 'jest-mock-extended';
import { PrometheusMetricsService } from '../prometheus-metrics.service'; import { PrometheusMetricsService } from '../prometheus-metrics.service';
import type express from 'express'; import type express from 'express';
import type { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; import type { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
import { mockInstance } from '@test/mocking';
import { GlobalConfig } from '@n8n/config';
const mockMiddleware = ( const mockMiddleware = (
_req: express.Request, _req: express.Request,
@ -16,13 +18,27 @@ jest.mock('prom-client');
jest.mock('express-prom-bundle', () => jest.fn(() => mockMiddleware)); jest.mock('express-prom-bundle', () => jest.fn(() => mockMiddleware));
describe('PrometheusMetricsService', () => { describe('PrometheusMetricsService', () => {
beforeEach(() => { const globalConfig = mockInstance(GlobalConfig, {
config.load(config.default); endpoints: {
metrics: {
prefix: 'n8n_',
includeDefaultMetrics: true,
includeApiEndpoints: true,
includeCacheMetrics: true,
includeMessageEventBusMetrics: true,
includeCredentialTypeLabel: false,
includeNodeTypeLabel: false,
includeWorkflowIdLabel: false,
includeApiPathLabel: true,
includeApiMethodLabel: true,
includeApiStatusCodeLabel: true,
},
},
}); });
describe('init', () => { describe('init', () => {
it('should set up `n8n_version_info`', async () => { it('should set up `n8n_version_info`', async () => {
const service = new PrometheusMetricsService(mock(), mock()); const service = new PrometheusMetricsService(mock(), mock(), globalConfig);
await service.init(mock<express.Application>()); await service.init(mock<express.Application>());
@ -34,7 +50,7 @@ describe('PrometheusMetricsService', () => {
}); });
it('should set up default metrics collection with `prom-client`', async () => { it('should set up default metrics collection with `prom-client`', async () => {
const service = new PrometheusMetricsService(mock(), mock()); const service = new PrometheusMetricsService(mock(), mock(), globalConfig);
await service.init(mock<express.Application>()); await service.init(mock<express.Application>());
@ -43,7 +59,7 @@ describe('PrometheusMetricsService', () => {
it('should set up `n8n_cache_hits_total`', async () => { it('should set up `n8n_cache_hits_total`', async () => {
config.set('endpoints.metrics.includeCacheMetrics', true); config.set('endpoints.metrics.includeCacheMetrics', true);
const service = new PrometheusMetricsService(mock(), mock()); const service = new PrometheusMetricsService(mock(), mock(), globalConfig);
await service.init(mock<express.Application>()); await service.init(mock<express.Application>());
@ -58,7 +74,7 @@ describe('PrometheusMetricsService', () => {
it('should set up `n8n_cache_misses_total`', async () => { it('should set up `n8n_cache_misses_total`', async () => {
config.set('endpoints.metrics.includeCacheMetrics', true); config.set('endpoints.metrics.includeCacheMetrics', true);
const service = new PrometheusMetricsService(mock(), mock()); const service = new PrometheusMetricsService(mock(), mock(), globalConfig);
await service.init(mock<express.Application>()); await service.init(mock<express.Application>());
@ -73,7 +89,7 @@ describe('PrometheusMetricsService', () => {
it('should set up `n8n_cache_updates_total`', async () => { it('should set up `n8n_cache_updates_total`', async () => {
config.set('endpoints.metrics.includeCacheMetrics', true); config.set('endpoints.metrics.includeCacheMetrics', true);
const service = new PrometheusMetricsService(mock(), mock()); const service = new PrometheusMetricsService(mock(), mock(), globalConfig);
await service.init(mock<express.Application>()); await service.init(mock<express.Application>());
@ -91,7 +107,7 @@ describe('PrometheusMetricsService', () => {
config.set('endpoints.metrics.includeApiPathLabel', true); config.set('endpoints.metrics.includeApiPathLabel', true);
config.set('endpoints.metrics.includeApiMethodLabel', true); config.set('endpoints.metrics.includeApiMethodLabel', true);
config.set('endpoints.metrics.includeApiStatusCodeLabel', true); config.set('endpoints.metrics.includeApiStatusCodeLabel', true);
const service = new PrometheusMetricsService(mock(), mock()); const service = new PrometheusMetricsService(mock(), mock(), globalConfig);
const app = mock<express.Application>(); const app = mock<express.Application>();
@ -122,7 +138,7 @@ describe('PrometheusMetricsService', () => {
it('should set up event bus metrics', async () => { it('should set up event bus metrics', async () => {
const eventBus = mock<MessageEventBus>(); const eventBus = mock<MessageEventBus>();
const service = new PrometheusMetricsService(mock(), eventBus); const service = new PrometheusMetricsService(mock(), eventBus, globalConfig);
await service.init(mock<express.Application>()); await service.init(mock<express.Application>());

View file

@ -1,4 +1,3 @@
import config from '@/config';
import { N8N_VERSION } from '@/constants'; import { N8N_VERSION } from '@/constants';
import type express from 'express'; import type express from 'express';
import promBundle from 'express-prom-bundle'; import promBundle from 'express-prom-bundle';
@ -11,32 +10,34 @@ import { MessageEventBus } from '@/eventbus/MessageEventBus/MessageEventBus';
import { EventMessageTypeNames } from 'n8n-workflow'; import { EventMessageTypeNames } from 'n8n-workflow';
import type { EventMessageTypes } from '@/eventbus'; import type { EventMessageTypes } from '@/eventbus';
import type { Includes, MetricCategory, MetricLabel } from './types'; import type { Includes, MetricCategory, MetricLabel } from './types';
import { GlobalConfig } from '@n8n/config';
@Service() @Service()
export class PrometheusMetricsService { export class PrometheusMetricsService {
constructor( constructor(
private readonly cacheService: CacheService, private readonly cacheService: CacheService,
private readonly eventBus: MessageEventBus, private readonly eventBus: MessageEventBus,
private readonly globalConfig: GlobalConfig,
) {} ) {}
private readonly counters: { [key: string]: Counter<string> | null } = {}; private readonly counters: { [key: string]: Counter<string> | null } = {};
private readonly prefix = config.getEnv('endpoints.metrics.prefix'); private readonly prefix = this.globalConfig.endpoints.metrics.prefix;
private readonly includes: Includes = { private readonly includes: Includes = {
metrics: { metrics: {
default: config.getEnv('endpoints.metrics.includeDefaultMetrics'), default: this.globalConfig.endpoints.metrics.includeDefaultMetrics,
routes: config.getEnv('endpoints.metrics.includeApiEndpoints'), routes: this.globalConfig.endpoints.metrics.includeApiEndpoints,
cache: config.getEnv('endpoints.metrics.includeCacheMetrics'), cache: this.globalConfig.endpoints.metrics.includeCacheMetrics,
logs: config.getEnv('endpoints.metrics.includeMessageEventBusMetrics'), logs: this.globalConfig.endpoints.metrics.includeMessageEventBusMetrics,
}, },
labels: { labels: {
credentialsType: config.getEnv('endpoints.metrics.includeCredentialTypeLabel'), credentialsType: this.globalConfig.endpoints.metrics.includeCredentialTypeLabel,
nodeType: config.getEnv('endpoints.metrics.includeNodeTypeLabel'), nodeType: this.globalConfig.endpoints.metrics.includeNodeTypeLabel,
workflowId: config.getEnv('endpoints.metrics.includeWorkflowIdLabel'), workflowId: this.globalConfig.endpoints.metrics.includeWorkflowIdLabel,
apiPath: config.getEnv('endpoints.metrics.includeApiPathLabel'), apiPath: this.globalConfig.endpoints.metrics.includeApiPathLabel,
apiMethod: config.getEnv('endpoints.metrics.includeApiMethodLabel'), apiMethod: this.globalConfig.endpoints.metrics.includeApiMethodLabel,
apiStatusCode: config.getEnv('endpoints.metrics.includeApiStatusCodeLabel'), apiStatusCode: this.globalConfig.endpoints.metrics.includeApiStatusCodeLabel,
}, },
}; };

View file

@ -6,8 +6,9 @@ import { parse as parseQueryString } from 'querystring';
import { Parser as XmlParser } from 'xml2js'; import { Parser as XmlParser } from 'xml2js';
import { parseIncomingMessage } from 'n8n-core'; import { parseIncomingMessage } from 'n8n-core';
import { jsonParse } from 'n8n-workflow'; import { jsonParse } from 'n8n-workflow';
import config from '@/config';
import { UnprocessableRequestError } from '@/errors/response-errors/unprocessable.error'; import { UnprocessableRequestError } from '@/errors/response-errors/unprocessable.error';
import { GlobalConfig } from '@n8n/config';
import Container from 'typedi';
const xmlParser = new XmlParser({ const xmlParser = new XmlParser({
async: true, async: true,
@ -16,7 +17,7 @@ const xmlParser = new XmlParser({
explicitArray: false, // Only put properties in array if length > 1 explicitArray: false, // Only put properties in array if length > 1
}); });
const payloadSizeMax = config.getEnv('endpoints.payloadSizeMax'); const payloadSizeMax = Container.get(GlobalConfig).endpoints.payloadSizeMax;
export const rawBodyReader: RequestHandler = async (req, _res, next) => { export const rawBodyReader: RequestHandler = async (req, _res, next) => {
parseIncomingMessage(req); parseIncomingMessage(req);

View file

@ -66,7 +66,7 @@ export class FrontendService {
private initSettings() { private initSettings() {
const instanceBaseUrl = this.urlService.getInstanceBaseUrl(); const instanceBaseUrl = this.urlService.getInstanceBaseUrl();
const restEndpoint = config.getEnv('endpoints.rest'); const restEndpoint = this.globalConfig.endpoints.rest;
const telemetrySettings: ITelemetrySettings = { const telemetrySettings: ITelemetrySettings = {
enabled: config.getEnv('diagnostics.enabled'), enabled: config.getEnv('diagnostics.enabled'),
@ -88,11 +88,11 @@ export class FrontendService {
isDocker: this.isDocker(), isDocker: this.isDocker(),
databaseType: this.globalConfig.database.type, databaseType: this.globalConfig.database.type,
previewMode: process.env.N8N_PREVIEW_MODE === 'true', previewMode: process.env.N8N_PREVIEW_MODE === 'true',
endpointForm: config.getEnv('endpoints.form'), endpointForm: this.globalConfig.endpoints.form,
endpointFormTest: config.getEnv('endpoints.formTest'), endpointFormTest: this.globalConfig.endpoints.formTest,
endpointFormWaiting: config.getEnv('endpoints.formWaiting'), endpointFormWaiting: this.globalConfig.endpoints.formWaiting,
endpointWebhook: config.getEnv('endpoints.webhook'), endpointWebhook: this.globalConfig.endpoints.webhook,
endpointWebhookTest: config.getEnv('endpoints.webhookTest'), endpointWebhookTest: this.globalConfig.endpoints.webhookTest,
saveDataErrorExecution: config.getEnv('executions.saveDataOnError'), saveDataErrorExecution: config.getEnv('executions.saveDataOnError'),
saveDataSuccessExecution: config.getEnv('executions.saveDataOnSuccess'), saveDataSuccessExecution: config.getEnv('executions.saveDataOnSuccess'),
saveManualExecutions: config.getEnv('executions.saveDataManualExecutions'), saveManualExecutions: config.getEnv('executions.saveDataManualExecutions'),
@ -246,7 +246,7 @@ export class FrontendService {
getSettings(pushRef?: string): IN8nUISettings { getSettings(pushRef?: string): IN8nUISettings {
this.internalHooks.onFrontendSettingsAPI(pushRef); this.internalHooks.onFrontendSettingsAPI(pushRef);
const restEndpoint = config.getEnv('endpoints.rest'); const restEndpoint = this.globalConfig.endpoints.rest;
// Update all urls, in case `WEBHOOK_URL` was updated by `--tunnel` // Update all urls, in case `WEBHOOK_URL` was updated by `--tunnel`
const instanceBaseUrl = this.urlService.getInstanceBaseUrl(); const instanceBaseUrl = this.urlService.getInstanceBaseUrl();

View file

@ -444,9 +444,8 @@ export class TelemetryEventRelay {
version_cli: N8N_VERSION, version_cli: N8N_VERSION,
db_type: this.globalConfig.database.type, db_type: this.globalConfig.database.type,
n8n_version_notifications_enabled: this.globalConfig.versionNotifications.enabled, n8n_version_notifications_enabled: this.globalConfig.versionNotifications.enabled,
n8n_disable_production_main_process: config.getEnv( n8n_disable_production_main_process:
'endpoints.disableProductionWebhooksOnMainProcess', this.globalConfig.endpoints.disableProductionWebhooksOnMainProcess,
),
system_info: { system_info: {
os: { os: {
type: os.type(), type: os.type(),

View file

@ -2,17 +2,48 @@ import { Container } from 'typedi';
import { parse as semverParse } from 'semver'; import { parse as semverParse } from 'semver';
import request, { type Response } from 'supertest'; import request, { type Response } from 'supertest';
import config from '@/config';
import { N8N_VERSION } from '@/constants'; import { N8N_VERSION } from '@/constants';
import { PrometheusMetricsService } from '@/metrics/prometheus-metrics.service'; import { PrometheusMetricsService } from '@/metrics/prometheus-metrics.service';
import { setupTestServer } from './shared/utils'; import { setupTestServer } from './shared/utils';
import { mockInstance } from '@test/mocking';
import { GlobalConfig } from '@n8n/config';
jest.unmock('@/eventbus/MessageEventBus/MessageEventBus'); jest.unmock('@/eventbus/MessageEventBus/MessageEventBus');
const toLines = (response: Response) => response.text.trim().split('\n'); const toLines = (response: Response) => response.text.trim().split('\n');
config.set('endpoints.metrics.enable', true); mockInstance(GlobalConfig, {
config.set('endpoints.metrics.prefix', 'n8n_test_'); database: {
type: 'sqlite',
sqlite: {
database: 'database.sqlite',
enableWAL: false,
executeVacuumOnStartup: false,
poolSize: 0,
},
logging: {
enabled: false,
maxQueryExecutionTime: 0,
options: 'error',
},
tablePrefix: '',
},
endpoints: {
metrics: {
prefix: 'n8n_test_',
includeDefaultMetrics: true,
includeApiEndpoints: true,
includeCacheMetrics: true,
includeMessageEventBusMetrics: true,
includeCredentialTypeLabel: false,
includeNodeTypeLabel: false,
includeWorkflowIdLabel: false,
includeApiPathLabel: true,
includeApiMethodLabel: true,
includeApiStatusCodeLabel: true,
},
},
});
const server = setupTestServer({ endpointGroups: ['metrics'] }); const server = setupTestServer({ endpointGroups: ['metrics'] });
const agent = request.agent(server.app); const agent = request.agent(server.app);

View file

@ -1,8 +1,7 @@
import config from '@/config';
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import Container from 'typedi'; import Container from 'typedi';
export const REST_PATH_SEGMENT = config.getEnv('endpoints.rest'); export const REST_PATH_SEGMENT = Container.get(GlobalConfig).endpoints.rest;
export const PUBLIC_API_REST_PATH_SEGMENT = Container.get(GlobalConfig).publicApi.path; export const PUBLIC_API_REST_PATH_SEGMENT = Container.get(GlobalConfig).publicApi.path;

View file

@ -10,16 +10,18 @@ import { ControllerRegistry, Get, Licensed, RestController } from '@/decorators'
import type { AuthService } from '@/auth/auth.service'; import type { AuthService } from '@/auth/auth.service';
import type { License } from '@/License'; import type { License } from '@/License';
import type { SuperAgentTest } from '@test-integration/types'; import type { SuperAgentTest } from '@test-integration/types';
import type { GlobalConfig } from '@n8n/config';
describe('ControllerRegistry', () => { describe('ControllerRegistry', () => {
const license = mock<License>(); const license = mock<License>();
const authService = mock<AuthService>(); const authService = mock<AuthService>();
const globalConfig = mock<GlobalConfig>({ endpoints: { rest: 'rest' } });
let agent: SuperAgentTest; let agent: SuperAgentTest;
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks(); jest.resetAllMocks();
const app = express(); const app = express();
new ControllerRegistry(license, authService).activate(app); new ControllerRegistry(license, authService, globalConfig).activate(app);
agent = testAgent(app); agent = testAgent(app);
}); });

View file

@ -2,7 +2,6 @@ import type SuperAgentTest from 'supertest/lib/agent';
import { agent as testAgent } from 'supertest'; import { agent as testAgent } from 'supertest';
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import config from '@/config';
import { AbstractServer } from '@/AbstractServer'; import { AbstractServer } from '@/AbstractServer';
import { ActiveWebhooks } from '@/ActiveWebhooks'; import { ActiveWebhooks } from '@/ActiveWebhooks';
import { ExternalHooks } from '@/ExternalHooks'; import { ExternalHooks } from '@/ExternalHooks';
@ -13,6 +12,8 @@ import { WaitingForms } from '@/WaitingForms';
import type { IResponseCallbackData } from '@/Interfaces'; import type { IResponseCallbackData } from '@/Interfaces';
import { mockInstance } from '../shared/mocking'; import { mockInstance } from '../shared/mocking';
import { GlobalConfig } from '@n8n/config';
import Container from 'typedi';
let agent: SuperAgentTest; let agent: SuperAgentTest;
@ -46,7 +47,7 @@ describe('WebhookServer', () => {
for (const [key, manager] of tests) { for (const [key, manager] of tests) {
describe(`for ${key}`, () => { describe(`for ${key}`, () => {
it('should handle preflight requests', async () => { it('should handle preflight requests', async () => {
const pathPrefix = config.getEnv(`endpoints.${key}`); const pathPrefix = Container.get(GlobalConfig).endpoints[key];
manager.getWebhookMethods.mockResolvedValueOnce(['GET']); manager.getWebhookMethods.mockResolvedValueOnce(['GET']);
const response = await agent const response = await agent
@ -60,7 +61,7 @@ describe('WebhookServer', () => {
}); });
it('should handle regular requests', async () => { it('should handle regular requests', async () => {
const pathPrefix = config.getEnv(`endpoints.${key}`); const pathPrefix = Container.get(GlobalConfig).endpoints[key];
manager.getWebhookMethods.mockResolvedValueOnce(['GET']); manager.getWebhookMethods.mockResolvedValueOnce(['GET']);
manager.executeWebhook.mockResolvedValueOnce( manager.executeWebhook.mockResolvedValueOnce(
mockResponse({ test: true }, { key: 'value ' }), mockResponse({ test: true }, { key: 'value ' }),