Skip to main content
Glama
webhook-sender.ts8.23 kB
/** * Webhook Sender * HTTP client for delivering webhook events * Part of Jaxon Digital Optimizely DXP MCP Server - DXP-136 Phase 2 */ import https from 'https'; import http from 'http'; import { URL } from 'url'; import crypto from 'crypto'; import WebhookTransformer from './webhook-transformer'; /** * Send options */ export interface SendOptions { headers?: Record<string, string>; timeout?: number; webhookId?: string; } /** * Send result */ export interface SendResult { success: boolean; statusCode?: number; error?: string; responseTime: number; retryable?: boolean; body?: string; errorCode?: string; } /** * Test options */ export interface TestOptions extends SendOptions { // Inherits all SendOptions } /** * Webhook Sender Class * Sends HTTP POST requests to webhook URLs */ class WebhookSender { /** * Send a webhook * @param url - Webhook URL * @param payload - Event payload * @param options - Send options * @returns { success: boolean, statusCode?: number, error?: string, responseTime: number } */ static async send(url: string, payload: any, options: SendOptions = {}): Promise<SendResult> { const startTime = Date.now(); const { headers = {}, timeout = 10000, // 10 second timeout webhookId = crypto.randomUUID() } = options; return new Promise((resolve) => { try { const parsedUrl = new URL(url); const isHttps = parsedUrl.protocol === 'https:'; const httpModule = isHttps ? https : http; // Transform payload to flat format (industry-standard) const transformedPayload = WebhookTransformer.transform(payload); // Prepare payload const payloadStr = JSON.stringify(transformedPayload); // Prepare headers const requestHeaders: Record<string, string | number> = { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(payloadStr), 'User-Agent': 'Jaxon-DXP-MCP-Webhook/1.0', 'X-Webhook-Event': transformedPayload.eventType || 'unknown', 'X-Webhook-ID': webhookId, 'X-Webhook-Timestamp': new Date().toISOString(), ...headers // Custom headers last (can override defaults except reserved ones) }; // Prepare request options const requestOptions: http.RequestOptions = { hostname: parsedUrl.hostname, port: parsedUrl.port || (isHttps ? 443 : 80), path: parsedUrl.pathname + parsedUrl.search, method: 'POST', headers: requestHeaders, timeout: timeout }; // Create request const req = httpModule.request(requestOptions, (res) => { let responseBody = ''; res.on('data', (chunk) => { responseBody += chunk.toString(); }); res.on('end', () => { const responseTime = Date.now() - startTime; const statusCode = res.statusCode; // Consider 2xx status codes as success if (statusCode && statusCode >= 200 && statusCode < 300) { resolve({ success: true, statusCode: statusCode, responseTime: responseTime, body: responseBody }); } else { // Non-2xx status codes are failures resolve({ success: false, statusCode: statusCode, error: `HTTP ${statusCode}: ${res.statusMessage}`, responseTime: responseTime, retryable: this.isRetryableStatusCode(statusCode || 0), body: responseBody }); } }); }); // Handle request errors req.on('error', (error: NodeJS.ErrnoException) => { const responseTime = Date.now() - startTime; resolve({ success: false, error: error.message, errorCode: error.code, responseTime: responseTime, retryable: this.isRetryableError(error) }); }); // Handle timeout req.on('timeout', () => { req.destroy(); const responseTime = Date.now() - startTime; resolve({ success: false, error: `Request timeout after ${timeout}ms`, responseTime: responseTime, retryable: true // Timeouts are retryable }); }); // Send the request req.write(payloadStr); req.end(); } catch (error: any) { const responseTime = Date.now() - startTime; resolve({ success: false, error: error.message, responseTime: responseTime, retryable: false // Unexpected errors are not retryable }); } }); } /** * Determine if an HTTP status code is retryable * @param statusCode - HTTP status code * @returns True if retryable */ static isRetryableStatusCode(statusCode: number): boolean { // Retry on server errors (5xx) and specific client errors if (statusCode >= 500) return true; // 5xx - Server errors if (statusCode === 408) return true; // Request Timeout if (statusCode === 429) return true; // Too Many Requests if (statusCode === 425) return true; // Too Early if (statusCode === 502) return true; // Bad Gateway if (statusCode === 503) return true; // Service Unavailable if (statusCode === 504) return true; // Gateway Timeout return false; // 4xx client errors are generally not retryable } /** * Determine if a network error is retryable * @param error - Error object * @returns True if retryable */ static isRetryableError(error: NodeJS.ErrnoException): boolean { // Network errors that are worth retrying const retryableErrors = [ 'ECONNREFUSED', // Connection refused 'ECONNRESET', // Connection reset 'ETIMEDOUT', // Connection timeout 'ENOTFOUND', // DNS lookup failed (temporary) 'EAI_AGAIN', // DNS lookup failed (temporary) 'EHOSTUNREACH', // Host unreachable 'ENETUNREACH', // Network unreachable 'EPIPE' // Broken pipe ]; return error.code ? retryableErrors.includes(error.code) : false; } /** * Test a webhook URL (send test event) * @param url - Webhook URL * @param options - Test options * @returns Test result */ static async test(url: string, options: TestOptions = {}): Promise<SendResult> { const testPayload = { eventType: 'test.ping', timestamp: new Date().toISOString(), operationId: 'test', data: { message: 'This is a test webhook from Jaxon DXP MCP Server' }, metadata: { operation: 'webhook_test', user: 'system' } }; return await this.send(url, testPayload, options); } } export default WebhookSender;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/JaxonDigital/optimizely-dxp-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server