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
185 lines
6.0 KiB
Go
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())
|
|
}
|