* 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/gphttp
HTTP utilities package providing transport configuration, default HTTP client, and a wrapper around http.ServeMux with panic recovery.
Overview
This package provides shared HTTP utilities used throughout GoDoxy:
- Default HTTP Client: Pre-configured
http.Clientwith secure settings - Transport Factory: Functions to create optimized
http.Transportconfigurations - ServeMux Wrapper: Extended
http.ServeMuxwith panic recovery for handler registration
Architecture
graph TD
A[HTTP Request] --> B[gphttp.Client]
B --> C[Transport]
C --> D[Network Connection]
E[Server Setup] --> F[gphttp.ServeMux]
F --> G[http.ServeMux]
G --> H[HTTP Handlers]
Core Components
HTTP Client
The package exports a pre-configured http.Client with secure defaults:
var (
httpClient = &http.Client{
Timeout: 5 * time.Second,
Transport: &http.Transport{
DisableKeepAlives: true,
ForceAttemptHTTP2: false,
DialContext: (&net.Dialer{
Timeout: 3 * time.Second,
KeepAlive: 60 * time.Second,
}).DialContext,
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
},
}
Get = httpClient.Get
Post = httpClient.Post
Head = httpClient.Head
Do = httpClient.Do
)
Transport Factory
Functions for creating optimized HTTP transports:
// NewTransport creates an http.Transport with proxy support and optimized settings
func NewTransport() *http.Transport
// NewTransportWithTLSConfig creates an http.Transport with custom TLS configuration
func NewTransportWithTLSConfig(tlsConfig *tls.Config) *http.Transport
Default transport settings:
MaxIdleConnsPerHost: 1000IdleConnTimeout: 90 secondsTLSHandshakeTimeout: 10 secondsResponseHeaderTimeout: 60 secondsWriteBufferSize/ReadBufferSize: 16KB
ServeMux Wrapper
Extended http.ServeMux with panic recovery:
type ServeMux struct {
*http.ServeMux
}
func NewServeMux() ServeMux
func (mux ServeMux) Handle(pattern string, handler http.Handler) (err error)
func (mux ServeMux) HandleFunc(pattern string, handler http.HandlerFunc) (err error)
The Handle and HandleFunc methods recover from panics and return them as errors, preventing one bad handler from crashing the entire server.
Usage Examples
Creating Custom Transports
import (
"crypto/tls"
"net/http"
"github.com/yusing/godoxy/internal/net/gphttp"
)
// Default transport with environment proxy
transport := gphttp.NewTransport()
// Custom TLS configuration
tlsConfig := &tls.Config{
ServerName: "example.com",
}
transport := gphttp.NewTransportWithTLSConfig(tlsConfig)
Using ServeMux with Panic Recovery
mux := gphttp.NewServeMux()
// Register handlers - panics are converted to errors
if err := mux.HandleFunc("/api", apiHandler); err != nil {
log.Printf("handler registration failed: %v", err)
}
Integration Points
- Used by
internal/net/gphttp/middlewarefor HTTP request/response processing - Used by
internal/net/gphttp/loadbalancerfor backend connections - Used throughout the route handling system
Configuration
The default client disables HTTP/2 (ForceAttemptHTTP2: false) and keep-alives (DisableKeepAlives: true) for security and compatibility reasons. The transport uses environment proxy settings via http.ProxyFromEnvironment.