fix(core): Preserve underlying cause when logging webhook execution failures (#31120)

This commit is contained in:
Matt Carabine 2026-05-28 12:23:16 +01:00 committed by GitHub
parent 383928ea3f
commit b2f4c2c6e4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 48 additions and 17 deletions

View File

@ -1,7 +1,9 @@
import { Logger } from '@n8n/backend-common';
import { mockInstance } from '@n8n/backend-test-utils';
import { type Response } from 'express';
import { mock } from 'jest-mock-extended';
import { isWebhookHtmlSandboxingDisabled, getHtmlSandboxCSP } from 'n8n-core';
import { randomString } from 'n8n-workflow';
import { OperationalError, randomString } from 'n8n-workflow';
import type { IHttpRequestMethods } from 'n8n-workflow';
import { ResponseError } from '@/errors/response-errors/abstract/response.error';
@ -20,6 +22,7 @@ jest.mock('n8n-core', () => ({
}));
describe('WebhookRequestHandler', () => {
const logger = mockInstance(Logger);
const webhookManager = mock<Required<IWebhookManager>>();
const handler = createWebhookHandlerFor(webhookManager) as (
req: WebhookRequest | WebhookOptionsRequest,
@ -209,6 +212,31 @@ describe('WebhookRequestHandler', () => {
});
});
it('should log the underlying error cause when execution fails', async () => {
const req = mock<WebhookRequest>({
path: '/webhook/abc',
method: 'GET',
params: { path: 'abc' },
});
const res = mock<Response>();
res.status.mockReturnValue(res);
const rootCause = new Error('SQLITE_BUSY: database is locked');
const wrapper = new OperationalError('There was a problem executing the workflow', {
cause: rootCause,
});
webhookManager.executeWebhook.mockRejectedValueOnce(wrapper);
await handler(req, res);
expect(logger.error).toHaveBeenCalledWith(
'Error in handling webhook request GET /webhook/abc: There was a problem executing the workflow',
expect.objectContaining({ error: wrapper }),
);
});
it('should not throw when legacy response headers contain invalid names', async () => {
const req = mock<WebhookRequest>({
path: '/',

View File

@ -10,6 +10,7 @@ import type { Project } from '@n8n/db';
import { ExecutionRepository } from '@n8n/db';
import { Container } from '@n8n/di';
import type express from 'express';
import merge from 'lodash/merge';
import { BinaryDataService, ErrorReporter, WAITING_TOKEN_QUERY_PARAM } from 'n8n-core';
import type {
IBinaryData,
@ -50,20 +51,13 @@ import {
} from 'n8n-workflow';
import { finished } from 'stream/promises';
import { WebhookService } from './webhook.service';
import {
WebhookResponseHeaders,
type WebhookNodeResponseHeaders,
} from './webhook-response-headers';
import type { IWebhookResponseCallbackData, WebhookRequest } from './webhook.types';
import { ActiveExecutions } from '@/active-executions';
import { AuthService } from '@/auth/auth.service';
import { MCP_TRIGGER_NODE_TYPE } from '@/constants';
import { EventService } from '@/events/event.service';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { NotFoundError } from '@/errors/response-errors/not-found.error';
import { UnprocessableRequestError } from '@/errors/response-errors/unprocessable.error';
import { EventService } from '@/events/event.service';
import { parseBody } from '@/middlewares';
import { OwnershipService } from '@/services/ownership.service';
import { WorkflowStatisticsService } from '@/services/workflow-statistics.service';
@ -77,7 +71,13 @@ import { createStaticResponse, createStreamResponse } from '@/webhooks/webhook-r
import * as WorkflowExecuteAdditionalData from '@/workflow-execute-additional-data';
import * as WorkflowHelpers from '@/workflow-helpers';
import { WorkflowRunner } from '@/workflow-runner';
import merge from 'lodash/merge';
import {
WebhookResponseHeaders,
type WebhookNodeResponseHeaders,
} from './webhook-response-headers';
import { WebhookService } from './webhook.service';
import type { IWebhookResponseCallbackData, WebhookRequest } from './webhook.types';
// Type guards for MCP queue mode data validation
interface McpToolCallPayload {
@ -944,6 +944,8 @@ export async function executeWebhook(
return runData;
})
.catch((e) => {
Container.get(ErrorReporter).error(e, { executionId });
if (!didSendResponse) {
responseCallback(
new OperationalError('There was a problem executing the workflow', {
@ -960,12 +962,13 @@ export async function executeWebhook(
}
return executionId;
} catch (e) {
const error =
e instanceof UnprocessableRequestError
? e
: new OperationalError('There was a problem executing the workflow', {
cause: e,
});
let error: Error;
if (e instanceof UnprocessableRequestError) {
error = e;
} else {
Container.get(ErrorReporter).error(e, { executionId });
error = new OperationalError('There was a problem executing the workflow', { cause: e });
}
if (didSendResponse) throw error;
responseCallback(error, {});
return;

View File

@ -84,7 +84,7 @@ class WebhookRequestHandler {
} else {
logger.error(
`Error in handling webhook request ${req.method} ${req.path}: ${error.message}`,
{ stacktrace: error.stack },
{ error },
);
}