/** * URL utility functions for provider management */ /** * Normalize a provider URL to ensure it has a valid protocol * Supports both http:// and https:// protocols * * @param url - URL to normalize (can be with or without protocol) * @param preferHttps - If true, prefer HTTPS when protocol is missing (default: false) * @returns Normalized URL with protocol, or null if invalid */ export function normalizeProviderUrl(url: string | null | undefined, preferHttps = false): string | null { if (!url || typeof url !== 'string') { return null; } const trimmed = url.trim(); if (!trimmed) { return null; } // If it already has a protocol, validate and return if (trimmed.startsWith('http://') || trimmed.startsWith('https://')) { try { const urlObj = new URL(trimmed); // Ensure it's http or https if (urlObj.protocol !== 'http:' && urlObj.protocol !== 'https:') { return null; } return urlObj.toString(); } catch { return null; } } // If it starts with //, add the preferred protocol if (trimmed.startsWith('//')) { const protocol = preferHttps ? 'https:' : 'http:'; try { return new URL(`${protocol}${trimmed}`).toString(); } catch { return null; } } // If no protocol, add the preferred one const protocol = preferHttps ? 'https://' : 'http://'; try { return new URL(`${protocol}${trimmed}`).toString(); } catch { return null; } } /** * Get both HTTP and HTTPS variants of a URL * Useful for trying both protocols when one fails * * @param url - Base URL (with or without protocol) * @returns Object with httpUrl and httpsUrl, or null if URL is invalid */ export function getProtocolVariants(url: string | null | undefined): { httpUrl: string; httpsUrl: string } | null { if (!url || typeof url !== 'string') { return null; } const trimmed = url.trim(); if (!trimmed) { return null; } let baseUrl: string; // Extract base URL without protocol if (trimmed.startsWith('http://')) { baseUrl = trimmed.substring(7); } else if (trimmed.startsWith('https://')) { baseUrl = trimmed.substring(8); } else if (trimmed.startsWith('//')) { baseUrl = trimmed.substring(2); } else { baseUrl = trimmed; } // Remove trailing slash for consistency baseUrl = baseUrl.replace(/\/$/, ''); try { // Validate that it's a valid URL structure new URL(`http://${baseUrl}`); return { httpUrl: `http://${baseUrl}`, httpsUrl: `https://${baseUrl}`, }; } catch { return null; } } /** * Try to determine the working protocol for a URL * Attempts both HTTP and HTTPS and returns the one that works * * @param baseUrl - Base URL without protocol or with one protocol * @param testPath - Optional path to test (default: '/') * @param timeout - Timeout in milliseconds (default: 5000) * @returns The working protocol ('http' or 'https') or null if neither works */ export async function detectWorkingProtocol( baseUrl: string, testPath = '/', timeout = 5000 ): Promise<'http' | 'https' | null> { const variants = getProtocolVariants(baseUrl); if (!variants) { return null; } const testUrl = (protocol: 'http' | 'https'): Promise => { return new Promise((resolve) => { const url = protocol === 'http' ? variants.httpUrl : variants.httpsUrl; const testUrl = `${url}${testPath}`; const controller = new AbortController(); const timeoutId = setTimeout(() => { controller.abort(); resolve(false); }, timeout); fetch(testUrl, { method: 'HEAD', signal: controller.signal, }) .then((response) => { clearTimeout(timeoutId); resolve(response.ok || response.status < 500); }) .catch(() => { clearTimeout(timeoutId); resolve(false); }); }); }; // Try HTTPS first (more common for modern servers) const httpsWorks = await testUrl('https'); if (httpsWorks) { return 'https'; } // Fall back to HTTP const httpWorks = await testUrl('http'); if (httpWorks) { return 'http'; } return null; }