Files
fasthttp/fasthttpadaptor/adaptor_test.go
T
Erik Dubbelboer 2272d532e1 Reimplement flushing support for fasthttpadaptor (#2081)
Use a simpler implementation, and do more tests.
Instead of https://github.com/valyala/fasthttp/pull/2069
2025-10-06 03:22:13 +02:00

706 lines
18 KiB
Go

package fasthttpadaptor
import (
"bufio"
"bytes"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"reflect"
"testing"
"time"
"github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttputil"
)
func TestNewFastHTTPHandler(t *testing.T) {
t.Parallel()
expectedMethod := fasthttp.MethodPost
expectedProto := "HTTP/1.1"
expectedProtoMajor := 1
expectedProtoMinor := 1
expectedRequestURI := "/foo/bar?baz=123"
expectedBody := "<!doctype html><html>"
expectedContentLength := len(expectedBody)
expectedHost := "foobar.com"
expectedRemoteAddr := "1.2.3.4:6789"
expectedHeader := map[string]string{
"Foo-Bar": "baz",
"Abc": "defg",
"XXX-Remote-Addr": "123.43.4543.345",
}
expectedURL, err := url.ParseRequestURI(expectedRequestURI)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
expectedContextKey := "contextKey"
expectedContextValue := "contextValue"
expectedContentType := "text/html; charset=utf-8"
callsCount := 0
nethttpH := func(w http.ResponseWriter, r *http.Request) {
callsCount++
if r.Method != expectedMethod {
t.Fatalf("unexpected method %q. Expecting %q", r.Method, expectedMethod)
}
if r.Proto != expectedProto {
t.Fatalf("unexpected proto %q. Expecting %q", r.Proto, expectedProto)
}
if r.ProtoMajor != expectedProtoMajor {
t.Fatalf("unexpected protoMajor %d. Expecting %d", r.ProtoMajor, expectedProtoMajor)
}
if r.ProtoMinor != expectedProtoMinor {
t.Fatalf("unexpected protoMinor %d. Expecting %d", r.ProtoMinor, expectedProtoMinor)
}
if r.RequestURI != expectedRequestURI {
t.Fatalf("unexpected requestURI %q. Expecting %q", r.RequestURI, expectedRequestURI)
}
if r.ContentLength != int64(expectedContentLength) {
t.Fatalf("unexpected contentLength %d. Expecting %d", r.ContentLength, expectedContentLength)
}
if len(r.TransferEncoding) != 0 {
t.Fatalf("unexpected transferEncoding %q. Expecting []", r.TransferEncoding)
}
if r.Host != expectedHost {
t.Fatalf("unexpected host %q. Expecting %q", r.Host, expectedHost)
}
if r.RemoteAddr != expectedRemoteAddr {
t.Fatalf("unexpected remoteAddr %q. Expecting %q", r.RemoteAddr, expectedRemoteAddr)
}
body, err := io.ReadAll(r.Body)
r.Body.Close()
if err != nil {
t.Fatalf("unexpected error when reading request body: %v", err)
}
if string(body) != expectedBody {
t.Fatalf("unexpected body %q. Expecting %q", body, expectedBody)
}
if !reflect.DeepEqual(r.URL, expectedURL) {
t.Fatalf("unexpected URL: %#v. Expecting %#v", r.URL, expectedURL)
}
if r.Context().Value(expectedContextKey) != expectedContextValue {
t.Fatalf("unexpected context value for key %q. Expecting %q", expectedContextKey, expectedContextValue)
}
for k, expectedV := range expectedHeader {
v := r.Header.Get(k)
if v != expectedV {
t.Fatalf("unexpected header value %q for key %q. Expecting %q", v, k, expectedV)
}
}
w.Header().Set("Header1", "value1")
w.Header().Set("Header2", "value2")
w.WriteHeader(http.StatusBadRequest)
if _, err := w.Write(body); err != nil {
t.Fatalf("unexpected error when writing response body: %v", err)
}
}
fasthttpH := NewFastHTTPHandler(http.HandlerFunc(nethttpH))
fasthttpH = setContextValueMiddleware(fasthttpH, expectedContextKey, expectedContextValue)
var ctx fasthttp.RequestCtx
var req fasthttp.Request
req.Header.SetMethod(expectedMethod)
req.SetRequestURI(expectedRequestURI)
req.Header.SetHost(expectedHost)
if _, err := req.BodyWriter().Write([]byte(expectedBody)); err != nil {
t.Fatalf("unexpected error when writing request body: %v", err)
}
for k, v := range expectedHeader {
req.Header.Set(k, v)
}
remoteAddr, err := net.ResolveTCPAddr("tcp", expectedRemoteAddr)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ctx.Init(&req, remoteAddr, nil)
fasthttpH(&ctx)
if callsCount != 1 {
t.Fatalf("unexpected callsCount: %d. Expecting 1", callsCount)
}
resp := &ctx.Response
if resp.StatusCode() != fasthttp.StatusBadRequest {
t.Fatalf("unexpected statusCode: %d. Expecting %d", resp.StatusCode(), fasthttp.StatusBadRequest)
}
if string(resp.Header.Peek("Header1")) != "value1" {
t.Fatalf("unexpected header value: %q. Expecting %q", resp.Header.Peek("Header1"), "value1")
}
if string(resp.Header.Peek("Header2")) != "value2" {
t.Fatalf("unexpected header value: %q. Expecting %q", resp.Header.Peek("Header2"), "value2")
}
if string(resp.Body()) != expectedBody {
t.Fatalf("unexpected response body %q. Expecting %q", resp.Body(), expectedBody)
}
if string(resp.Header.Peek("Content-Type")) != expectedContentType {
t.Fatalf("unexpected response content-type %q. Expecting %q", string(resp.Header.Peek("Content-Type")), expectedContentType)
}
}
func TestNewFastHTTPHandlerWithCookies(t *testing.T) {
expectedMethod := fasthttp.MethodPost
expectedRequestURI := "/foo/bar?baz=123"
expectedHost := "foobar.com"
expectedRemoteAddr := "1.2.3.4:6789"
var ctx fasthttp.RequestCtx
var req fasthttp.Request
req.Header.SetMethod(expectedMethod)
req.SetRequestURI(expectedRequestURI)
req.Header.SetHost(expectedHost)
req.Header.SetCookie("cookieOne", "valueCookieOne")
req.Header.SetCookie("cookieTwo", "valueCookieTwo")
remoteAddr, err := net.ResolveTCPAddr("tcp", expectedRemoteAddr)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ctx.Init(&req, remoteAddr, nil)
nethttpH := func(w http.ResponseWriter, r *http.Request) {
// real handler warped by middleware, in this example do nothing
}
fasthttpH := NewFastHTTPHandler(http.HandlerFunc(nethttpH))
netMiddleware := func(_ http.ResponseWriter, r *http.Request) {
// assume middleware do some change on r, such as reset header's host
r.Header.Set("Host", "example.com")
// convert ctx again in case request may modify by middleware
ctx.Request.Header.Set("Host", r.Header.Get("Host"))
// since cookies of r are not changed, expect "cookieOne=valueCookieOne"
cookie, _ := r.Cookie("cookieOne")
if err != nil {
// will error, but if line 172 is commented, then no error will happen
t.Errorf("should not error")
}
if cookie.Value != "valueCookieOne" {
t.Errorf("cookie error, expect %s, find %s", "valueCookieOne", cookie.Value)
}
// instead of using responseWriter and r, use ctx again, like what have done in fiber
fasthttpH(&ctx)
}
fastMiddleware := NewFastHTTPHandler(http.HandlerFunc(netMiddleware))
fastMiddleware(&ctx)
}
func setContextValueMiddleware(next fasthttp.RequestHandler, key string, value any) fasthttp.RequestHandler {
return func(ctx *fasthttp.RequestCtx) {
ctx.SetUserValue(key, value)
next(ctx)
}
}
func TestHijack(t *testing.T) {
t.Parallel()
nethttpH := func(w http.ResponseWriter, r *http.Request) {
if f, ok := w.(http.Hijacker); !ok {
t.Errorf("expected http.ResponseWriter to implement http.Hijacker")
} else {
if _, err := w.Write([]byte("foo")); err != nil {
t.Error(err)
}
if c, rw, err := f.Hijack(); err != nil {
t.Error(err)
} else {
if _, err := rw.WriteString("bar"); err != nil {
t.Error(err)
}
if err := rw.Flush(); err != nil {
t.Error(err)
}
if err := c.Close(); err != nil {
t.Error(err)
}
}
}
}
s := &fasthttp.Server{
Handler: NewFastHTTPHandler(http.HandlerFunc(nethttpH)),
}
ln := fasthttputil.NewInmemoryListener()
go func() {
if err := s.Serve(ln); err != nil {
t.Errorf("unexpected error: %v", err)
}
}()
clientCh := make(chan struct{})
go func() {
c, err := ln.Dial()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if _, err = c.Write([]byte("GET / HTTP/1.1\r\nHost: aa\r\n\r\n")); err != nil {
t.Errorf("unexpected error: %v", err)
}
buf, err := io.ReadAll(c)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if string(buf) != "foobar" {
t.Errorf("unexpected response: %q. Expecting %q", buf, "foobar")
}
close(clientCh)
}()
select {
case <-clientCh:
case <-time.After(time.Second):
t.Fatal("timeout")
}
}
func TestFlushHandler(t *testing.T) {
t.Parallel()
nethttpH := func(w http.ResponseWriter, r *http.Request) {
if f, ok := w.(http.Flusher); !ok {
t.Errorf("expected http.ResponseWriter to implement http.Flusher")
} else {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Length", "6")
w.Header().Set("X-Foo", "bar")
if _, err := w.Write([]byte("foo")); err != nil {
t.Error(err)
}
f.Flush()
time.Sleep(time.Millisecond * 500)
if _, err := w.Write([]byte("bar")); err != nil {
t.Error(err)
}
f.Flush()
}
}
s := &fasthttp.Server{
Handler: NewFastHTTPHandler(http.HandlerFunc(nethttpH)),
}
ln := fasthttputil.NewInmemoryListener()
go func() {
if err := s.Serve(ln); err != nil {
t.Errorf("unexpected error: %v", err)
}
}()
clientCh := make(chan struct{})
go func() {
c, err := ln.Dial()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if _, err = c.Write([]byte("GET / HTTP/1.1\r\nHost: aa\r\n\r\n")); err != nil {
t.Errorf("unexpected error: %v", err)
}
resp, err := http.ReadResponse(bufio.NewReader(c), nil)
if err != nil {
t.Errorf("unexpected error reading response: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("unexpected status code: %d. Expecting %d", resp.StatusCode, http.StatusOK)
}
if resp.Header.Get("Content-Type") != "text/plain; charset=utf-8" {
t.Errorf("unexpected Content-Type header: %q. Expecting %q", resp.Header.Get("Content-Type"), "text/plain; charset=utf-8")
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil && err != io.ErrUnexpectedEOF {
t.Errorf("unexpected error reading body: %v", err)
}
if string(body) != "foobar" {
t.Errorf("unexpected response body: %q. Expecting %q", body, "foobar")
}
close(clientCh)
}()
select {
case <-clientCh:
case <-time.After(time.Second):
t.Fatal("timeout")
}
}
func TestFlushHandlerClosed(t *testing.T) {
t.Parallel()
nethttpH := func(w http.ResponseWriter, r *http.Request) {
if f, ok := w.(http.Flusher); !ok {
t.Errorf("expected http.ResponseWriter to implement http.Flusher")
} else {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.Header().Set("Content-Length", "6")
w.Header().Set("X-Foo", "bar")
if _, err := w.Write([]byte("foo")); err != nil {
t.Error(err)
}
f.Flush()
time.Sleep(time.Second)
if _, err := w.Write([]byte("bar")); err != nil {
t.Error(err)
}
f.Flush()
}
}
s := &fasthttp.Server{
Handler: NewFastHTTPHandler(http.HandlerFunc(nethttpH)),
}
ln := fasthttputil.NewInmemoryListener()
go func() {
if err := s.Serve(ln); err != nil {
t.Errorf("unexpected error: %v", err)
}
}()
clientCh := make(chan struct{})
go func() {
c, err := ln.Dial()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if _, err = c.Write([]byte("GET / HTTP/1.1\r\nHost: aa\r\n\r\n")); err != nil {
t.Errorf("unexpected error: %v", err)
}
time.AfterFunc(500*time.Millisecond, func() {
c.Close()
})
resp, err := http.ReadResponse(bufio.NewReader(c), nil)
if err != nil {
t.Errorf("unexpected error reading response: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("unexpected status code: %d. Expecting %d", resp.StatusCode, http.StatusOK)
}
if resp.Header.Get("Content-Type") != "text/plain; charset=utf-8" {
t.Errorf("unexpected Content-Type header: %q. Expecting %q", resp.Header.Get("Content-Type"), "text/plain; charset=utf-8")
}
body, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil && err != io.ErrUnexpectedEOF {
t.Errorf("unexpected error reading body: %v", err)
}
if string(body) != "foo" {
t.Errorf("unexpected response body: %q. Expecting %q", body, "foo")
}
close(clientCh)
}()
select {
case <-clientCh:
case <-time.After(time.Second):
t.Fatal("timeout")
}
}
func TestHijackFlush(t *testing.T) {
t.Parallel()
nethttpH := func(w http.ResponseWriter, r *http.Request) {
if f, ok := w.(http.Hijacker); !ok {
t.Errorf("expected http.ResponseWriter to implement http.Hijacker")
} else {
if _, err := w.Write([]byte("foo")); err != nil {
t.Error(err)
}
if c, rw, err := f.Hijack(); err != nil {
t.Error(err)
} else {
if _, err := rw.WriteString("bar"); err != nil {
t.Error(err)
}
if err := rw.Flush(); err != nil {
t.Error(err)
}
time.Sleep(time.Second)
_ = c.Close()
}
}
}
s := &fasthttp.Server{
Handler: NewFastHTTPHandler(http.HandlerFunc(nethttpH)),
}
ln := fasthttputil.NewInmemoryListener()
go func() {
if err := s.Serve(ln); err != nil {
t.Errorf("unexpected error: %v", err)
}
}()
clientCh := make(chan struct{})
go func() {
c, err := ln.Dial()
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if _, err = c.Write([]byte("GET / HTTP/1.1\r\nHost: aa\r\n\r\n")); err != nil {
t.Errorf("unexpected error: %v", err)
}
time.AfterFunc(500*time.Millisecond, func() {
c.Close()
})
buf, err := io.ReadAll(c)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
if string(buf) != "foobar" {
t.Errorf("unexpected response: %q. Expecting %q", buf, "foobar")
}
close(clientCh)
}()
select {
case <-clientCh:
case <-time.After(time.Second):
t.Fatal("timeout")
}
}
func TestResourceRecyclingUnderLoad_OneEndpoint(t *testing.T) {
t.Parallel()
handler := func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
}
s := &fasthttp.Server{
Handler: NewFastHTTPHandler(http.HandlerFunc(handler)),
}
requestCount := 10
responseTimeout := 500 * time.Millisecond
expectedBody := "Hello World!"
ln := fasthttputil.NewInmemoryListener()
go func() {
if err := s.Serve(ln); err != nil {
t.Errorf("unexpected error: %v", err)
}
}()
for reqID := 1; reqID <= requestCount; reqID++ {
req := httptest.NewRequest("GET", "/", http.NoBody)
body, err := sendRequest(ln, req, responseTimeout)
if err != nil {
t.Errorf("[%d] unexpected error sending request: %v", reqID, err)
}
if string(body) != expectedBody {
t.Errorf("[%d] unexpected response: %q. Expecting %q", reqID, body, expectedBody)
}
}
}
func TestResourceRecyclingUnderLoad_MultipleEndpoints(t *testing.T) {
t.Parallel()
handlers := []struct {
endpoint string
handler fasthttp.RequestHandler
expectedBody string
}{
{
endpoint: "/done",
handler: NewFastHTTPHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello World!")
})),
expectedBody: "Hello World!",
},
{
endpoint: "/flush",
handler: NewFastHTTPHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if f, ok := w.(http.Flusher); ok {
if _, err := w.Write([]byte("foo")); err != nil {
t.Error(err)
}
f.Flush()
time.Sleep(250 * time.Millisecond)
if _, err := w.Write([]byte("bar")); err != nil {
t.Error(err)
}
f.Flush()
} else {
http.Error(w, "Flusher not supported", http.StatusInternalServerError)
}
})),
expectedBody: "foobar",
},
{
endpoint: "/hijack",
handler: NewFastHTTPHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if hj, ok := w.(http.Hijacker); ok {
conn, rw, err := hj.Hijack()
if err != nil {
t.Errorf("unexpected error: %v", err)
return
}
defer conn.Close()
if _, err := rw.WriteString("hijacked"); err != nil {
t.Errorf("unexpected error: %v", err)
}
rw.Flush()
} else {
http.Error(w, "Hijacker not supported", http.StatusInternalServerError)
}
})),
expectedBody: "hijacked",
},
}
s := &fasthttp.Server{
Handler: func(ctx *fasthttp.RequestCtx) {
for _, h := range handlers {
if string(ctx.Path()) == h.endpoint {
h.handler(ctx)
return
}
}
ctx.Error("Not Found", fasthttp.StatusNotFound)
},
}
repeatCount := 3
responseTimeout := 500 * time.Millisecond
ln := fasthttputil.NewInmemoryListener()
go func() {
if err := s.Serve(ln); err != nil {
t.Errorf("unexpected error: %v", err)
}
}()
for range repeatCount {
for _, handler := range handlers {
req := httptest.NewRequest("GET", handler.endpoint, http.NoBody)
body, err := sendRequest(ln, req, responseTimeout)
if err != nil {
t.Errorf("[%s] unexpected error sending request: %v", handler.endpoint, err)
}
if string(body) != handler.expectedBody {
t.Errorf("[%s] unexpected response: %q. Expecting %q", handler.endpoint, body, handler.expectedBody)
}
}
}
}
func sendRequest(ln *fasthttputil.InmemoryListener, req *http.Request, responseTimeout time.Duration) ([]byte, error) {
c, err := ln.Dial()
if err != nil {
return nil, err
}
if err := req.Write(c); err != nil {
return nil, err
}
time.AfterFunc(responseTimeout, func() {
c.Close()
})
response, err := io.ReadAll(c)
if err != nil {
return nil, err
}
resp, err := http.ReadResponse(bufio.NewReader(bytes.NewReader(response)), nil)
if err != nil {
// Hijacked response, return the full response instead of the parsed body.
return response, nil
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}
func TestNewFastHTTPHandlerPanic(t *testing.T) {
var ctx fasthttp.RequestCtx
var req fasthttp.Request
req.Header.SetMethod(fasthttp.MethodPost)
req.SetRequestURI("/")
req.Header.SetHost("example.com")
remoteAddr, err := net.ResolveTCPAddr("tcp", "1.2.3.4:6789")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
ctx.Init(&req, remoteAddr, nil)
nethttpH := func(w http.ResponseWriter, r *http.Request) {
panic("test panic")
}
fasthttpH := NewFastHTTPHandler(http.HandlerFunc(nethttpH))
defer func() {
recover() //nolint:errcheck
}()
fasthttpH(&ctx)
t.Error("expected panic, but it didn't happen")
}