diff --git a/pkg/backend/transparent.go b/pkg/backend/transparent.go index 7bc72d8..8dea6b6 100644 --- a/pkg/backend/transparent.go +++ b/pkg/backend/transparent.go @@ -125,27 +125,7 @@ func (p *TransparentBackend) Run(ctx context.Context) (http.Handler, error) { base: http.DefaultTransport, targetHost: p.url.Host, }, - // FlushInterval -1 enables immediate flushing for SSE streams. - // Without this the proxy buffers the response, breaking - // Server-Sent Events used by MCP Streamable HTTP. FlushInterval: -1, - // ErrorHandler prevents the default ReverseProxy behavior of - // panicking when the backend closes the connection (e.g. when - // an SSE stream ends or the client disconnects). Instead we - // log the error and return a 502 to the client. - ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) { - p.logger.Warn("reverse proxy error", - zap.String("method", r.Method), - zap.String("path", r.URL.Path), - zap.Error(err), - ) - // Only write an error response if headers haven't been sent yet - // (i.e. the stream hasn't started). For in-flight SSE streams - // the ResponseWriter is already flushed, so just return. - if !isHeadersSent(w) { - http.Error(w, "Bad Gateway", http.StatusBadGateway) - } - }, Rewrite: func(pr *httputil.ProxyRequest) { pr.SetURL(p.url) if p.isTrusted(pr.In.RemoteAddr) { @@ -168,17 +148,6 @@ func (p *TransparentBackend) Run(ctx context.Context) (http.Handler, error) { return &rp, nil } -// isHeadersSent checks whether the ResponseWriter has already started sending -// the response (status + headers). Once flushed we can no longer write an -// error status, so callers should just close the connection instead. -func isHeadersSent(w http.ResponseWriter) bool { - // A non-zero status in the header map means WriteHeader was called. - // The standard http.response tracks this internally; the only reliable - // external signal is whether Content-Type (or any header set by the - // backend) is already present in the response. - return w.Header().Get("Content-Type") != "" -} - func (p *TransparentBackend) isTrusted(hostport string) bool { if host, _, err := net.SplitHostPort(hostport); err == nil { hostport = host diff --git a/pkg/mcp-proxy/main.go b/pkg/mcp-proxy/main.go index 3fa4ff8..cc7b8b1 100644 --- a/pkg/mcp-proxy/main.go +++ b/pkg/mcp-proxy/main.go @@ -305,7 +305,13 @@ func Run( }) router.Use(ginzap.Ginzap(logger, time.RFC3339, true)) - router.Use(ginzap.RecoveryWithZap(logger, true)) + router.Use(ginzap.CustomRecoveryWithZap(logger, true, func(c *gin.Context, err any) { + if err == http.ErrAbortHandler { + c.Abort() + return + } + c.AbortWithStatus(http.StatusInternalServerError) + })) store := cookie.NewStore(secret) store.Options(sessions.Options{ Path: "/", diff --git a/pkg/proxy/proxy.go b/pkg/proxy/proxy.go index cbb203a..3938365 100644 --- a/pkg/proxy/proxy.go +++ b/pkg/proxy/proxy.go @@ -87,22 +87,6 @@ func (p *ProxyRouter) handleProxy(c *gin.Context) { } } - // Recover from http.ErrAbortHandler panics that occur when SSE - // streams end (backend closes connection, client disconnects). - // The default ReverseProxy panics with ErrAbortHandler on flush - // errors — this is expected for SSE and should not crash the proxy. - defer func() { - if r := recover(); r != nil { - // http.ErrAbortHandler is the sentinel panic used by - // net/http to silently abort a handler. Let it go. - if r == http.ErrAbortHandler { - return - } - // Re-panic for anything unexpected - panic(r) - } - }() - p.proxy.ServeHTTP(c.Writer, c.Request) }