Fix SSE real-time streaming in Home Assistant Ingress mode
Add padding to overcome aiohttp 64KB buffer in HA Supervisor. Problem: - HA Supervisor uses aiohttp with 64KB StreamResponse buffer - Small SSE events (~200-500 bytes) were buffered until connection closed - Users saw all streams appear at once instead of real-time updates Solution: - Detect Ingress mode via X-Ingress-Path header - Add 64KB SSE comment padding to fill proxy buffers - Increase progress interval to 3 sec in Ingress mode (reduce traffic) - Normal mode (Docker/direct) unchanged - works exactly as before Traffic impact: - Normal mode: ~17KB per scan (unchanged) - Ingress mode: ~2-3MB per scan (acceptable for real-time updates) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -411,7 +411,14 @@ func (s *Scanner) testStreamsConcurrently(ctx context.Context, streams []models.
|
||||
defer cancelProgress()
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
// Use longer interval for Ingress mode to reduce traffic (padding is ~64KB per event)
|
||||
// Normal mode: 1 second, Ingress mode: 3 seconds
|
||||
progressInterval := 1 * time.Second
|
||||
if streamWriter.IsIngress() {
|
||||
progressInterval = 3 * time.Second
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(progressInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
@@ -419,7 +426,7 @@ func (s *Scanner) testStreamsConcurrently(ctx context.Context, streams []models.
|
||||
case <-progressCtx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
// Send progress every second to prevent WriteTimeout
|
||||
// Send progress to prevent WriteTimeout and show scanning activity
|
||||
_ = streamWriter.SendJSON("progress", models.ProgressMessage{
|
||||
Tested: int(atomic.LoadInt32(&tested)),
|
||||
Found: int(atomic.LoadInt32(&found)),
|
||||
|
||||
+67
-5
@@ -5,9 +5,20 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// IngressPaddingSize is the padding size for Home Assistant Ingress mode.
|
||||
// HA Supervisor uses aiohttp with 64KB buffer for StreamResponse.
|
||||
// We need to fill this buffer to force immediate delivery of SSE events.
|
||||
IngressPaddingSize = 64 * 1024 // 64KB
|
||||
|
||||
// IngressHeader is the header that Home Assistant Ingress adds to requests
|
||||
IngressHeader = "X-Ingress-Path"
|
||||
)
|
||||
|
||||
// Event represents a Server-Sent Event
|
||||
type Event struct {
|
||||
ID string
|
||||
@@ -253,8 +264,9 @@ func generateClientID() string {
|
||||
|
||||
// StreamWriter provides a simple interface for writing SSE events
|
||||
type StreamWriter struct {
|
||||
client *Client
|
||||
server *Server
|
||||
client *Client
|
||||
server *Server
|
||||
isIngress bool // True when running through Home Assistant Ingress proxy
|
||||
}
|
||||
|
||||
// NewStreamWriter creates a new stream writer for a client
|
||||
@@ -275,6 +287,9 @@ func (s *Server) NewStreamWriter(w http.ResponseWriter, r *http.Request) (*Strea
|
||||
// Send initial flush to establish connection
|
||||
flusher.Flush()
|
||||
|
||||
// Detect Home Assistant Ingress mode by checking for X-Ingress-Path header
|
||||
isIngress := r.Header.Get(IngressHeader) != ""
|
||||
|
||||
// Create client
|
||||
ctx, cancel := context.WithCancel(r.Context())
|
||||
client := &Client{
|
||||
@@ -287,8 +302,9 @@ func (s *Server) NewStreamWriter(w http.ResponseWriter, r *http.Request) (*Strea
|
||||
}
|
||||
|
||||
return &StreamWriter{
|
||||
client: client,
|
||||
server: s,
|
||||
client: client,
|
||||
server: s,
|
||||
isIngress: isIngress,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -304,7 +320,48 @@ func (sw *StreamWriter) SendEvent(eventType string, data interface{}) error {
|
||||
return fmt.Errorf("response does not support flushing")
|
||||
}
|
||||
|
||||
return sw.server.writeEvent(sw.client.Response, flusher, event)
|
||||
// Use Ingress-aware write method
|
||||
return sw.writeEventWithIngress(sw.client.Response, flusher, event)
|
||||
}
|
||||
|
||||
// writeEventWithIngress writes an event and adds padding for Ingress mode
|
||||
func (sw *StreamWriter) writeEventWithIngress(w http.ResponseWriter, flusher http.Flusher, event Event) error {
|
||||
// Write the event using standard method
|
||||
if err := sw.server.writeEvent(w, flusher, event); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// In Ingress mode, add padding to fill the 64KB buffer and force immediate delivery
|
||||
if sw.isIngress {
|
||||
if err := sw.writePadding(w, flusher); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// writePadding writes SSE comment padding to fill proxy buffers.
|
||||
// SSE comments (lines starting with ':') are ignored by clients.
|
||||
func (sw *StreamWriter) writePadding(w http.ResponseWriter, flusher http.Flusher) error {
|
||||
// Create padding using SSE comments which are ignored by clients
|
||||
// Each line is ": " + padding content + "\n"
|
||||
// We need ~64KB to fill the aiohttp StreamResponse buffer
|
||||
const lineSize = 1024 // 1KB per line
|
||||
const numLines = 64 // 64 lines = 64KB
|
||||
|
||||
paddingLine := ": " + strings.Repeat(".", lineSize-4) + "\n" // -4 for ": " and "\n"
|
||||
|
||||
for i := 0; i < numLines; i++ {
|
||||
if _, err := fmt.Fprint(w, paddingLine); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Flush the padding
|
||||
flusher.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendJSON sends JSON data as an event
|
||||
@@ -312,6 +369,11 @@ func (sw *StreamWriter) SendJSON(eventType string, v interface{}) error {
|
||||
return sw.SendEvent(eventType, v)
|
||||
}
|
||||
|
||||
// IsIngress returns true if running through Home Assistant Ingress proxy
|
||||
func (sw *StreamWriter) IsIngress() bool {
|
||||
return sw.isIngress
|
||||
}
|
||||
|
||||
// SendMessage sends a simple message
|
||||
func (sw *StreamWriter) SendMessage(message string) error {
|
||||
return sw.SendEvent("message", map[string]string{"message": message})
|
||||
|
||||
Reference in New Issue
Block a user