Files
oxy/buffer/buffer_test.go
2026-01-26 10:38:03 +01:00

588 lines
17 KiB
Go

package buffer
import (
"bufio"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vulcand/oxy/v2/forward"
"github.com/vulcand/oxy/v2/testutils"
"github.com/vulcand/oxy/v2/utils"
)
func TestBuffer_simple(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("hello"))
})
t.Cleanup(srv.Close)
// forwarder will proxy the request to whatever destination
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.MustParseRequestURI(srv.URL)
fwd.ServeHTTP(w, req)
})
// stream handler will forward requests to redirect
st, err := New(rdr)
require.NoError(t, err)
proxy := httptest.NewServer(st)
t.Cleanup(proxy.Close)
re, body, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, re.StatusCode)
assert.Equal(t, "hello", string(body))
}
func TestBuffer_chunkedEncodingSuccess(t *testing.T) {
var (
reqBody string
contentLength int64
)
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
body, err := io.ReadAll(req.Body)
require.NoError(t, err)
reqBody = string(body)
contentLength = req.ContentLength
_, _ = w.Write([]byte("hello"))
})
t.Cleanup(srv.Close)
// forwarder will proxy the request to whatever destination
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.MustParseRequestURI(srv.URL)
fwd.ServeHTTP(w, req)
})
// stream handler will forward requests to redirect
st, err := New(rdr)
require.NoError(t, err)
proxy := httptest.NewServer(st)
t.Cleanup(proxy.Close)
conn, err := net.Dial("tcp", testutils.MustParseRequestURI(proxy.URL).Host)
require.NoError(t, err)
_, _ = fmt.Fprintf(conn, "POST / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n5\r\ntest1\r\n5\r\ntest2\r\n0\r\n\r\n")
status, err := bufio.NewReader(conn).ReadString('\n')
require.NoError(t, err)
assert.Equal(t, "testtest1test2", reqBody)
assert.Equal(t, "HTTP/1.1 200 OK\r\n", status)
assert.EqualValues(t, len(reqBody), contentLength)
}
func TestBuffer_chunkedEncodingLimitReached(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("hello"))
})
t.Cleanup(srv.Close)
// forwarder will proxy the request to whatever destination
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.MustParseRequestURI(srv.URL)
fwd.ServeHTTP(w, req)
})
// stream handler will forward requests to redirect
st, err := New(rdr, MemRequestBodyBytes(4), MaxRequestBodyBytes(8))
require.NoError(t, err)
proxy := httptest.NewServer(st)
t.Cleanup(proxy.Close)
conn, err := net.Dial("tcp", testutils.MustParseRequestURI(proxy.URL).Host)
require.NoError(t, err)
_, _ = fmt.Fprint(conn, "POST / HTTP/1.1\r\nHost: 127.0.0.1:8080\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n5\r\ntest1\r\n5\r\ntest2\r\n0\r\n\r\n")
status, err := bufio.NewReader(conn).ReadString('\n')
require.NoError(t, err)
assert.Equal(t, "HTTP/1.1 413 Request Entity Too Large\r\n", status)
}
func TestBuffer_chunkedResponse(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, _ *http.Request) {
h := w.(http.Hijacker)
conn, _, _ := h.Hijack()
_, _ = fmt.Fprintf(conn, "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n5\r\ntest1\r\n5\r\ntest2\r\n0\r\n\r\n")
_ = conn.Close()
})
t.Cleanup(srv.Close)
fwd := forward.New(false)
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.MustParseRequestURI(srv.URL)
fwd.ServeHTTP(w, req)
})
st, err := New(rdr)
require.NoError(t, err)
proxy := httptest.NewServer(st)
t.Cleanup(proxy.Close)
re, body, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, "testtest1test2", string(body))
assert.Equal(t, http.StatusOK, re.StatusCode)
assert.Equal(t, strconv.Itoa(len("testtest1test2")), re.Header.Get("Content-Length"))
}
func TestBuffer_requestLimitReached(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("hello"))
})
t.Cleanup(srv.Close)
// forwarder will proxy the request to whatever destination
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.MustParseRequestURI(srv.URL)
fwd.ServeHTTP(w, req)
})
// stream handler will forward requests to redirect
st, err := New(rdr, MaxRequestBodyBytes(4))
require.NoError(t, err)
proxy := httptest.NewServer(st)
t.Cleanup(proxy.Close)
re, _, err := testutils.Get(proxy.URL, testutils.Body("this request is too long"))
require.NoError(t, err)
assert.Equal(t, http.StatusRequestEntityTooLarge, re.StatusCode)
}
func TestBuffer_responseLimitReached(t *testing.T) {
cases := []struct {
name string
body string
maxResponseBodyBytes int64
}{
{
name: "small limit with body larger than max response bytes",
body: "hello, this response is too large",
maxResponseBodyBytes: 4,
},
{
name: "small limit with body larger than 32768 bytes",
body: strings.Repeat("A", 32769),
maxResponseBodyBytes: 4,
},
{
name: "larger limit with body larger than 32768 bytes",
body: strings.Repeat("A", 32769),
maxResponseBodyBytes: 2000,
},
{
name: "larger limit with body larger than 32768 + 1999 bytes",
body: strings.Repeat("A", 32769+1999),
maxResponseBodyBytes: 2000,
},
{
name: "larger limit with body larger than 32768 + 2000 bytes",
body: strings.Repeat("A", 32769+2000),
maxResponseBodyBytes: 2000,
},
{
name: "larger limit with body larger than 65536 + 1999 bytes",
body: strings.Repeat("A", 65537+1999),
maxResponseBodyBytes: 2000,
},
{
name: "larger limit with body larger than 65536 + 2000 bytes",
body: strings.Repeat("A", 65537+2000),
maxResponseBodyBytes: 2000,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(tc.body))
})
t.Cleanup(srv.Close)
// forwarder will proxy the request to whatever destination
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.MustParseRequestURI(srv.URL)
fwd.ServeHTTP(w, req)
})
// stream handler will forward requests to redirect
st, err := New(rdr, MaxResponseBodyBytes(tc.maxResponseBodyBytes))
require.NoError(t, err)
proxy := httptest.NewServer(st)
t.Cleanup(proxy.Close)
re, _, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusInternalServerError, re.StatusCode)
})
}
}
func TestBuffer_fileStreamingResponse(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("hello, this response is too large to fit in memory"))
})
t.Cleanup(srv.Close)
// forwarder will proxy the request to whatever destination
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.MustParseRequestURI(srv.URL)
fwd.ServeHTTP(w, req)
})
// stream handler will forward requests to redirect
st, err := New(rdr, MemResponseBodyBytes(4))
require.NoError(t, err)
proxy := httptest.NewServer(st)
t.Cleanup(proxy.Close)
re, body, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, re.StatusCode)
assert.Equal(t, "hello, this response is too large to fit in memory", string(body))
}
func TestBuffer_customErrorHandler(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("hello, this response is too large"))
})
t.Cleanup(srv.Close)
// forwarder will proxy the request to whatever destination
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.MustParseRequestURI(srv.URL)
fwd.ServeHTTP(w, req)
})
// stream handler will forward requests to redirect
errHandler := utils.ErrorHandlerFunc(func(w http.ResponseWriter, _ *http.Request, _ error) {
w.WriteHeader(http.StatusTeapot)
_, _ = w.Write([]byte(http.StatusText(http.StatusTeapot)))
})
st, err := New(rdr, MaxResponseBodyBytes(4), ErrorHandler(errHandler))
require.NoError(t, err)
proxy := httptest.NewServer(st)
t.Cleanup(proxy.Close)
re, _, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusTeapot, re.StatusCode)
}
func TestBuffer_notModified(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusNotModified)
})
t.Cleanup(srv.Close)
// forwarder will proxy the request to whatever destination
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.MustParseRequestURI(srv.URL)
fwd.ServeHTTP(w, req)
})
// stream handler will forward requests to redirect
st, err := New(rdr)
require.NoError(t, err)
proxy := httptest.NewServer(st)
t.Cleanup(proxy.Close)
re, _, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusNotModified, re.StatusCode)
}
func TestBuffer_noBody(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
})
t.Cleanup(srv.Close)
// forwarder will proxy the request to whatever destination
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.MustParseRequestURI(srv.URL)
fwd.ServeHTTP(w, req)
})
// stream handler will forward requests to redirect
st, err := New(rdr)
require.NoError(t, err)
proxy := httptest.NewServer(st)
t.Cleanup(proxy.Close)
re, _, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, re.StatusCode)
}
// Make sure that stream handler preserves TLS settings.
func TestBuffer_preservesTLS(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
t.Cleanup(srv.Close)
// forwarder will proxy the request to whatever destination
fwd := forward.New(false)
var cs *tls.ConnectionState
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
cs = req.TLS
req.URL = testutils.MustParseRequestURI(srv.URL)
fwd.ServeHTTP(w, req)
})
// stream handler will forward requests to redirect
st, err := New(rdr)
require.NoError(t, err)
proxy := httptest.NewUnstartedServer(st)
proxy.StartTLS()
t.Cleanup(proxy.Close)
re, _, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, re.StatusCode)
assert.NotNil(t, cs)
}
func TestBuffer_notNilBody(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte("hello"))
})
t.Cleanup(srv.Close)
// forwarder will proxy the request to whatever destination
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.MustParseRequestURI(srv.URL)
// During a request check if the request body is no nil before sending to the next middleware
// Because we can send a POST request without body
assert.NotNil(t, req.Body)
fwd.ServeHTTP(w, req)
})
// stream handler will forward requests to redirect
st, err := New(rdr, MaxRequestBodyBytes(10))
require.NoError(t, err)
proxy := httptest.NewServer(st)
t.Cleanup(proxy.Close)
re, body, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, re.StatusCode)
assert.Equal(t, "hello", string(body))
re, body, err = testutils.Post(proxy.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, re.StatusCode)
assert.Equal(t, "hello", string(body))
}
func TestBuffer_GRPC_ErrorResponse(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Grpc-Status", "10" /* ABORTED */)
w.WriteHeader(http.StatusOK)
// To skip the "Content-Length" header.
w.(http.Flusher).Flush()
})
t.Cleanup(srv.Close)
// forwarder will proxy the request to whatever destination
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.MustParseRequestURI(srv.URL)
fwd.ServeHTTP(w, req)
})
// stream handler will forward requests to redirect
st, err := New(rdr)
require.NoError(t, err)
proxy := httptest.NewServer(st)
t.Cleanup(proxy.Close)
re, body, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, re.StatusCode)
assert.Empty(t, body)
}
func TestBuffer_GRPC_OKResponse(t *testing.T) {
srv := testutils.NewHandler(func(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Grpc-Status", "0" /* OK */)
_, _ = w.Write([]byte("grpc-body"))
// To skip the "Content-Length" header.
w.(http.Flusher).Flush()
})
t.Cleanup(srv.Close)
// forwarder will proxy the request to whatever destination
fwd := forward.New(false)
// this is our redirect to server
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.MustParseRequestURI(srv.URL)
fwd.ServeHTTP(w, req)
})
// stream handler will forward requests to redirect
st, err := New(rdr)
require.NoError(t, err)
proxy := httptest.NewServer(st)
t.Cleanup(proxy.Close)
re, body, err := testutils.Get(proxy.URL)
require.NoError(t, err)
assert.Equal(t, http.StatusOK, re.StatusCode)
assert.Equal(t, "grpc-body", string(body))
}
func TestBuffer_disableRequestBuffer(t *testing.T) {
var (
reqBody string
contentLength int64
actuallyBuffered bool
)
srv := testutils.NewHandler(func(w http.ResponseWriter, req *http.Request) {
body, err := io.ReadAll(req.Body)
require.NoError(t, err)
reqBody = string(body)
contentLength = req.ContentLength
// When buffering is disabled, chunked requests should preserve their transfer encoding, and have no content-length.
actuallyBuffered = contentLength > 0 || len(req.TransferEncoding) == 0
_, _ = w.Write([]byte("response"))
})
t.Cleanup(srv.Close)
fwd := forward.New(false)
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.MustParseRequestURI(srv.URL)
fwd.ServeHTTP(w, req)
})
// buffer with disabled request buffering.
st, err := New(rdr, DisableRequestBuffer())
require.NoError(t, err)
proxy := httptest.NewServer(st)
t.Cleanup(proxy.Close)
// Send a chunked request - when buffering is disabled, it should remain chunked.
conn, err := net.Dial("tcp", testutils.MustParseRequestURI(proxy.URL).Host)
require.NoError(t, err)
t.Cleanup(func() {
_ = conn.Close()
})
_, _ = fmt.Fprintf(conn, "POST / HTTP/1.1\r\nHost: %s\r\nTransfer-Encoding: chunked\r\n\r\n4\r\ntest\r\n0\r\n\r\n", testutils.MustParseRequestURI(proxy.URL).Host)
status, err := bufio.NewReader(conn).ReadString('\n')
require.NoError(t, err)
assert.Equal(t, "HTTP/1.1 200 OK\r\n", status)
assert.Equal(t, "test", reqBody)
// When buffering is disabled, chunked encoding should be preserved (not converted to Content-Length).
assert.False(t, actuallyBuffered, "Request should not have been buffered")
assert.Equal(t, int64(-1), contentLength, "Content-Length should be -1 for chunked requests when not buffered")
}
func TestBuffer_disableResponseBuffer(t *testing.T) {
largeResponseBody := strings.Repeat("A", 1000)
srv := testutils.NewHandler(func(w http.ResponseWriter, _ *http.Request) {
_, _ = w.Write([]byte(largeResponseBody))
})
t.Cleanup(srv.Close)
fwd := forward.New(false)
rdr := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
req.URL = testutils.MustParseRequestURI(srv.URL)
fwd.ServeHTTP(w, req)
})
// buffer with a small max response size.
st, err := New(rdr, MaxResponseBodyBytes(4))
require.NoError(t, err)
proxy := httptest.NewServer(st)
t.Cleanup(proxy.Close)
resp, _, err := testutils.Get(proxy.URL)
require.NoError(t, err)
// Response should not pass through as it exceeds the limit.
assert.Equal(t, http.StatusInternalServerError, resp.StatusCode)
// buffer with disabled response buffering and a small max response size.
st, err = New(rdr, DisableResponseBuffer(), MaxResponseBodyBytes(4))
require.NoError(t, err)
proxy2 := httptest.NewServer(st)
t.Cleanup(proxy2.Close)
resp2, body, err := testutils.Get(proxy2.URL)
require.NoError(t, err)
// Response should pass through even though it exceeds the limit, because buffering has been disabled.
assert.Equal(t, http.StatusOK, resp2.StatusCode)
assert.Equal(t, largeResponseBody, string(body))
}