fix(core): Show a single error toast when a manual execution fails pre-flight validation (#34541)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mike Repeć 2026-07-20 12:56:46 +02:00 committed by GitHub
parent 5cb674ed37
commit 8d1fc022bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 60 additions and 1 deletions

View File

@ -9,7 +9,7 @@ import {
} from '@n8n/db';
import { Container, Service } from '@n8n/di';
import type { Response } from 'express';
import { DirectedGraph, WorkflowExecute } from 'n8n-core';
import { DirectedGraph, WorkflowExecute, WorkflowHasIssuesError } from 'n8n-core';
import * as core from 'n8n-core';
import {
type IExecuteData,
@ -357,6 +357,59 @@ describe('run', () => {
await expect(runner.run(data)).resolves.toBe('1');
});
});
describe('workflow issues pre-flight failure', () => {
function arrangeFailingRunDeps(error: Error) {
const activeExecutions = Container.get(ActiveExecutions);
vi.spyOn(activeExecutions, 'add').mockResolvedValue('1');
vi.spyOn(Container.get(CredentialsPermissionChecker), 'check').mockResolvedValueOnce();
vi.spyOn(WorkflowExecute.prototype, 'processRunExecutionData').mockImplementationOnce(() => {
throw error;
});
vi.spyOn(WorkflowExecuteAdditionalData, 'getBase').mockResolvedValue(
mock<IWorkflowExecuteAdditionalData>(),
);
const data = mock<IWorkflowExecutionDataProcess>({
workflowData: { nodes: [], id: 'workflow-id', settings: undefined, staticData: {} },
executionData: createRunExecutionData({}),
triggerToStartFrom: undefined,
startNodes: undefined,
destinationNode: undefined,
userId: 'mock-user-id',
});
return { data };
}
it('surfaces a WorkflowHasIssuesError as a failed run instead of rejecting', async () => {
const error = new WorkflowHasIssuesError(
{ node1: { parameters: { field: ['is missing'] } } },
{},
);
const { data } = arrangeFailingRunDeps(error);
// @ts-expect-error Private method
const failExecution = vi.spyOn(runner, 'failExecution').mockResolvedValueOnce();
const processError = vi.spyOn(runner, 'processError').mockResolvedValueOnce();
await expect(runner.run(data)).resolves.toBe('1');
expect(failExecution).toHaveBeenCalledWith(data, '1', error);
expect(processError).not.toHaveBeenCalled();
});
it('still rejects on other startup errors', async () => {
const error = new Error('boom');
const { data } = arrangeFailingRunDeps(error);
// @ts-expect-error Private method
const failExecution = vi.spyOn(runner, 'failExecution').mockResolvedValueOnce();
const processError = vi.spyOn(runner, 'processError').mockResolvedValueOnce();
await expect(runner.run(data)).rejects.toThrowError(error);
expect(processError).toHaveBeenCalled();
expect(failExecution).not.toHaveBeenCalled();
});
});
});
describe('enqueueExecution', () => {

View File

@ -12,6 +12,7 @@ import {
InstanceSettings,
StorageConfig,
WorkflowExecute,
WorkflowHasIssuesError,
} from 'n8n-core';
import type {
ExecutionError,
@ -479,6 +480,11 @@ export class WorkflowRunner {
),
);
} catch (error) {
if (error instanceof WorkflowHasIssuesError) {
await this.failExecution(data, executionId, error);
return;
}
await this.processError(
error,
new Date(),