Hey Reddit,
I’ve been working on an application that streams log updates to a web browser using Server-Sent Events (SSE) with aiohttp
in Python. Everything was working great, except for one issue: the loading spinner on the browser tab would never stop after the initial page load.
The Problem
When using SSE for real-time updates, the browser keeps the tab in the loading state because the connection is kept open. Even though the page is loaded and streaming updates, the browser never realizes that the connection is fully established, and the loading spinner continues to spin.
The Solution
After researching, I found that the solution lies in sending an immediate response after the connection is prepared. Specifically:
- I flush the headers immediately after the connection is opened.
- I send a small comment message (
: connectednn
) to let the browser know that the connection is live, which causes the browser to stop the loading spinner.
from aiohttp import web import asyncio import os LOG_FILE_PATH = "test.txt" # Function to read the last N lines from a file efficiently def tail_file(file_path, lines=10): try: with open(file_path, "rb") as f: f.seek(0, os.SEEK_END) position = f.tell() last_pos = position buffer = [] line_count = 0 while position > 0 and line_count < lines: position -= 1 f.seek(position) char = f.read(1) if char == b'n': line_count += 1 if line_count > lines: break buffer.append(char) buffer.reverse() return b"".join(buffer).decode(errors="replace").splitlines()[-lines:], last_pos except Exception as e: raise RuntimeError(f"Error reading the file: {e}") # Asynchronous generator to stream log updates async def log_stream(): last_position = 0 # Initial state: Send the last 10 lines try: logs, last_position = tail_file(LOG_FILE_PATH) yield "data: " + "n".join(logs).replace("n", "ndata: ") + "nn" except Exception as e: yield f"data: Error reading log file: {e}nn" return # Continuously monitor for updates while True: try: current_size = os.path.getsize(LOG_FILE_PATH) # Check if file is truncated or rotated if last_position > current_size: last_position = 0 # Reset if file is truncated # Check for updates only if the file size has changed if last_position < current_size: with open(LOG_FILE_PATH, "r") as f: f.seek(last_position) lines = f.readlines() if lines: yield "data: " + "".join(lines).replace("n", "ndata: ") + "nn" last_position = f.tell() # Update the position pointer else: # Send a heartbeat if no updates yield ": heartbeatnn" # ':' means comment in SSE # Polling interval (reduce frequency for better performance) await asyncio.sleep(2) except Exception as e: yield f"data: Error watching log file: {e}nn" return # HTTP handler for SSE endpoint async def sse_handler(request): response = web.StreamResponse( status=200, reason="OK", headers={ "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", }, ) print("SSE connection opened") await response.prepare(request) # Immediately prepare the response print("SSE connection ready") # Send an initial comment to let the browser know the connection is open await response.write(b": connectednn") # Send the last 10 lines and start streaming updates try: async for line in log_stream(): await response.write(line.encode("utf-8")) except asyncio.CancelledError: print("SSE connection closed") finally: print("SSE cleanup completed") return response # Create an aiohttp web application app = web.Application() app.router.add_get("/log", sse_handler) # Run the server if __name__ == "__main__": web.run_app(app, host="localhost", port=8080)
submitted by /u/BerryAffectionate840
[link] [comments]
r/learnpython Hey Reddit, I’ve been working on an application that streams log updates to a web browser using Server-Sent Events (SSE) with aiohttp in Python. Everything was working great, except for one issue: the loading spinner on the browser tab would never stop after the initial page load. The Problem When using SSE for real-time updates, the browser keeps the tab in the loading state because the connection is kept open. Even though the page is loaded and streaming updates, the browser never realizes that the connection is fully established, and the loading spinner continues to spin. The Solution After researching, I found that the solution lies in sending an immediate response after the connection is prepared. Specifically: I flush the headers immediately after the connection is opened. I send a small comment message (: connectednn) to let the browser know that the connection is live, which causes the browser to stop the loading spinner. from aiohttp import web import asyncio import os LOG_FILE_PATH = “test.txt” # Function to read the last N lines from a file efficiently def tail_file(file_path, lines=10): try: with open(file_path, “rb”) as f: f.seek(0, os.SEEK_END) position = f.tell() last_pos = position buffer = [] line_count = 0 while position > 0 and line_count < lines: position -= 1 f.seek(position) char = f.read(1) if char == b’n’: line_count += 1 if line_count > lines: break buffer.append(char) buffer.reverse() return b””.join(buffer).decode(errors=”replace”).splitlines()[-lines:], last_pos except Exception as e: raise RuntimeError(f”Error reading the file: {e}”) # Asynchronous generator to stream log updates async def log_stream(): last_position = 0 # Initial state: Send the last 10 lines try: logs, last_position = tail_file(LOG_FILE_PATH) yield “data: ” + “n”.join(logs).replace(“n”, “ndata: “) + “nn” except Exception as e: yield f”data: Error reading log file: {e}nn” return # Continuously monitor for updates while True: try: current_size = os.path.getsize(LOG_FILE_PATH) # Check if file is truncated or rotated if last_position > current_size: last_position = 0 # Reset if file is truncated # Check for updates only if the file size has changed if last_position < current_size: with open(LOG_FILE_PATH, “r”) as f: f.seek(last_position) lines = f.readlines() if lines: yield “data: ” + “”.join(lines).replace(“n”, “ndata: “) + “nn” last_position = f.tell() # Update the position pointer else: # Send a heartbeat if no updates yield “: heartbeatnn” # ‘:’ means comment in SSE # Polling interval (reduce frequency for better performance) await asyncio.sleep(2) except Exception as e: yield f”data: Error watching log file: {e}nn” return # HTTP handler for SSE endpoint async def sse_handler(request): response = web.StreamResponse( status=200, reason=”OK”, headers={ “Content-Type”: “text/event-stream”, “Cache-Control”: “no-cache”, “Connection”: “keep-alive”, }, ) print(“SSE connection opened”) await response.prepare(request) # Immediately prepare the response print(“SSE connection ready”) # Send an initial comment to let the browser know the connection is open await response.write(b”: connectednn”) # Send the last 10 lines and start streaming updates try: async for line in log_stream(): await response.write(line.encode(“utf-8”)) except asyncio.CancelledError: print(“SSE connection closed”) finally: print(“SSE cleanup completed”) return response # Create an aiohttp web application app = web.Application() app.router.add_get(“/log”, sse_handler) # Run the server if __name__ == “__main__”: web.run_app(app, host=”localhost”, port=8080) submitted by /u/BerryAffectionate840 [link] [comments]
Hey Reddit,
I’ve been working on an application that streams log updates to a web browser using Server-Sent Events (SSE) with aiohttp
in Python. Everything was working great, except for one issue: the loading spinner on the browser tab would never stop after the initial page load.
The Problem
When using SSE for real-time updates, the browser keeps the tab in the loading state because the connection is kept open. Even though the page is loaded and streaming updates, the browser never realizes that the connection is fully established, and the loading spinner continues to spin.
The Solution
After researching, I found that the solution lies in sending an immediate response after the connection is prepared. Specifically:
- I flush the headers immediately after the connection is opened.
- I send a small comment message (
: connectednn
) to let the browser know that the connection is live, which causes the browser to stop the loading spinner.
from aiohttp import web import asyncio import os LOG_FILE_PATH = "test.txt" # Function to read the last N lines from a file efficiently def tail_file(file_path, lines=10): try: with open(file_path, "rb") as f: f.seek(0, os.SEEK_END) position = f.tell() last_pos = position buffer = [] line_count = 0 while position > 0 and line_count < lines: position -= 1 f.seek(position) char = f.read(1) if char == b'n': line_count += 1 if line_count > lines: break buffer.append(char) buffer.reverse() return b"".join(buffer).decode(errors="replace").splitlines()[-lines:], last_pos except Exception as e: raise RuntimeError(f"Error reading the file: {e}") # Asynchronous generator to stream log updates async def log_stream(): last_position = 0 # Initial state: Send the last 10 lines try: logs, last_position = tail_file(LOG_FILE_PATH) yield "data: " + "n".join(logs).replace("n", "ndata: ") + "nn" except Exception as e: yield f"data: Error reading log file: {e}nn" return # Continuously monitor for updates while True: try: current_size = os.path.getsize(LOG_FILE_PATH) # Check if file is truncated or rotated if last_position > current_size: last_position = 0 # Reset if file is truncated # Check for updates only if the file size has changed if last_position < current_size: with open(LOG_FILE_PATH, "r") as f: f.seek(last_position) lines = f.readlines() if lines: yield "data: " + "".join(lines).replace("n", "ndata: ") + "nn" last_position = f.tell() # Update the position pointer else: # Send a heartbeat if no updates yield ": heartbeatnn" # ':' means comment in SSE # Polling interval (reduce frequency for better performance) await asyncio.sleep(2) except Exception as e: yield f"data: Error watching log file: {e}nn" return # HTTP handler for SSE endpoint async def sse_handler(request): response = web.StreamResponse( status=200, reason="OK", headers={ "Content-Type": "text/event-stream", "Cache-Control": "no-cache", "Connection": "keep-alive", }, ) print("SSE connection opened") await response.prepare(request) # Immediately prepare the response print("SSE connection ready") # Send an initial comment to let the browser know the connection is open await response.write(b": connectednn") # Send the last 10 lines and start streaming updates try: async for line in log_stream(): await response.write(line.encode("utf-8")) except asyncio.CancelledError: print("SSE connection closed") finally: print("SSE cleanup completed") return response # Create an aiohttp web application app = web.Application() app.router.add_get("/log", sse_handler) # Run the server if __name__ == "__main__": web.run_app(app, host="localhost", port=8080)
submitted by /u/BerryAffectionate840
[link] [comments]