Files
fiber/client/core_test.go
zeekayandHanzo Dev f60e254acb module: rename to github.com/zap-proto/fiber/v3
Fork of gofiber/fiber v3 based on the v3.2.0 tag. Rewrites all self-imports
github.com/gofiber/fiber/v3 -> github.com/zap-proto/fiber/v3. Upstream
gofiber/schema and gofiber/utils/v2 dependencies are left untouched.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-10 12:49:35 -07:00

545 lines
13 KiB
Go

package client
import (
"bytes"
"context"
"crypto/tls"
"errors"
"net"
"sync"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/valyala/fasthttp"
"github.com/valyala/fasthttp/fasthttputil"
"github.com/zap-proto/fiber/v3"
)
func Test_AddMissing_Port(t *testing.T) {
t.Parallel()
type args struct {
addr string
isTLS bool
}
tests := []struct {
name string
want string
args args
}{
{
name: "do anything",
args: args{
addr: "example.com:1234",
},
want: "example.com:1234",
},
{
name: "add 80 port",
args: args{
addr: "example.com",
},
want: "example.com:80",
},
{
name: "add 443 port",
args: args{
addr: "example.com",
isTLS: true,
},
want: "example.com:443",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tt.want, addMissingPort(tt.args.addr, tt.args.isTLS))
})
}
}
func Test_Exec_Func(t *testing.T) {
t.Parallel()
ln := fasthttputil.NewInmemoryListener()
app := fiber.New()
app.Get("/normal", func(c fiber.Ctx) error {
return c.SendString(c.Hostname())
})
app.Get("/return-error", func(_ fiber.Ctx) error {
return errors.New("the request is error")
})
app.Get("/redirect", func(c fiber.Ctx) error {
return c.Redirect().Status(fiber.StatusFound).To("/normal")
})
app.Get("/hang-up", func(c fiber.Ctx) error {
time.Sleep(time.Second)
return c.SendString(c.Hostname() + " hang up")
})
go func() {
assert.NoError(t, app.Listener(ln, fiber.ListenConfig{DisableStartupMessage: true}))
}()
time.Sleep(300 * time.Millisecond)
t.Run("normal request", func(t *testing.T) {
t.Parallel()
core, client, req := newCore(), New(), AcquireRequest()
core.ctx = context.Background()
core.client = client
core.req = req
client.SetDial(func(_ string) (net.Conn, error) { return ln.Dial() })
req.RawRequest.SetRequestURI("http://example.com/normal")
resp, err := core.execFunc()
require.NoError(t, err)
require.Equal(t, 200, resp.RawResponse.StatusCode())
require.Equal(t, "example.com", string(resp.RawResponse.Body()))
})
t.Run("follow redirect with retry config", func(t *testing.T) {
t.Parallel()
core, client, req := newCore(), New(), AcquireRequest()
core.ctx = context.Background()
core.client = client
core.req = req
client.SetRetryConfig(&RetryConfig{MaxRetryCount: 1})
client.SetDial(func(_ string) (net.Conn, error) { return ln.Dial() })
req.SetMaxRedirects(1)
req.RawRequest.Header.SetMethod(fiber.MethodGet)
req.RawRequest.SetRequestURI("http://example.com/redirect")
resp, err := core.execFunc()
require.NoError(t, err)
require.Equal(t, 200, resp.RawResponse.StatusCode())
require.Equal(t, "example.com", string(resp.RawResponse.Body()))
})
t.Run("the request return an error", func(t *testing.T) {
t.Parallel()
core, client, req := newCore(), New(), AcquireRequest()
core.ctx = context.Background()
core.client = client
core.req = req
client.SetDial(func(_ string) (net.Conn, error) { return ln.Dial() })
req.RawRequest.SetRequestURI("http://example.com/return-error")
resp, err := core.execFunc()
require.NoError(t, err)
require.Equal(t, 500, resp.RawResponse.StatusCode())
require.Equal(t, "the request is error", string(resp.RawResponse.Body()))
})
t.Run("the request timeout", func(t *testing.T) {
t.Parallel()
core, client, req := newCore(), New(), AcquireRequest()
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
core.ctx = ctx
core.client = client
core.req = req
client.SetDial(func(_ string) (net.Conn, error) { return ln.Dial() })
req.RawRequest.SetRequestURI("http://example.com/hang-up")
_, err := core.execFunc()
require.Equal(t, ErrTimeoutOrCancel, err)
})
t.Run("cancel drains errChan", func(t *testing.T) {
core, client, req := newCore(), New(), AcquireRequest()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
core.ctx = ctx
core.client = client
core.req = req
req.RawRequest.SetRequestURI("http://example.com/drain-err")
blockingTransport := newBlockingErrTransport(errors.New("upstream failure"))
client.transport = blockingTransport
defer blockingTransport.release()
type execResult struct {
resp *Response
err error
}
resultCh := make(chan execResult, 1)
go func() {
resp, err := core.execFunc()
resultCh <- execResult{resp: resp, err: err}
}()
select {
case <-blockingTransport.called:
case <-time.After(time.Second):
t.Fatal("transport Do was not invoked")
}
cancel()
var result execResult
select {
case result = <-resultCh:
case <-time.After(time.Second):
t.Fatal("execFunc did not return")
}
require.Nil(t, result.resp)
require.ErrorIs(t, result.err, ErrTimeoutOrCancel)
blockingTransport.release()
select {
case <-blockingTransport.finished:
case <-time.After(time.Second):
t.Fatal("transport Do did not finish")
}
})
}
func Test_Execute(t *testing.T) {
t.Parallel()
ln := fasthttputil.NewInmemoryListener()
app := fiber.New()
app.Get("/normal", func(c fiber.Ctx) error {
return c.SendString(c.Hostname())
})
app.Get("/return-error", func(_ fiber.Ctx) error {
return errors.New("the request is error")
})
app.Get("/hang-up", func(c fiber.Ctx) error {
time.Sleep(time.Second)
return c.SendString(c.Hostname() + " hang up")
})
go func() {
assert.NoError(t, app.Listener(ln, fiber.ListenConfig{DisableStartupMessage: true}))
}()
t.Run("add user request hooks", func(t *testing.T) {
t.Parallel()
core, client, req := newCore(), New(), AcquireRequest()
client.AddRequestHook(func(_ *Client, _ *Request) error {
require.Equal(t, "http://example.com", req.URL())
return nil
})
client.SetDial(func(_ string) (net.Conn, error) {
return ln.Dial()
})
req.SetURL("http://example.com")
resp, err := core.execute(context.Background(), client, req)
require.NoError(t, err)
require.Equal(t, "Not Found", string(resp.RawResponse.Body()))
})
t.Run("add user response hooks", func(t *testing.T) {
t.Parallel()
core, client, req := newCore(), New(), AcquireRequest()
client.AddResponseHook(func(_ *Client, _ *Response, req *Request) error {
require.Equal(t, "http://example.com", req.URL())
return nil
})
client.SetDial(func(_ string) (net.Conn, error) {
return ln.Dial()
})
req.SetURL("http://example.com")
resp, err := core.execute(context.Background(), client, req)
require.NoError(t, err)
require.Equal(t, "Not Found", string(resp.RawResponse.Body()))
})
t.Run("no timeout", func(t *testing.T) {
t.Parallel()
core, client, req := newCore(), New(), AcquireRequest()
client.SetDial(func(_ string) (net.Conn, error) {
return ln.Dial()
})
req.SetURL("http://example.com/hang-up")
resp, err := core.execute(context.Background(), client, req)
require.NoError(t, err)
require.Equal(t, "example.com hang up", string(resp.RawResponse.Body()))
})
t.Run("client timeout", func(t *testing.T) {
t.Parallel()
core, client, req := newCore(), New(), AcquireRequest()
client.SetTimeout(500 * time.Millisecond)
client.SetDial(func(_ string) (net.Conn, error) {
return ln.Dial()
})
req.SetURL("http://example.com/hang-up")
_, err := core.execute(context.Background(), client, req)
require.Equal(t, ErrTimeoutOrCancel, err)
})
t.Run("request timeout", func(t *testing.T) {
t.Parallel()
core, client, req := newCore(), New(), AcquireRequest()
client.SetDial(func(_ string) (net.Conn, error) {
return ln.Dial()
})
req.SetURL("http://example.com/hang-up").
SetTimeout(300 * time.Millisecond)
_, err := core.execute(context.Background(), client, req)
require.Equal(t, ErrTimeoutOrCancel, err)
})
t.Run("request timeout has higher level", func(t *testing.T) {
t.Parallel()
core, client, req := newCore(), New(), AcquireRequest()
client.SetTimeout(30 * time.Millisecond)
client.SetDial(func(_ string) (net.Conn, error) {
return ln.Dial()
})
req.SetURL("http://example.com/hang-up").
SetTimeout(3000 * time.Millisecond)
resp, err := core.execute(context.Background(), client, req)
require.NoError(t, err)
require.Equal(t, "example.com hang up", string(resp.RawResponse.Body()))
})
}
type blockingErrTransport struct {
err error
called chan struct{}
unblock chan struct{}
finished chan struct{}
calledOnce sync.Once
releaseOnce sync.Once
finishedOnce sync.Once
}
func newBlockingErrTransport(err error) *blockingErrTransport {
return &blockingErrTransport{
err: err,
called: make(chan struct{}),
unblock: make(chan struct{}),
finished: make(chan struct{}),
}
}
func (b *blockingErrTransport) Do(_ *fasthttp.Request, _ *fasthttp.Response) error {
b.calledOnce.Do(func() { close(b.called) })
<-b.unblock
b.finishedOnce.Do(func() { close(b.finished) })
return b.err
}
func (b *blockingErrTransport) DoTimeout(req *fasthttp.Request, resp *fasthttp.Response, _ time.Duration) error {
return b.Do(req, resp)
}
func (b *blockingErrTransport) DoDeadline(req *fasthttp.Request, resp *fasthttp.Response, _ time.Time) error {
return b.Do(req, resp)
}
func (b *blockingErrTransport) DoRedirects(req *fasthttp.Request, resp *fasthttp.Response, _ int) error {
return b.Do(req, resp)
}
func (*blockingErrTransport) CloseIdleConnections() {
}
func (*blockingErrTransport) TLSConfig() *tls.Config {
return nil
}
func (*blockingErrTransport) SetTLSConfig(_ *tls.Config) {
}
func (*blockingErrTransport) SetDial(_ fasthttp.DialFunc) {
}
func (*blockingErrTransport) Client() any {
return nil
}
func (*blockingErrTransport) StreamResponseBody() bool {
return false
}
func (*blockingErrTransport) SetStreamResponseBody(_ bool) {
}
func (b *blockingErrTransport) release() {
b.releaseOnce.Do(func() { close(b.unblock) })
}
func Test_Core_RequestBodyStream(t *testing.T) {
t.Parallel()
t.Run("request with body stream is properly copied", func(t *testing.T) {
t.Parallel()
app := fiber.New()
app.Post("/echo", func(c fiber.Ctx) error {
body := c.Body()
return c.Send(body)
})
ln := fasthttputil.NewInmemoryListener()
go func() {
err := app.Listener(ln, fiber.ListenConfig{DisableStartupMessage: true})
if err != nil {
panic(err)
}
}()
t.Cleanup(func() {
require.NoError(t, app.Shutdown())
})
client := New().SetDial(func(_ string) (net.Conn, error) {
return ln.Dial()
})
// Create a request with a body stream using SetRawBody which properly sets the body
streamContent := "this is streamed body content"
req := AcquireRequest().SetClient(client)
req.SetURL("http://example.com/echo")
req.SetMethod(fiber.MethodPost)
req.SetRawBody([]byte(streamContent))
resp, err := req.Send()
require.NoError(t, err)
defer resp.Close()
require.Equal(t, streamContent, string(resp.Body()))
})
t.Run("request body stream with content length", func(t *testing.T) {
t.Parallel()
resultCh := make(chan struct {
body string
length int
}, 1)
app := fiber.New()
app.Post("/check-length", func(c fiber.Ctx) error {
resultCh <- struct {
body string
length int
}{
body: string(c.Body()),
length: c.Request().Header.ContentLength(),
}
return c.SendString("ok")
})
ln := fasthttputil.NewInmemoryListener()
go func() {
err := app.Listener(ln, fiber.ListenConfig{DisableStartupMessage: true})
if err != nil {
panic(err)
}
}()
t.Cleanup(func() {
require.NoError(t, app.Shutdown())
})
client := New().SetDial(func(_ string) (net.Conn, error) {
return ln.Dial()
})
streamContent := "body with known length"
req := AcquireRequest().SetClient(client)
req.SetURL("http://example.com/check-length")
req.SetMethod(fiber.MethodPost)
req.SetRawBody([]byte(streamContent))
resp, err := req.Send()
require.NoError(t, err)
defer resp.Close()
result := <-resultCh
require.Equal(t, streamContent, result.body)
require.Equal(t, len(streamContent), result.length)
})
t.Run("raw body stream survives CopyTo", func(t *testing.T) {
t.Parallel()
const streamContent = "streaming raw request body"
resultCh := make(chan struct {
body string
length int
}, 1)
app := fiber.New()
app.Post("/copy-to-body-stream", func(c fiber.Ctx) error {
body := string(c.Body())
resultCh <- struct {
body string
length int
}{
body: body,
length: c.Request().Header.ContentLength(),
}
return c.SendString(body)
})
ln := fasthttputil.NewInmemoryListener()
go func() {
err := app.Listener(ln, fiber.ListenConfig{DisableStartupMessage: true})
if err != nil {
panic(err)
}
}()
t.Cleanup(func() {
require.NoError(t, app.Shutdown())
})
core, client, req := newCore(), New(), AcquireRequest()
core.ctx = context.Background()
core.client = client
core.req = req
client.SetDial(func(_ string) (net.Conn, error) {
return ln.Dial()
})
req.RawRequest.SetRequestURI("http://example.com/copy-to-body-stream")
req.RawRequest.Header.SetMethod(fiber.MethodPost)
req.RawRequest.SetBodyStream(bytes.NewBufferString(streamContent), len(streamContent))
resp, err := core.execFunc()
require.NoError(t, err)
defer resp.Close()
result := <-resultCh
require.Equal(t, streamContent, string(resp.Body()))
require.Equal(t, streamContent, result.body)
require.Equal(t, len(streamContent), result.length)
})
}