diff --git a/mac/VibeTunnel/Core/Services/EventSource.swift b/mac/VibeTunnel/Core/Services/EventSource.swift index ee76a55c..52152167 100644 --- a/mac/VibeTunnel/Core/Services/EventSource.swift +++ b/mac/VibeTunnel/Core/Services/EventSource.swift @@ -1,6 +1,12 @@ import Foundation import os.log +extension Data { + var hexString: String { + map { String(format: "%02hhx", $0) }.joined() + } +} + /// Event received from an EventSource (Server-Sent Events) stream struct Event { let id: String? @@ -46,13 +52,18 @@ final class EventSource: NSObject { let configuration = URLSessionConfiguration.default configuration.timeoutIntervalForRequest = 0 // No timeout for SSE configuration.timeoutIntervalForResource = 0 + // Disable automatic decompression for SSE streaming + configuration.httpAdditionalHeaders = ["Accept-Encoding": "identity"] self.urlSession = URLSession(configuration: configuration, delegate: self, delegateQueue: nil) } // MARK: - Connection Management func connect() { - guard !isConnected else { return } + guard !isConnected else { + logger.warning("Already connected, ignoring connect request") + return + } var request = URLRequest(url: url) request.setValue("text/event-stream", forHTTPHeaderField: "Accept") @@ -68,10 +79,13 @@ final class EventSource: NSObject { request.setValue(lastEventId, forHTTPHeaderField: "Last-Event-ID") } - logger.debug("Connecting to EventSource: \(self.url)") + logger.info("🔌 Connecting to EventSource: \(self.url)") + logger.debug("Headers: \(request.allHTTPHeaderFields ?? [:])") dataTask = urlSession?.dataTask(with: request) dataTask?.resume() + + logger.info("📡 EventSource dataTask started") } func disconnect() { @@ -85,6 +99,7 @@ final class EventSource: NSObject { // MARK: - Event Parsing private func processBuffer() { + logger.debug("🔄 Processing buffer with \(self.buffer.count) characters") let lines = buffer.split(separator: "\n", omittingEmptySubsequences: false).map(String.init) var eventData: [String] = [] var eventType: String? @@ -181,13 +196,19 @@ extension EventSource: URLSessionDataDelegate { didReceive response: URLResponse, completionHandler: @escaping (URLSession.ResponseDisposition) -> Void ) { + logger.info("📥 URLSession didReceive response") + guard let httpResponse = response as? HTTPURLResponse else { + logger.error("Response is not HTTPURLResponse") completionHandler(.cancel) return } + logger.info("Response status: \(httpResponse.statusCode), headers: \(httpResponse.allHeaderFields)") + if httpResponse.statusCode == 200 { isConnected = true + logger.info("✅ EventSource connected successfully") DispatchQueue.main.async { self.onOpen?() } @@ -202,12 +223,23 @@ extension EventSource: URLSessionDataDelegate { } func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) { + logger.debug("📨 EventSource received \(data.count) bytes of data") + + // Check if data might be compressed + if data.count > 2 { + let header = [UInt8](data.prefix(2)) + if header[0] == 0x1f && header[1] == 0x8b { + logger.error("❌ Received gzip compressed data! SSE should not be compressed.") + return + } + } + guard let text = String(data: data, encoding: .utf8) else { - logger.error("Failed to decode data as UTF-8") + logger.error("Failed to decode data as UTF-8. First 20 bytes: \(data.prefix(20).hexString)") return } - logger.debug("📨 EventSource received data: \(text)") + logger.debug("📨 EventSource received text: \(text)") buffer += text processBuffer() } diff --git a/mac/VibeTunnel/VibeTunnelApp.swift b/mac/VibeTunnel/VibeTunnelApp.swift index 983c9b70..35e7466e 100644 --- a/mac/VibeTunnel/VibeTunnelApp.swift +++ b/mac/VibeTunnel/VibeTunnelApp.swift @@ -297,9 +297,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, @preconcurrency UNUser statusBarController?.updateStatusItemDisplay() // Session monitoring starts automatically - - // Start native notification service - await notificationService.start() + + // NotificationService is started by ServerManager when the server is ready } else { logger.error("HTTP server failed to start") if let error = serverManager.lastError { diff --git a/web/src/server/server.ts b/web/src/server/server.ts index 0c5d86b0..098936ae 100644 --- a/web/src/server/server.ts +++ b/web/src/server/server.ts @@ -397,12 +397,12 @@ export async function createApp(): Promise { logger.debug('Configured security headers with helmet'); // Add compression middleware with Brotli support - // Skip compression for SSE streams (asciicast) + // Skip compression for SSE streams (asciicast and events) app.use( compression({ filter: (req, res) => { - // Skip compression for Server-Sent Events (asciicast streams) - if (req.path.match(/\/api\/sessions\/[^/]+\/stream$/)) { + // Skip compression for Server-Sent Events + if (req.path.match(/\/api\/sessions\/[^/]+\/stream$/) || req.path === '/api/events') { return false; } // Use default filter for other requests @@ -412,7 +412,7 @@ export async function createApp(): Promise { level: 6, // Balanced compression level }) ); - logger.debug('Configured compression middleware (with asciicast exclusion)'); + logger.debug('Configured compression middleware (with SSE exclusion)'); // Add JSON body parser middleware with size limit app.use(express.json({ limit: '10mb' }));