mirror of
https://github.com/yusing/godoxy.git
synced 2026-03-18 15:34:38 +01:00
* fix(middleware): restore SSE streaming for POST endpoints Regression introduced in16935865(v0.27.0). Before that commit, LazyResponseModifier only buffered HTML responses and let everything else pass through via the IsBuffered() early return. The refactor replaced it with NewResponseModifier which unconditionally buffers all writes until FlushRelease() fires after the handler returns. That kills real-time streaming for any SSE endpoint that uses POST. The existing bypass at ServeHTTP line 193 only fires when the *request* carries Accept: text/event-stream. That works for browser EventSource (which always sets that header) but not for programmatic fetch() calls, which set Content-Type: application/json on the request and only emit Content-Type: text/event-stream on the *response*. Fix: introduce ssePassthroughWriter, a thin http.ResponseWriter wrapper that sits in front of the ResponseModifier. It watches for Content-Type: text/event-stream in the response headers at the moment WriteHeader or the first Write is called. Once detected it copies the buffered headers to the real writer and switches all subsequent writes to pass directly through with an immediate Flush(), bypassing the ResponseModifier buffer entirely. Also tighten the Accept header check from == to strings.Contains so that Accept: text/event-stream, */* is handled correctly. Reported against Dockhand (https://github.com/Finsys/dockhand) where container update progress, image pull logs and vulnerability scan output all stopped streaming after users upgraded to GoDoxy v0.27.0. GET SSE endpoints (container logs) continued to work because browsers send Accept: text/event-stream for EventSource connections. * fix(middleware): make Content-Type SSE check case-insensitive * refactor(middleware): extract Content-Type into a named constant * fix(middleware): enhance safe guard to avoid buffering SSE, WS and large bodies Reverts some changes in16935865and apply more rubust handling. Use a lazy response modifier that buffers only when the response is safe to mutate. This prevents middleware from intercepting websocket/SSE streams, encoded payloads, and non-text or oversized responses. Set a 4MB max buffered size and gate buffering via response headers (content type, transfer/content encoding, and content length). Skip mutation when a response is not buffered or mutation setup fails, and simplify chained response modifiers to operate on the same response. Also update the goutils submodule for max body limit support. --------- Co-authored-by: yusing <yusing.wys@gmail.com>
internal/net
The net package provides network utility functions for GoDoxy, including TCP connection testing and network-related helpers.
Overview
The net package implements network utility functions that are used throughout GoDoxy for connectivity testing, TCP operations, and network-related utilities.
Key Features
- TCP connection testing (ping)
- Connection utilities
Core Functions
TCP Ping
// PingTCP pings a TCP endpoint by attempting a connection.
func PingTCP(ctx context.Context, ip net.IP, port int) error
Usage
Basic Usage
import "github.com/yusing/godoxy/internal/net"
func checkService(ctx context.Context, ip string, port int) error {
addr := net.ParseIP(ip)
if addr == nil {
return fmt.Errorf("invalid IP: %s", ip)
}
err := net.PingTCP(ctx, addr, port)
if err != nil {
return fmt.Errorf("service %s:%d unreachable: %w", ip, port, err)
}
fmt.Printf("Service %s:%d is reachable\n", ip, port)
return nil
}
Timeout Usage
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
ip := net.ParseIP("192.168.1.100")
err := net.PingTCP(ctx, ip, 8080)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
log.Println("Connection timed out")
} else {
log.Printf("Connection failed: %v", err)
}
}
Implementation
func PingTCP(ctx context.Context, ip net.IP, port int) error {
var dialer net.Dialer
conn, err := dialer.DialContext(ctx, "tcp", fmt.Sprintf("%s:%d", ip, port))
if err != nil {
return err
}
conn.Close()
return nil
}
Data Flow
sequenceDiagram
participant Caller
participant Dialer
participant TCPEndpoint
participant Connection
Caller->>Dialer: DialContext("tcp", "ip:port")
Dialer->>TCPEndpoint: SYN
TCPEndpoint-->>Dialer: SYN-ACK
Dialer->>Connection: Create connection
Connection-->>Dialer: Connection
Dialer-->>Caller: nil error
Note over Caller,Connection: Connection immediately closed
Connection->>TCPEndpoint: FIN
TCPEndpoint-->>Connection: FIN-ACK
Use Cases
Service Health Check
func checkServices(ctx context.Context, services []Service) error {
for _, svc := range services {
ip := net.ParseIP(svc.IP)
if ip == nil {
return fmt.Errorf("invalid IP for %s: %s", svc.Name, svc.IP)
}
if err := net.PingTCP(ctx, ip, svc.Port); err != nil {
return fmt.Errorf("service %s (%s:%d) unreachable: %w",
svc.Name, svc.IP, svc.Port, err)
}
}
return nil
}
Proxmox Container Reachability
// Check if a Proxmox container is reachable on its proxy port
func checkContainerReachability(ctx context.Context, node *proxmox.Node, vmid int, port int) error {
ips, err := node.LXCGetIPs(ctx, vmid)
if err != nil {
return err
}
for _, ip := range ips {
if err := net.PingTCP(ctx, ip, port); err == nil {
return nil // Found reachable IP
}
}
return fmt.Errorf("no reachable IP found for container %d", vmid)
}
Related Packages
- Route: Uses TCP ping for load balancing health checks
- Proxmox: Uses TCP ping to verify container reachability
- Idlewatcher: Uses TCP ping to check idle status