fix: StatusWriter Unwrap and process output error recovery (#23383)

Add Unwrap() to StatusWriter so http.ResponseController.SetWriteDeadline
can reach the underlying net.Conn through the middleware wrapper. Without
this, the agent's 20s WriteTimeout killed blocking process output
connections.

Also add 30s headroom to the write deadline in handleProcessOutput so
the response can be written after a full-duration blocking wait.

On the tool layer, waitForProcess and the process_output tool now try a
non-blocking snapshot on any error, not just context timeout. Transport
errors (like the WriteTimeout EOF) previously returned with no process
ID and no recovery path. Now if the process finished, the result is
returned transparently. If still running, the error includes the process
ID and tells the agent to use process_output.
This commit is contained in:
Mathias Fredriksson
2026-03-20 20:00:55 +00:00
committed by GitHub
parent 599f21afa3
commit 4aa94fcd4c
5 changed files with 198 additions and 30 deletions
+6
View File
@@ -90,6 +90,12 @@ func minInt(a, b int) int {
return b
}
// Unwrap returns the underlying ResponseWriter, allowing
// http.ResponseController to reach it for SetWriteDeadline, etc.
func (w *StatusWriter) Unwrap() http.ResponseWriter {
return w.ResponseWriter
}
func (w *StatusWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
hijacker, ok := w.ResponseWriter.(http.Hijacker)
if !ok {
+40
View File
@@ -7,6 +7,7 @@ import (
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
@@ -117,6 +118,45 @@ func TestStatusWriter(t *testing.T) {
require.Equal(t, "hijacked", err.Error())
})
t.Run("Unwrap", func(t *testing.T) {
t.Parallel()
rec := httptest.NewRecorder()
w := &tracing.StatusWriter{ResponseWriter: rec}
got := w.Unwrap()
require.Equal(t, rec, got, "Unwrap should return the inner ResponseWriter")
})
t.Run("SetWriteDeadlineThroughMiddleware", func(t *testing.T) {
t.Parallel()
// Use a real HTTP server so the ResponseWriter is backed by
// a net.Conn that supports SetWriteDeadline.
// http.ResponseController reaches it by calling Unwrap() on
// each wrapper in the chain.
var setDeadlineErr error
handlerCalled := false
handler := tracing.StatusWriterMiddleware(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
handlerCalled = true
rc := http.NewResponseController(w)
setDeadlineErr = rc.SetWriteDeadline(time.Now().Add(time.Minute))
w.WriteHeader(http.StatusNoContent)
}))
srv := httptest.NewServer(handler)
t.Cleanup(srv.Close)
req, err := http.NewRequestWithContext(t.Context(), http.MethodGet, srv.URL, nil)
require.NoError(t, err)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
resp.Body.Close()
require.True(t, handlerCalled, "handler must be invoked")
require.Equal(t, http.StatusNoContent, resp.StatusCode)
// Assert in the test goroutine, not the handler goroutine.
require.NoError(t, setDeadlineErr, "SetWriteDeadline should succeed through StatusWriter")
})
t.Run("Middleware", func(t *testing.T) {
t.Parallel()