backend: enhance test webhook url error handling

This commit is contained in:
juancarmore 2025-09-05 12:02:40 +02:00
parent 2bea178e76
commit 66d0014c7e
3 changed files with 98 additions and 9 deletions

View File

@ -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');

View File

@ -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

View File

@ -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<void> {
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<WebhookPreferences> {
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