Files
pulse/server/resilientApiClient.js
T
rcourtman 4452b27655 fix: resolve circular reference errors in alert system
- Fixed undefined guest.type errors in generateAlertMessage by adding proper null checks
- Separated notification status from alert objects to prevent circular references during serialization
- Added comprehensive error handling with fallback data when alert serialization fails
- Improved alert state management to track notification status separately
- Fixed alert display issues in UI by ensuring proper data structure
- Added debugging helpers for troubleshooting serialization issues
- Cleaned up console logging and removed development artifacts

These changes resolve the "Converting circular structure to JSON" errors that were
preventing alerts from being properly displayed and managed in the UI.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-09 16:15:56 +01:00

162 lines
4.6 KiB
JavaScript

const axios = require('axios');
const https = require('https');
const dnsResolver = require('./dnsResolver');
/**
* Creates a resilient API client that handles DNS failures gracefully
*/
class ResilientApiClient {
constructor(baseConfig, authInterceptor) {
this.baseConfig = baseConfig;
this.authInterceptor = authInterceptor;
this.hostname = this.extractHostname(baseConfig.baseURL);
this.clients = new Map(); // Map of IP -> axios instance
this.lastWorkingIp = null;
}
extractHostname(url) {
try {
const urlObj = new URL(url);
return urlObj.hostname;
} catch (error) {
// Handle cases where baseURL might not be a full URL
const match = url.match(/^https?:\/\/([^:\/]+)/);
return match ? match[1] : null;
}
}
/**
* Create an axios instance for a specific IP
*/
createClientForIp(ip) {
// Replace hostname with IP in the base URL
const baseURL = this.baseConfig.baseURL.replace(this.hostname, ip);
const client = axios.create({
...this.baseConfig,
baseURL: baseURL,
httpsAgent: new https.Agent({
rejectUnauthorized: this.baseConfig.httpsAgent?.options?.rejectUnauthorized ?? true,
// Set a shorter keepAlive timeout for failed connections
keepAlive: true,
keepAliveMsecs: 1000,
timeout: this.baseConfig.timeout || 30000
}),
headers: {
...this.baseConfig.headers,
// Add Host header to ensure proper SSL certificate validation
'Host': this.hostname
}
});
// Apply auth interceptor if provided
if (this.authInterceptor) {
client.interceptors.request.use(this.authInterceptor);
}
// Add response interceptor to track working IPs
client.interceptors.response.use(
(response) => {
// Mark this IP as working
this.lastWorkingIp = ip;
return response;
},
(error) => {
// Mark IP as failed on certain errors
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.code === 'EHOSTUNREACH') {
dnsResolver.markHostFailed(ip);
}
return Promise.reject(error);
}
);
return client;
}
/**
* Get or create a client for a specific IP
*/
getClientForIp(ip) {
if (!this.clients.has(ip)) {
this.clients.set(ip, this.createClientForIp(ip));
}
return this.clients.get(ip);
}
/**
* Try to make a request with automatic failover
*/
async request(config) {
// First, try to resolve the hostname
let ips;
try {
ips = await dnsResolver.resolveHostname(this.hostname);
} catch (error) {
console.error(`[ResilientApiClient] DNS resolution failed for ${this.hostname}: ${error.message}`);
throw new Error(`DNS resolution failed for ${this.hostname}: ${error.message}`);
}
if (ips.length === 0) {
throw new Error(`No IP addresses available for ${this.hostname}`);
}
// If we have a last working IP, try it first
if (this.lastWorkingIp && ips.includes(this.lastWorkingIp)) {
ips = [this.lastWorkingIp, ...ips.filter(ip => ip !== this.lastWorkingIp)];
}
let lastError;
// Try each IP in sequence
for (const ip of ips) {
if (dnsResolver.isHostFailed(ip)) {
continue;
}
try {
const client = this.getClientForIp(ip);
const response = await client.request(config);
return response;
} catch (error) {
lastError = error;
console.warn(`[ResilientApiClient] Request failed for ${this.hostname} via IP ${ip}: ${error.message}`);
// If this is a connection error, mark the IP as failed
if (error.code === 'ECONNREFUSED' || error.code === 'ETIMEDOUT' || error.code === 'EHOSTUNREACH') {
dnsResolver.markHostFailed(ip);
}
// Continue to next IP
}
}
// All IPs failed
console.error(`[ResilientApiClient] All ${ips.length} IPs failed for ${this.hostname}`);
throw lastError || new Error(`All IPs failed for ${this.hostname}`);
}
// Proxy common axios methods
async get(url, config) {
return this.request({ ...config, method: 'GET', url });
}
async post(url, data, config) {
return this.request({ ...config, method: 'POST', url, data });
}
async put(url, data, config) {
return this.request({ ...config, method: 'PUT', url, data });
}
async delete(url, config) {
return this.request({ ...config, method: 'DELETE', url });
}
async patch(url, data, config) {
return this.request({ ...config, method: 'PATCH', url, data });
}
}
module.exports = ResilientApiClient;