diff --git a/src/lib/antigravity/QuotaService.ts b/src/lib/antigravity/QuotaService.ts index 467fd41..e40dbfd 100644 --- a/src/lib/antigravity/QuotaService.ts +++ b/src/lib/antigravity/QuotaService.ts @@ -126,7 +126,12 @@ export class QuotaService { } catch (error: any) { if (axios.isAxiosError(error)) { const status = error.response?.status; - const text = JSON.stringify(error.response?.data || ''); + let text = ''; + try { + text = JSON.stringify(error.response?.data || ''); + } catch { + text = '[Unable to serialize response data]'; + } // ✅ Handle 403 Forbidden specifically - return immediately, do not retry if (status === 403) { diff --git a/src/main.ts b/src/main.ts index 34f9432..b3be982 100644 --- a/src/main.ts +++ b/src/main.ts @@ -20,9 +20,28 @@ import { ConfigManager } from './ipc/config/manager'; const packetLogPath = path.join(app.getPath('userData'), 'orpc_packets.log'); +/** + * Safely stringify an object, handling circular references + */ +function safeStringifyPacket(obj: unknown): string { + const seen = new WeakSet(); + return JSON.stringify(obj, (key, value) => { + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return '[Circular]'; + } + seen.add(value); + } + return value; + }); +} + function logPacket(data: any) { try { - fs.appendFileSync(packetLogPath, `[${new Date().toISOString()}] ${JSON.stringify(data)}\n`); + fs.appendFileSync( + packetLogPath, + `[${new Date().toISOString()}] ${safeStringifyPacket(data)}\n`, + ); } catch (e) { if (e instanceof Error) { console.error(e.message); diff --git a/src/server/modules/proxy/clients/gemini.client.ts b/src/server/modules/proxy/clients/gemini.client.ts index c5b6721..fd6af6b 100644 --- a/src/server/modules/proxy/clients/gemini.client.ts +++ b/src/server/modules/proxy/clients/gemini.client.ts @@ -27,7 +27,8 @@ export class GeminiClient { this.logger.error(`Gemini Stream API Error: ${error.message}`); throw new Error(error.response?.data?.error?.message || error.message); } - throw error; + // Re-throw as clean Error to avoid circular reference issues + throw error instanceof Error ? new Error(error.message) : new Error(String(error)); } } @@ -50,11 +51,12 @@ export class GeminiClient { } catch (error) { if (axios.isAxiosError(error)) { this.logger.error( - `Gemini API Error: ${error.message} - ${JSON.stringify(error.response?.data)}`, + `Gemini API Error: ${error.message} - ${this.safeStringify(error.response?.data)}`, ); throw new Error(error.response?.data?.error?.message || error.message); } - throw error; + // Re-throw as clean Error to avoid circular reference issues + throw error instanceof Error ? new Error(error.message) : new Error(String(error)); } } @@ -77,7 +79,6 @@ export class GeminiClient { return response.data; } catch (error) { this.handleAxiosError(error, 'StreamInternal'); - throw error; } } @@ -96,17 +97,38 @@ export class GeminiClient { return response.data.response || response.data; } catch (error) { this.handleAxiosError(error, 'GenerateInternal'); - throw error; } } private handleAxiosError(error: any, context: string) { if (axios.isAxiosError(error)) { this.logger.error( - `Gemini ${context} API Error: ${error.message} - ${JSON.stringify(error.response?.data)}`, + `Gemini ${context} API Error: ${error.message} - ${this.safeStringify(error.response?.data)}`, ); throw new Error(error.response?.data?.error?.message || error.message); } - throw error; + // Re-throw as clean Error to avoid circular reference issues + throw error instanceof Error ? new Error(error.message) : new Error(String(error)); + } + + /** + * Safely stringify an object, handling circular references + */ + private safeStringify(obj: unknown): string { + if (obj === undefined || obj === null) { + return String(obj); + } + try { + const seen = new WeakSet(); + return JSON.stringify(obj, (key, value) => { + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) return '[Circular]'; + seen.add(value); + } + return value; + }); + } catch { + return '[Unserializable]'; + } } } diff --git a/src/server/modules/proxy/proxy.service.ts b/src/server/modules/proxy/proxy.service.ts index 7313722..7d1ed4b 100644 --- a/src/server/modules/proxy/proxy.service.ts +++ b/src/server/modules/proxy/proxy.service.ts @@ -217,7 +217,11 @@ export class ProxyService { subscriber.complete(); }); - upstreamStream.on('error', (err: any) => subscriber.error(err)); + upstreamStream.on('error', (err: any) => { + // Convert to clean Error to avoid circular reference issues (socket objects) + const cleanError = err instanceof Error ? new Error(err.message) : new Error(String(err)); + subscriber.error(cleanError); + }); }); } @@ -372,7 +376,9 @@ export class ProxyService { }); upstreamStream.on('error', (err: any) => { - subscriber.error(err); + // Convert to clean Error to avoid circular reference issues (socket objects) + const cleanError = err instanceof Error ? new Error(err.message) : new Error(String(err)); + subscriber.error(cleanError); }); }); } diff --git a/src/utils/logger.ts b/src/utils/logger.ts index c99b3d4..1e90da9 100644 --- a/src/utils/logger.ts +++ b/src/utils/logger.ts @@ -4,6 +4,33 @@ import { getAgentDir } from './paths'; export type LogLevel = 'info' | 'warn' | 'error' | 'debug'; +/** + * Safely stringify an object, handling circular references + * This prevents "Converting circular structure to JSON" errors + * when logging objects like axios errors that contain socket references + */ +function safeStringify(obj: unknown): string { + const seen = new WeakSet(); + return JSON.stringify(obj, (key, value) => { + // Handle Error objects specially + if (value instanceof Error) { + return { + name: value.name, + message: value.message, + stack: value.stack, + }; + } + // Handle circular references + if (typeof value === 'object' && value !== null) { + if (seen.has(value)) { + return '[Circular]'; + } + seen.add(value); + } + return value; + }); +} + class Logger { private logFilePath: string; @@ -22,7 +49,7 @@ class Logger { private formatMessage(level: LogLevel, message: string, ...args: unknown[]): string { const timestamp = new Date().toISOString(); const formattedArgs = args - .map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg))) + .map((arg) => (typeof arg === 'object' ? safeStringify(arg) : String(arg))) .join(' '); return `[${timestamp}] [${level.toUpperCase()}] ${message} ${formattedArgs}`; }