webhook-handler.tsβ’4.03 kB
/**
* Webhook trigger handler
*
* Handles webhook-based workflow triggers:
* - Supports GET, POST, PUT, DELETE methods
* - Passes data as body (POST/PUT/DELETE) or query params (GET)
* - Includes SSRF protection
*/
import { z } from 'zod';
import { Workflow, WebhookRequest } from '../../types/n8n-api';
import {
TriggerType,
TriggerResponse,
TriggerHandlerCapabilities,
DetectedTrigger,
WebhookTriggerInput,
} from '../types';
import { BaseTriggerHandler } from './base-handler';
import { buildTriggerUrl } from '../trigger-detector';
/**
* Zod schema for webhook input validation
*/
const webhookInputSchema = z.object({
workflowId: z.string(),
triggerType: z.literal('webhook'),
httpMethod: z.enum(['GET', 'POST', 'PUT', 'DELETE']).optional(),
webhookPath: z.string().optional(),
data: z.record(z.unknown()).optional(),
headers: z.record(z.string()).optional(),
timeout: z.number().optional(),
waitForResponse: z.boolean().optional(),
});
/**
* Webhook trigger handler
*/
export class WebhookHandler extends BaseTriggerHandler<WebhookTriggerInput> {
readonly triggerType: TriggerType = 'webhook';
readonly capabilities: TriggerHandlerCapabilities = {
requiresActiveWorkflow: true,
supportedMethods: ['GET', 'POST', 'PUT', 'DELETE'],
canPassInputData: true,
};
readonly inputSchema = webhookInputSchema;
async execute(
input: WebhookTriggerInput,
workflow: Workflow,
triggerInfo?: DetectedTrigger
): Promise<TriggerResponse> {
const startTime = Date.now();
try {
// Build webhook URL
const baseUrl = this.getBaseUrl();
if (!baseUrl) {
return this.errorResponse(input, 'Cannot determine n8n base URL', startTime);
}
// Use provided webhook path or extract from trigger info
let webhookUrl: string;
if (input.webhookPath) {
// User provided explicit path
webhookUrl = `${baseUrl.replace(/\/+$/, '')}/webhook/${input.webhookPath}`;
} else if (triggerInfo?.webhookPath) {
// Use detected path from workflow
webhookUrl = buildTriggerUrl(baseUrl, triggerInfo, 'production');
} else {
return this.errorResponse(
input,
'No webhook path available. Provide webhookPath parameter or ensure workflow has a webhook trigger.',
startTime
);
}
// Determine HTTP method
const httpMethod = input.httpMethod || triggerInfo?.httpMethod || 'POST';
// SSRF protection - validate the webhook URL before making the request
const { SSRFProtection } = await import('../../utils/ssrf-protection');
const validation = await SSRFProtection.validateWebhookUrl(webhookUrl);
if (!validation.valid) {
return this.errorResponse(input, `SSRF protection: ${validation.reason}`, startTime);
}
// Build webhook request
const webhookRequest: WebhookRequest = {
webhookUrl,
httpMethod: httpMethod as 'GET' | 'POST' | 'PUT' | 'DELETE',
data: input.data,
headers: input.headers,
waitForResponse: input.waitForResponse ?? true,
};
// Trigger the webhook
const response = await this.client.triggerWebhook(webhookRequest);
return this.normalizeResponse(response, input, startTime, {
status: response.status,
statusText: response.statusText,
metadata: {
duration: Date.now() - startTime,
webhookPath: input.webhookPath || triggerInfo?.webhookPath,
httpMethod,
},
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
// Try to extract execution ID from error if available
const errorDetails = (error as any)?.details;
const executionId = errorDetails?.executionId || errorDetails?.id;
return this.errorResponse(input, errorMessage, startTime, {
executionId,
code: (error as any)?.code,
details: errorDetails,
});
}
}
}