Make request deadline extensions reach the socket behind the logging middleware
Long agent turns were cut at exactly 30s on the live instance with "The connection dropped partway through." — the #78 failure, which its tests said was fixed. The tests host openEventStream on a bare gin.New(); in production, slog-gin replaces c.Writer with a wrapper that embeds the gin.ResponseWriter interface, which has no Unwrap, so the ResponseController built from the handler's writer can't reach the connection and every SetWriteDeadline returns ErrNotSupported. The stream fell back to the server's absolute WriteTimeout; the first write past it failed, cancelled the request context, and closed the socket under the client mid-frame. The scan upload's read/write extensions failed the same way, with the errors discarded. captureController now runs first on the engine and stashes a controller built before anything wraps the writer; openEventStream and scanSeedPacket take it from responseController(c). The regression tests run the stream through New() — the real stack, in the real order — and check from the client side; the scan path logs once instead of swallowing the error. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
@@ -0,0 +1,48 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// responseControllerKey is where captureController stashes the controller in
|
||||
// the gin context for responseController to find.
|
||||
const responseControllerKey = "pansy.responseController"
|
||||
|
||||
// captureController hands every handler an http.ResponseController that can
|
||||
// actually reach the connection. It MUST be the first middleware on the engine.
|
||||
//
|
||||
// A ResponseController finds the connection's deadline setters by unwrapping
|
||||
// the ResponseWriter it was built from, one layer at a time, until it reaches
|
||||
// one that has them. gin's own writer unwraps cleanly. The logging middleware's
|
||||
// does not: it replaces c.Writer with a type that embeds the gin.ResponseWriter
|
||||
// INTERFACE, which has no Unwrap, so a controller built from c.Writer inside a
|
||||
// handler stops there and every SetReadDeadline/SetWriteDeadline returns
|
||||
// ErrNotSupported. That left the per-frame SSE deadline (#78) and the scan
|
||||
// upload's extensions dead in production while their tests — on a bare engine
|
||||
// with no logging — passed: long agent turns were cut at the server's absolute
|
||||
// 30s WriteTimeout, and the client saw "The connection dropped partway through."
|
||||
//
|
||||
// Building the controller here, ahead of every wrapper, sidesteps the question
|
||||
// of what any later middleware does to the writer. Handlers that extend a
|
||||
// deadline take it from responseController; sse_deadline_test.go runs the
|
||||
// scenario through New so a reorder or a new wrapper fails a test.
|
||||
func captureController() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Set(responseControllerKey, http.NewResponseController(c.Writer))
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// responseController returns the controller captureController stored, or — on
|
||||
// an engine without that middleware, which only tests build — one made from
|
||||
// c.Writer as it stands.
|
||||
func responseController(c *gin.Context) *http.ResponseController {
|
||||
if v, ok := c.Get(responseControllerKey); ok {
|
||||
if rc, ok := v.(*http.ResponseController); ok {
|
||||
return rc
|
||||
}
|
||||
}
|
||||
return http.NewResponseController(c.Writer)
|
||||
}
|
||||
Reference in New Issue
Block a user