fix(core): Use editor base URL for workflow and execution links (#23630)

Signed-off-by: majiayu000 <majiayu000@gmail.com>
Signed-off-by: majiayu000 <1835304752@qq.com>
Co-authored-by: Matsu <huhta.matias@gmail.com>
This commit is contained in:
lif 2026-04-30 16:25:26 +08:00 committed by GitHub
parent 83250c1710
commit 896461bee3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 138 additions and 5 deletions

View File

@ -682,8 +682,10 @@ describe('WorkflowExecuteAdditionalData', () => {
});
describe('getBase', () => {
const mockWebhookBaseUrl = 'webhook-base-url.com';
const mockWebhookBaseUrl = 'https://webhook.example.com/';
const mockInstanceBaseUrl = 'https://editor.example.com';
jest.spyOn(urlService, 'getWebhookBaseUrl').mockReturnValue(mockWebhookBaseUrl);
jest.spyOn(urlService, 'getInstanceBaseUrl').mockReturnValue(mockInstanceBaseUrl);
const globalConfig = mockInstance(GlobalConfig);
Container.set(GlobalConfig, globalConfig);
@ -706,7 +708,7 @@ describe('WorkflowExecuteAdditionalData', () => {
credentialsHelper,
executeWorkflow: expect.any(Function),
restApiUrl: `${mockWebhookBaseUrl}/rest/`,
instanceBaseUrl: mockWebhookBaseUrl,
instanceBaseUrl: `${mockInstanceBaseUrl}/`,
formWaitingBaseUrl: `${mockWebhookBaseUrl}/form-waiting/`,
webhookBaseUrl: `${mockWebhookBaseUrl}/webhook/`,
webhookWaitingBaseUrl: `${mockWebhookBaseUrl}/webhook-waiting/`,

View File

@ -0,0 +1,129 @@
import { Logger } from '@n8n/backend-common';
import { mockInstance } from '@n8n/backend-test-utils';
import { GlobalConfig } from '@n8n/config';
import { Container } from '@n8n/di';
import { mock } from 'jest-mock-extended';
import { ErrorReporter } from 'n8n-core';
import type { INode, IRun, IWorkflowBase } from 'n8n-workflow';
import { createRunExecutionData, NodeOperationError } from 'n8n-workflow';
import { OwnershipService } from '@/services/ownership.service';
import { UrlService } from '@/services/url.service';
import { WorkflowExecutionService } from '@/workflows/workflow-execution.service';
import { executeErrorWorkflow } from '../execute-error-workflow';
describe('executeErrorWorkflow', () => {
mockInstance(Logger);
mockInstance(ErrorReporter);
const globalConfig = mockInstance(GlobalConfig);
const urlService = mockInstance(UrlService);
const ownershipService = mockInstance(OwnershipService);
const workflowExecutionService = mockInstance(WorkflowExecutionService);
Container.set(GlobalConfig, globalConfig);
Container.set(UrlService, urlService);
Container.set(OwnershipService, ownershipService);
Container.set(WorkflowExecutionService, workflowExecutionService);
const mockNode = mock<INode>({
name: 'TestNode',
type: 'n8n-nodes-base.set',
typeVersion: 1,
position: [0, 0],
parameters: {},
});
beforeEach(() => {
jest.resetAllMocks();
globalConfig.nodes = mock<GlobalConfig['nodes']>({
errorTriggerType: 'n8n-nodes-base.errorTrigger',
});
});
describe('pastExecutionUrl', () => {
it('should use getInstanceBaseUrl for pastExecutionUrl', () => {
const mockInstanceBaseUrl = 'https://editor.example.com';
urlService.getInstanceBaseUrl.mockReturnValue(mockInstanceBaseUrl);
const workflowData = mock<IWorkflowBase>({
id: 'workflow-123',
name: 'Test Workflow',
settings: { errorWorkflow: 'error-workflow-456' },
nodes: [],
});
const testError = new NodeOperationError(mockNode, 'Test error');
const fullRunData: IRun = {
data: createRunExecutionData({
resultData: {
error: testError,
lastNodeExecuted: 'TestNode',
runData: {},
},
}),
mode: 'trigger',
startedAt: new Date(),
storedAt: 'db',
status: 'error',
};
const mockProject = { id: 'project-123' };
ownershipService.getWorkflowProjectCached.mockResolvedValue(mockProject as never);
workflowExecutionService.executeErrorWorkflow.mockResolvedValue(undefined);
executeErrorWorkflow(workflowData, fullRunData, 'trigger', 'execution-789');
expect(urlService.getInstanceBaseUrl).toHaveBeenCalled();
});
it('should construct correct pastExecutionUrl format with instanceBaseUrl', async () => {
const mockInstanceBaseUrl = 'https://editor.example.com';
urlService.getInstanceBaseUrl.mockReturnValue(mockInstanceBaseUrl);
const workflowData = mock<IWorkflowBase>({
id: 'workflow-123',
name: 'Test Workflow',
settings: { errorWorkflow: 'error-workflow-456' },
nodes: [],
});
const testError = new NodeOperationError(mockNode, 'Test error');
const fullRunData: IRun = {
data: createRunExecutionData({
resultData: {
error: testError,
lastNodeExecuted: 'TestNode',
runData: {},
},
}),
mode: 'trigger',
startedAt: new Date(),
storedAt: 'db',
status: 'error',
};
const mockProject = { id: 'project-123' };
ownershipService.getWorkflowProjectCached.mockResolvedValue(mockProject as never);
let capturedWorkflowErrorData: unknown;
workflowExecutionService.executeErrorWorkflow.mockImplementation(
async (_errorWorkflow, workflowErrorData) => {
capturedWorkflowErrorData = workflowErrorData;
},
);
executeErrorWorkflow(workflowData, fullRunData, 'trigger', 'execution-789');
// Wait for async operations
await new Promise(process.nextTick);
expect(capturedWorkflowErrorData).toMatchObject({
execution: {
id: 'execution-789',
url: 'https://editor.example.com/workflow/workflow-123/executions/execution-789',
},
});
});
});
});

View File

@ -31,7 +31,7 @@ export function executeErrorWorkflow(
// Check if there was an error and if so if an errorWorkflow or a trigger is set
let pastExecutionUrl: string | undefined;
if (executionId !== undefined) {
pastExecutionUrl = `${Container.get(UrlService).getWebhookBaseUrl()}workflow/${
pastExecutionUrl = `${Container.get(UrlService).getInstanceBaseUrl()}/workflow/${
workflowData.id
}/executions/${executionId}`;
}

View File

@ -471,7 +471,9 @@ export async function getBase({
executionTimeoutTimestamp?: number;
workflowSettings?: IWorkflowSettings;
} = {}): Promise<IWorkflowExecuteAdditionalData> {
const urlBaseWebhook = Container.get(UrlService).getWebhookBaseUrl();
const urlService = Container.get(UrlService);
const urlBaseWebhook = urlService.getWebhookBaseUrl();
const instanceBaseUrl = urlService.getInstanceBaseUrl();
const globalConfig = Container.get(GlobalConfig);
@ -484,7 +486,7 @@ export async function getBase({
credentialsHelper: Container.get(CredentialsHelper),
executeWorkflow,
restApiUrl: urlBaseWebhook + globalConfig.endpoints.rest,
instanceBaseUrl: urlBaseWebhook,
instanceBaseUrl: `${instanceBaseUrl}/`,
formWaitingBaseUrl: urlBaseWebhook + globalConfig.endpoints.formWaiting,
webhookBaseUrl: urlBaseWebhook + globalConfig.endpoints.webhook,
webhookWaitingBaseUrl: urlBaseWebhook + globalConfig.endpoints.webhookWaiting,