From b2f4c2c6e45ade2987015ebbb2ea4b8b07222eca Mon Sep 17 00:00:00 2001 From: Matt Carabine Date: Thu, 28 May 2026 12:23:16 +0100 Subject: [PATCH] fix(core): Preserve underlying cause when logging webhook execution failures (#31120) --- .../__tests__/webhook-request-handler.test.ts | 30 ++++++++++++++++- packages/cli/src/webhooks/webhook-helpers.ts | 33 ++++++++++--------- .../src/webhooks/webhook-request-handler.ts | 2 +- 3 files changed, 48 insertions(+), 17 deletions(-) diff --git a/packages/cli/src/webhooks/__tests__/webhook-request-handler.test.ts b/packages/cli/src/webhooks/__tests__/webhook-request-handler.test.ts index b600d7b9485..38f2b33b195 100644 --- a/packages/cli/src/webhooks/__tests__/webhook-request-handler.test.ts +++ b/packages/cli/src/webhooks/__tests__/webhook-request-handler.test.ts @@ -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>(); 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({ + path: '/webhook/abc', + method: 'GET', + params: { path: 'abc' }, + }); + + const res = mock(); + 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({ path: '/', diff --git a/packages/cli/src/webhooks/webhook-helpers.ts b/packages/cli/src/webhooks/webhook-helpers.ts index 2a0b0ec93fe..28f554c9b5a 100644 --- a/packages/cli/src/webhooks/webhook-helpers.ts +++ b/packages/cli/src/webhooks/webhook-helpers.ts @@ -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; diff --git a/packages/cli/src/webhooks/webhook-request-handler.ts b/packages/cli/src/webhooks/webhook-request-handler.ts index c477877b411..a4f8d1e01bc 100644 --- a/packages/cli/src/webhooks/webhook-request-handler.ts +++ b/packages/cli/src/webhooks/webhook-request-handler.ts @@ -84,7 +84,7 @@ class WebhookRequestHandler { } else { logger.error( `Error in handling webhook request ${req.method} ${req.path}: ${error.message}`, - { stacktrace: error.stack }, + { error }, ); }