From 66d0014c7ecb091efca081a71cfbc70c0df930ae Mon Sep 17 00:00:00 2001 From: juancarmore Date: Fri, 5 Sep 2025 12:02:40 +0200 Subject: [PATCH] backend: enhance test webhook url error handling --- .../webhook-preferences.controller.ts | 1 - backend/src/models/error.model.ts | 12 ++- .../src/services/openvidu-webhook.service.ts | 94 +++++++++++++++++-- 3 files changed, 98 insertions(+), 9 deletions(-) diff --git a/backend/src/controllers/global-preferences/webhook-preferences.controller.ts b/backend/src/controllers/global-preferences/webhook-preferences.controller.ts index caf7c1d..2886343 100644 --- a/backend/src/controllers/global-preferences/webhook-preferences.controller.ts +++ b/backend/src/controllers/global-preferences/webhook-preferences.controller.ts @@ -55,7 +55,6 @@ export const testWebhook = async (req: Request, res: Response) => { try { await webhookService.testWebhookUrl(url); logger.info(`Webhook URL '${url}' is valid`); - // If the URL is valid, we can return a success response return res.status(200).json({ message: 'Webhook URL is valid' }); } catch (error) { handleError(res, error, 'testing webhook URL'); diff --git a/backend/src/models/error.model.ts b/backend/src/models/error.model.ts index 7327e1e..3652c91 100644 --- a/backend/src/models/error.model.ts +++ b/backend/src/models/error.model.ts @@ -63,8 +63,16 @@ export const errorAzureNotAvailable = (error: any): OpenViduMeetError => { return new OpenViduMeetError('ABS Error', `Azure Blob Storage is not available ${error}`, 503); }; -export const errorWebhookUrlUnreachable = (url: string): OpenViduMeetError => { - return new OpenViduMeetError('Webhook Error', `Webhook URL '${url}' is unreachable`, 400); +export const errorInvalidWebhookUrl = (url: string, reason: string): OpenViduMeetError => { + return new OpenViduMeetError('Webhook Error', `Webhook URL '${url}' is invalid: ${reason}`, 400); +}; + +export const errorApiKeyNotConfiguredForWebhooks = (): OpenViduMeetError => { + return new OpenViduMeetError( + 'Webhook Error', + 'There are no API keys configured yet. Please, create one to use webhooks.', + 400 + ); }; // Auth errors diff --git a/backend/src/services/openvidu-webhook.service.ts b/backend/src/services/openvidu-webhook.service.ts index 3c54d98..8cae00d 100644 --- a/backend/src/services/openvidu-webhook.service.ts +++ b/backend/src/services/openvidu-webhook.service.ts @@ -9,7 +9,11 @@ import { } from '@typings-ce'; import crypto from 'crypto'; import { inject, injectable } from 'inversify'; -import { errorWebhookUrlUnreachable } from '../models/error.model.js'; +import { + errorApiKeyNotConfiguredForWebhooks, + errorInvalidWebhookUrl, + OpenViduMeetError +} from '../models/error.model.js'; import { AuthService, LoggerService, MeetStorageService } from './index.js'; @injectable() @@ -112,16 +116,20 @@ export class OpenViduWebhookService { }; try { - await this.sendRequest(url, { + const signature = await this.generateWebhookSignature(creationDate, data); + + await this.sendTestRequest(url, { method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + 'X-Timestamp': creationDate.toString(), + 'X-Signature': signature }, body: JSON.stringify(data) }); } catch (error) { - this.logger.error(`Error testing webhook URL ${url}: ${error}`); - throw errorWebhookUrlUnreachable(url); + this.logger.error(`Error sending test webhook to URL '${url}': ${error}`); + throw error; } } @@ -207,6 +215,80 @@ export class OpenViduWebhookService { } } + /** + * Sends a test request to a webhook URL with specific error handling for testing purposes. + * + * @param url - The webhook URL to test + * @param options - Request options + */ + protected async sendTestRequest(url: string, options: RequestInit): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 10000); // 10 second timeout + + try { + const response = await fetch(url, { + ...options, + signal: controller.signal + }); + + clearTimeout(timeoutId); + + if (!response.ok) { + const reason = + response.status >= 500 + ? `Server error (${response.status} ${response.statusText})` + : response.status >= 400 + ? `Client error (${response.status} ${response.statusText})` + : `Unexpected response (${response.status})`; + + throw errorInvalidWebhookUrl(url, reason); + } + + // Success case + this.logger.info(`Webhook test successful for URL: ${url}`); + } catch (error: any) { + clearTimeout(timeoutId); + + // If it's already our webhook error, re-throw it + if (error instanceof OpenViduMeetError && error.name === 'Webhook Error') { + throw error; + } + + // Handle specific error types + let reason: string; + + if (error.name === 'AbortError') { + reason = 'Request timed out after 10 seconds'; + } else if (error.name === 'TypeError' && error.message.includes('fetch')) { + // Network errors + const errorCode = error.cause?.code; + + switch (errorCode) { + case 'ENOTFOUND': + reason = 'Domain name could not be resolved'; + break; + case 'ECONNREFUSED': + reason = 'Connection refused by server'; + break; + case 'ECONNRESET': + reason = 'Connection reset by server'; + break; + case 'CERT_HAS_EXPIRED': + case 'UNABLE_TO_VERIFY_LEAF_SIGNATURE': + case 'SELF_SIGNED_CERT_IN_CHAIN': + reason = 'SSL/TLS certificate error'; + break; + default: + reason = `Network error: ${error.message}`; + } + } else { + reason = `Connection failed: ${error.message}`; + } + + throw errorInvalidWebhookUrl(url, reason); + } + } + protected async getWebhookPreferences(): Promise { try { const { webhooksPreferences } = await this.globalPrefService.getGlobalPreferences(); @@ -228,7 +310,7 @@ export class OpenViduWebhookService { } if (apiKeys.length === 0) { - throw new Error('There are no API keys configured yet. Please, create one to use webhooks.'); + throw errorApiKeyNotConfiguredForWebhooks(); } // Return the first API key