From 89a551b964076f2e61b71c0b8fa34f9464100cb0 Mon Sep 17 00:00:00 2001 From: li Date: Fri, 10 Jul 2026 20:56:31 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix(handler):=20opsCaptureWriter=20?= =?UTF-8?q?=E9=87=8A=E6=94=BE=E5=90=8E=E8=AE=BF=E9=97=AE=20nil=20panic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #3961 --- .../handler/ops_capture_writer_nil_test.go | 36 +++++++++++++++++++ backend/internal/handler/ops_error_logger.go | 27 ++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 backend/internal/handler/ops_capture_writer_nil_test.go diff --git a/backend/internal/handler/ops_capture_writer_nil_test.go b/backend/internal/handler/ops_capture_writer_nil_test.go new file mode 100644 index 0000000000..76f50b8e45 --- /dev/null +++ b/backend/internal/handler/ops_capture_writer_nil_test.go @@ -0,0 +1,36 @@ +package handler + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestOpsCaptureWriter_NilInnerWriter_NoPanic(t *testing.T) { + w := &opsCaptureWriter{} + w.ResponseWriter = nil + + assert.NotPanics(t, func() { + assert.Equal(t, 0, w.Status()) + }, "Status() on released writer must not panic") + + assert.NotPanics(t, func() { + assert.Equal(t, -1, w.Size()) + }, "Size() on released writer must not panic") + + assert.NotPanics(t, func() { + assert.False(t, w.Written()) + }, "Written() on released writer must not panic") + + assert.NotPanics(t, func() { + n, err := w.Write([]byte("test")) + assert.Equal(t, 0, n) + assert.NoError(t, err) + }, "Write() on released writer must not panic") + + assert.NotPanics(t, func() { + n, err := w.WriteString("test") + assert.Equal(t, 0, n) + assert.NoError(t, err) + }, "WriteString() on released writer must not panic") +} diff --git a/backend/internal/handler/ops_error_logger.go b/backend/internal/handler/ops_error_logger.go index 5a1e57ff7d..16aee7fe5a 100644 --- a/backend/internal/handler/ops_error_logger.go +++ b/backend/internal/handler/ops_error_logger.go @@ -498,7 +498,31 @@ func releaseOpsCaptureWriter(w *opsCaptureWriter) { opsCaptureWriterPool.Put(w) } +func (w *opsCaptureWriter) Status() int { + if w.ResponseWriter == nil { + return 0 + } + return w.ResponseWriter.Status() +} + +func (w *opsCaptureWriter) Size() int { + if w.ResponseWriter == nil { + return -1 + } + return w.ResponseWriter.Size() +} + +func (w *opsCaptureWriter) Written() bool { + if w.ResponseWriter == nil { + return false + } + return w.ResponseWriter.Written() +} + func (w *opsCaptureWriter) Write(b []byte) (int, error) { + if w.ResponseWriter == nil { + return 0, nil + } if w.Status() >= 400 && w.limit > 0 && w.buf.Len() < w.limit { remaining := w.limit - w.buf.Len() if len(b) > remaining { @@ -511,6 +535,9 @@ func (w *opsCaptureWriter) Write(b []byte) (int, error) { } func (w *opsCaptureWriter) WriteString(s string) (int, error) { + if w.ResponseWriter == nil { + return 0, nil + } if w.Status() >= 400 && w.limit > 0 && w.buf.Len() < w.limit { remaining := w.limit - w.buf.Len() if len(s) > remaining { From bc3cb290276922074213c5bc8ebc404bc6d083a8 Mon Sep 17 00:00:00 2001 From: li Date: Fri, 10 Jul 2026 21:03:40 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix(handler):=20=E8=A1=A5=E9=BD=90=20opsCap?= =?UTF-8?q?tureWriter=20=E5=85=A8=E9=83=A8=E5=A7=94=E6=89=98=E6=96=B9?= =?UTF-8?q?=E6=B3=95=E7=9A=84=20nil=20=E5=AE=88=E5=8D=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../handler/ops_capture_writer_nil_test.go | 41 ++++++++++---- backend/internal/handler/ops_error_logger.go | 54 +++++++++++++++++++ 2 files changed, 86 insertions(+), 9 deletions(-) diff --git a/backend/internal/handler/ops_capture_writer_nil_test.go b/backend/internal/handler/ops_capture_writer_nil_test.go index 76f50b8e45..4e96333f9d 100644 --- a/backend/internal/handler/ops_capture_writer_nil_test.go +++ b/backend/internal/handler/ops_capture_writer_nil_test.go @@ -12,25 +12,48 @@ func TestOpsCaptureWriter_NilInnerWriter_NoPanic(t *testing.T) { assert.NotPanics(t, func() { assert.Equal(t, 0, w.Status()) - }, "Status() on released writer must not panic") - + }) assert.NotPanics(t, func() { assert.Equal(t, -1, w.Size()) - }, "Size() on released writer must not panic") - + }) assert.NotPanics(t, func() { assert.False(t, w.Written()) - }, "Written() on released writer must not panic") - + }) assert.NotPanics(t, func() { n, err := w.Write([]byte("test")) assert.Equal(t, 0, n) assert.NoError(t, err) - }, "Write() on released writer must not panic") - + }) assert.NotPanics(t, func() { n, err := w.WriteString("test") assert.Equal(t, 0, n) assert.NoError(t, err) - }, "WriteString() on released writer must not panic") + }) + assert.NotPanics(t, func() { + h := w.Header() + assert.NotNil(t, h) + }) + assert.NotPanics(t, func() { + w.WriteHeader(200) + }) + assert.NotPanics(t, func() { + w.WriteHeaderNow() + }) + assert.NotPanics(t, func() { + w.Flush() + }) + assert.NotPanics(t, func() { + conn, rw, err := w.Hijack() + assert.Nil(t, conn) + assert.Nil(t, rw) + assert.Error(t, err) + }) + assert.NotPanics(t, func() { + ch := w.CloseNotify() + assert.NotNil(t, ch) + }) + assert.NotPanics(t, func() { + p := w.Pusher() + assert.Nil(t, p) + }) } diff --git a/backend/internal/handler/ops_error_logger.go b/backend/internal/handler/ops_error_logger.go index 16aee7fe5a..64aa3ba495 100644 --- a/backend/internal/handler/ops_error_logger.go +++ b/backend/internal/handler/ops_error_logger.go @@ -1,11 +1,14 @@ package handler import ( + "bufio" "bytes" "context" "encoding/json" "errors" "log" + "net" + "net/http" "runtime" "runtime/debug" "strconv" @@ -519,6 +522,57 @@ func (w *opsCaptureWriter) Written() bool { return w.ResponseWriter.Written() } +func (w *opsCaptureWriter) Header() http.Header { + if w.ResponseWriter == nil { + return http.Header{} + } + return w.ResponseWriter.Header() +} + +func (w *opsCaptureWriter) WriteHeader(code int) { + if w.ResponseWriter == nil { + return + } + w.ResponseWriter.WriteHeader(code) +} + +func (w *opsCaptureWriter) WriteHeaderNow() { + if w.ResponseWriter == nil { + return + } + w.ResponseWriter.WriteHeaderNow() +} + +func (w *opsCaptureWriter) Flush() { + if w.ResponseWriter == nil { + return + } + w.ResponseWriter.Flush() +} + +func (w *opsCaptureWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { + if w.ResponseWriter == nil { + return nil, nil, errors.New("response writer released") + } + return w.ResponseWriter.Hijack() +} + +func (w *opsCaptureWriter) CloseNotify() <-chan bool { + if w.ResponseWriter == nil { + ch := make(chan bool) + close(ch) + return ch + } + return w.ResponseWriter.CloseNotify() +} + +func (w *opsCaptureWriter) Pusher() http.Pusher { + if w.ResponseWriter == nil { + return nil + } + return w.ResponseWriter.Pusher() +} + func (w *opsCaptureWriter) Write(b []byte) (int, error) { if w.ResponseWriter == nil { return 0, nil