Files
upstream-chi/middleware/wrap_writer_test.go
upstream-mirror 72c9f2ee75
Some checks failed
Test / test (1.24.x, ubuntu-latest) (push) Successful in 2m56s
Test / test (1.25.x, ubuntu-latest) (push) Successful in 2m15s
Test / test (1.26.x, ubuntu-latest) (push) Successful in 2m23s
Test / test (1.27.x, ubuntu-latest) (push) Failing after 3m56s
Test / test (1.24.x, windows-latest) (push) Has been cancelled
Test / test (1.25.x, windows-latest) (push) Has been cancelled
Test / test (1.26.x, windows-latest) (push) Has been cancelled
Test / test (1.27.x, windows-latest) (push) Has been cancelled
snapshot of https://github.com/go-chi/chi.git (master, 2026-09-15; blobs > 131072B omitted)
2026-09-15 21:48:17 +05:00

185 lines
6.0 KiB
Go

package middleware
import (
"bytes"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// readerFromRecorder is a ResponseRecorder that also satisfies io.ReaderFrom,
// mirroring the real net/http response writer (which implements ReadFrom and
// is what httpFancyWriter.ReadFrom fast-paths to). Bytes copied via ReadFrom
// are recorded to the body so a test can observe whether they reached the
// original writer.
type readerFromRecorder struct {
*httptest.ResponseRecorder
}
func (r *readerFromRecorder) ReadFrom(src io.Reader) (int64, error) {
return io.Copy(r.ResponseRecorder.Body, src)
}
func TestWrapResponseWriterStatusWhenFlushed(t *testing.T) {
for _, writer := range []struct {
name string
wrap func(http.ResponseWriter) WrapResponseWriter
}{
{"flushWriter", func(w http.ResponseWriter) WrapResponseWriter {
return &flushWriter{basicWriter{ResponseWriter: w}}
}},
{"flushHijackWriter", func(w http.ResponseWriter) WrapResponseWriter {
return &flushHijackWriter{basicWriter{ResponseWriter: w}}
}},
{"httpFancyWriter", func(w http.ResponseWriter) WrapResponseWriter {
return &httpFancyWriter{basicWriter{ResponseWriter: w}}
}},
{"http2FancyWriter", func(w http.ResponseWriter) WrapResponseWriter {
return &http2FancyWriter{basicWriter{ResponseWriter: w}}
}},
} {
t.Run(writer.name, func(t *testing.T) {
for _, status := range []struct {
name string
code int
want int
}{
{"implicit", 0, http.StatusOK},
{"explicit", http.StatusCreated, http.StatusCreated},
} {
t.Run(status.name, func(t *testing.T) {
original := httptest.NewRecorder()
wrap := writer.wrap(original)
if status.code != 0 {
wrap.WriteHeader(status.code)
}
wrap.(http.Flusher).Flush()
assertEqual(t, status.want, wrap.Status())
assertEqual(t, status.want, original.Code)
assertEqual(t, true, original.Flushed)
assertEqual(t, 0, wrap.BytesWritten())
// Flushing commits the status, even before any body is written.
wrap.WriteHeader(http.StatusInternalServerError)
_, err := wrap.Write([]byte("body"))
assertNoError(t, err)
wrap.(http.Flusher).Flush()
assertEqual(t, status.want, wrap.Status())
assertEqual(t, status.want, original.Code)
assertEqual(t, "body", original.Body.String())
assertEqual(t, 4, wrap.BytesWritten())
})
}
})
}
}
func TestBasicWritesTeesWritesWithoutDiscard(t *testing.T) {
// explicitly create the struct instead of NewRecorder to control the value of Code
original := &httptest.ResponseRecorder{
HeaderMap: make(http.Header),
Body: new(bytes.Buffer),
}
wrap := &basicWriter{ResponseWriter: original}
var buf bytes.Buffer
wrap.Tee(&buf)
_, err := wrap.Write([]byte("hello world"))
assertNoError(t, err)
assertEqual(t, 200, original.Code)
assertEqual(t, []byte("hello world"), original.Body.Bytes())
assertEqual(t, []byte("hello world"), buf.Bytes())
assertEqual(t, 11, wrap.BytesWritten())
}
func TestBasicWriterDiscardsWritesToOriginalResponseWriter(t *testing.T) {
t.Run("With Tee", func(t *testing.T) {
// explicitly create the struct instead of NewRecorder to control the value of Code
original := &httptest.ResponseRecorder{
HeaderMap: make(http.Header),
Body: new(bytes.Buffer),
}
wrap := &basicWriter{ResponseWriter: original}
var buf bytes.Buffer
wrap.Tee(&buf)
wrap.Discard()
_, err := wrap.Write([]byte("hello world"))
assertNoError(t, err)
assertEqual(t, 0, original.Code) // wrapper shouldn't call WriteHeader implicitly
assertEqual(t, 0, original.Body.Len())
assertEqual(t, []byte("hello world"), buf.Bytes())
assertEqual(t, 11, wrap.BytesWritten())
})
t.Run("Without Tee", func(t *testing.T) {
// explicitly create the struct instead of NewRecorder to control the value of Code
original := &httptest.ResponseRecorder{
HeaderMap: make(http.Header),
Body: new(bytes.Buffer),
}
wrap := &basicWriter{ResponseWriter: original}
wrap.Discard()
_, err := wrap.Write([]byte("hello world"))
assertNoError(t, err)
assertEqual(t, 0, original.Code) // wrapper shouldn't call WriteHeader implicitly
assertEqual(t, 0, original.Body.Len())
assertEqual(t, 11, wrap.BytesWritten())
})
}
// TestHttpFancyWriterReadFromByteCountWithTee is a regression test for
// https://github.com/go-chi/chi/issues/1067.
// httpFancyWriter.ReadFrom was adding n to basicWriter.bytes even when the
// write went through basicWriter.Write (which already increments the counter),
// resulting in double-counting the bytes when a Tee writer was set.
func TestHttpFancyWriterReadFromByteCountWithTee(t *testing.T) {
original := &httptest.ResponseRecorder{
HeaderMap: make(http.Header),
Body: new(bytes.Buffer),
}
f := &httpFancyWriter{basicWriter: basicWriter{ResponseWriter: original}}
var teeBuf bytes.Buffer
f.Tee(&teeBuf)
const input = "hello world"
n, err := f.ReadFrom(strings.NewReader(input))
assertNoError(t, err)
assertEqual(t, int64(len(input)), n)
// Before the fix, BytesWritten() returned 22 (double-counted).
assertEqual(t, len(input), f.BytesWritten())
assertEqual(t, []byte(input), teeBuf.Bytes())
}
// TestHttpFancyWriterReadFromHonorsDiscard verifies that Discard() suppresses
// the body even when it is written via ReadFrom/io.Copy. Before the fix, the
// non-tee ReadFrom path streamed straight to the original ResponseWriter's
// ReaderFrom, bypassing the discard flag entirely.
func TestHttpFancyWriterReadFromHonorsDiscard(t *testing.T) {
original := &readerFromRecorder{&httptest.ResponseRecorder{
HeaderMap: make(http.Header),
Body: new(bytes.Buffer),
}}
f := &httpFancyWriter{basicWriter: basicWriter{ResponseWriter: original}}
f.Discard()
const input = "hello world"
n, err := f.ReadFrom(strings.NewReader(input))
assertNoError(t, err)
assertEqual(t, int64(len(input)), n)
// The data must NOT reach the original writer once Discard() is set.
assertEqual(t, 0, original.Body.Len())
// BytesWritten still reflects the bytes consumed.
assertEqual(t, len(input), f.BytesWritten())
}