From 5d49693573aba44b40badcac5dc179158fcf93f8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 29 Jul 2025 15:30:35 +0200 Subject: [PATCH] Suppress socket disconnect error on normal Claude Code exit - Add isExitingNormally flag set during onExit callback - Check for common normal disconnect errors (EPIPE, ECONNRESET, Unknown error) - Log as debug instead of error for normal disconnects - Prevents misleading error message when exiting Claude Code normally --- .../push-notification-service.test.ts | 14 +++++----- .../services/push-notification-service.ts | 17 +++++------- web/src/server/fwd.ts | 26 ++++++++++++++++++- 3 files changed, 40 insertions(+), 17 deletions(-) diff --git a/web/src/client/services/push-notification-service.test.ts b/web/src/client/services/push-notification-service.test.ts index 0ceae0bb..0e7faaac 100644 --- a/web/src/client/services/push-notification-service.test.ts +++ b/web/src/client/services/push-notification-service.test.ts @@ -1,8 +1,8 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { DEFAULT_NOTIFICATION_PREFERENCES } from '../../types/config.js'; +import { notificationEventService } from './notification-event-service.js'; import type { NotificationPreferences } from './push-notification-service.js'; import { pushNotificationService } from './push-notification-service.js'; -import { notificationEventService } from './notification-event-service.js'; import { serverConfigService } from './server-config-service.js'; // Mock notification event service @@ -514,7 +514,9 @@ describe('PushNotificationService', () => { vibrationEnabled: true, }; - (serverConfigService.updateNotificationPreferences as vi.Mock).mockRejectedValue(new Error('Network error')); + (serverConfigService.updateNotificationPreferences as vi.Mock).mockRejectedValue( + new Error('Network error') + ); await expect(pushNotificationService.savePreferences(preferences)).rejects.toThrow( 'Network error' @@ -543,8 +545,8 @@ describe('PushNotificationService', () => { const testPromise = pushNotificationService.testNotification(); // Simulate receiving the event - await new Promise(resolve => setTimeout(resolve, 100)); // allow time for listener to be registered - + await new Promise((resolve) => setTimeout(resolve, 100)); // allow time for listener to be registered + expect(testNotificationHandler!).toBeDefined(); testNotificationHandler!({ title: 'VibeTunnel Test', @@ -584,8 +586,8 @@ describe('PushNotificationService', () => { const testPromise = pushNotificationService.testNotification(); // Simulate receiving the event - await new Promise(resolve => setTimeout(resolve, 100)); - + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(testNotificationHandler!).toBeDefined(); testNotificationHandler!({}); diff --git a/web/src/client/services/push-notification-service.ts b/web/src/client/services/push-notification-service.ts index adbbda96..cb9664c3 100644 --- a/web/src/client/services/push-notification-service.ts +++ b/web/src/client/services/push-notification-service.ts @@ -394,16 +394,13 @@ export class PushNotificationService { // Show notification if we have permission if (this.serviceWorkerRegistration && this.getPermission() === 'granted') { - await this.serviceWorkerRegistration.showNotification( - data.title || 'VibeTunnel Test', - { - body: data.body || 'Test notification received via SSE!', - icon: '/apple-touch-icon.png', - badge: '/favicon-32.png', - tag: 'vibetunnel-test-sse', - requireInteraction: false, - } - ); + await this.serviceWorkerRegistration.showNotification(data.title || 'VibeTunnel Test', { + body: data.body || 'Test notification received via SSE!', + icon: '/apple-touch-icon.png', + badge: '/favicon-32.png', + tag: 'vibetunnel-test-sse', + requireInteraction: false, + }); logger.log('āœ… Displayed SSE test notification'); } resolve(); diff --git a/web/src/server/fwd.ts b/web/src/server/fwd.ts index 3b28fa26..0f987861 100755 --- a/web/src/server/fwd.ts +++ b/web/src/server/fwd.ts @@ -390,6 +390,7 @@ export async function startVibeTunnelForward(args: string[]) { // Variables that need to be accessible in cleanup let sessionFileWatcher: fs.FSWatcher | undefined; let fileWatchDebounceTimer: NodeJS.Timeout | undefined; + let isExitingNormally = false; const sessionOptions: Parameters[1] = { sessionId: finalSessionId, @@ -405,6 +406,9 @@ export async function startVibeTunnelForward(args: string[]) { gitIsWorktree: gitInfo.gitIsWorktree, gitMainRepoPath: gitInfo.gitMainRepoPath, onExit: async (exitCode: number) => { + // Mark that we're exiting normally + isExitingNormally = true; + // Show exit message logger.log( chalk.yellow(`\nāœ“ VibeTunnel session ended`) + chalk.gray(` (exit code: ${exitCode})`) @@ -737,7 +741,27 @@ export async function startVibeTunnelForward(args: string[]) { // Handle socket events socketClient.on('disconnect', (error) => { - logger.error('Socket disconnected:', error?.message || 'Unknown error'); + // Don't log error if we're exiting normally + if (isExitingNormally) { + logger.debug('Socket disconnected during normal exit'); + return; + } + + // Check if this is a common disconnect error during normal operation + const errorMessage = error?.message || ''; + const isNormalDisconnect = + errorMessage.includes('EPIPE') || + errorMessage.includes('ECONNRESET') || + errorMessage.includes('socket hang up') || + errorMessage === 'Unknown error' || // Common during clean exits + !error; // No error object means clean disconnect + + if (isNormalDisconnect) { + logger.debug('Socket disconnected (normal termination)'); + } else { + logger.error('Socket disconnected:', error?.message || 'Unknown error'); + } + process.exit(1); });