mirror of https://github.com/gofiber/fiber.git
131 lines
4.4 KiB
Go
131 lines
4.4 KiB
Go
package timeout
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"net/http/httptest"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/gofiber/fiber/v3"
|
||
"github.com/stretchr/testify/require"
|
||
)
|
||
|
||
var (
|
||
// Custom error that we treat like a timeout when returned by the handler.
|
||
errCustomTimeout = errors.New("custom timeout error")
|
||
|
||
// Some unrelated error that should NOT trigger a request timeout.
|
||
errUnrelated = errors.New("unmatched error")
|
||
)
|
||
|
||
// sleepWithContext simulates a task that takes `d` time, but returns `te` if the context is canceled.
|
||
func sleepWithContext(ctx context.Context, d time.Duration, te error) error {
|
||
timer := time.NewTimer(d)
|
||
defer timer.Stop() // Clean up the timer
|
||
|
||
select {
|
||
case <-ctx.Done():
|
||
return te
|
||
case <-timer.C:
|
||
return nil
|
||
}
|
||
}
|
||
|
||
// TestTimeout_Success tests a handler that completes within the allotted timeout.
|
||
func TestTimeout_Success(t *testing.T) {
|
||
t.Parallel()
|
||
app := fiber.New()
|
||
|
||
// Our middleware wraps a handler that sleeps for 10ms, well under the 50ms limit.
|
||
app.Get("/fast", New(func(c fiber.Ctx) error {
|
||
// Simulate some work
|
||
if err := sleepWithContext(c.Context(), 10*time.Millisecond, context.DeadlineExceeded); err != nil {
|
||
return err
|
||
}
|
||
return c.SendString("OK")
|
||
}, 50*time.Millisecond))
|
||
|
||
req := httptest.NewRequest(fiber.MethodGet, "/fast", nil)
|
||
resp, err := app.Test(req)
|
||
require.NoError(t, err, "app.Test(req) should not fail")
|
||
require.Equal(t, fiber.StatusOK, resp.StatusCode, "Expected 200 OK for fast requests")
|
||
}
|
||
|
||
// TestTimeout_Exceeded tests a handler that exceeds the provided timeout.
|
||
func TestTimeout_Exceeded(t *testing.T) {
|
||
t.Parallel()
|
||
app := fiber.New()
|
||
|
||
// This handler sleeps 200ms, exceeding the 100ms limit.
|
||
app.Get("/slow", New(func(c fiber.Ctx) error {
|
||
if err := sleepWithContext(c.Context(), 200*time.Millisecond, context.DeadlineExceeded); err != nil {
|
||
return err
|
||
}
|
||
return c.SendString("Should never get here")
|
||
}, 100*time.Millisecond))
|
||
|
||
req := httptest.NewRequest(fiber.MethodGet, "/slow", nil)
|
||
resp, err := app.Test(req)
|
||
require.NoError(t, err, "app.Test(req) should not fail")
|
||
require.Equal(t, fiber.StatusRequestTimeout, resp.StatusCode, "Expected 408 Request Timeout")
|
||
}
|
||
|
||
// TestTimeout_CustomError tests that returning a user-defined error is also treated as a timeout.
|
||
func TestTimeout_CustomError(t *testing.T) {
|
||
t.Parallel()
|
||
app := fiber.New()
|
||
|
||
// This handler sleeps 50ms and returns errCustomTimeout if canceled.
|
||
app.Get("/custom", New(func(c fiber.Ctx) error {
|
||
// Sleep might time out, or might return early. If the context is canceled,
|
||
// we treat errCustomTimeout as a 'timeout-like' condition.
|
||
if err := sleepWithContext(c.Context(), 200*time.Millisecond, errCustomTimeout); err != nil {
|
||
return fmt.Errorf("wrapped: %w", err)
|
||
}
|
||
return c.SendString("Should never get here")
|
||
}, 100*time.Millisecond, errCustomTimeout))
|
||
|
||
req := httptest.NewRequest(fiber.MethodGet, "/custom", nil)
|
||
resp, err := app.Test(req)
|
||
require.NoError(t, err, "app.Test(req) should not fail")
|
||
require.Equal(t, fiber.StatusRequestTimeout, resp.StatusCode, "Expected 408 for custom timeout error")
|
||
}
|
||
|
||
// TestTimeout_UnmatchedError checks that if the handler returns an error
|
||
// that is neither a deadline exceeded nor a custom 'timeout' error, it is
|
||
// propagated as a regular 500 (internal server error).
|
||
func TestTimeout_UnmatchedError(t *testing.T) {
|
||
t.Parallel()
|
||
app := fiber.New()
|
||
|
||
app.Get("/unmatched", New(func(_ fiber.Ctx) error {
|
||
return errUnrelated // Not in the custom error list
|
||
}, 100*time.Millisecond, errCustomTimeout))
|
||
|
||
req := httptest.NewRequest(fiber.MethodGet, "/unmatched", nil)
|
||
resp, err := app.Test(req)
|
||
require.NoError(t, err, "app.Test(req) should not fail")
|
||
require.Equal(t, fiber.StatusInternalServerError, resp.StatusCode,
|
||
"Expected 500 because the error is not recognized as a timeout error")
|
||
}
|
||
|
||
// TestTimeout_ZeroDuration tests the edge case where the timeout is set to zero.
|
||
// Usually this means the request can never exceed a 'deadline' – effectively no timeout.
|
||
func TestTimeout_ZeroDuration(t *testing.T) {
|
||
t.Parallel()
|
||
app := fiber.New()
|
||
|
||
app.Get("/zero", New(func(c fiber.Ctx) error {
|
||
// Sleep 50ms, but there's no real 'deadline' since zero-timeout.
|
||
time.Sleep(50 * time.Millisecond)
|
||
return c.SendString("No timeout used")
|
||
}, 0))
|
||
|
||
req := httptest.NewRequest(fiber.MethodGet, "/zero", nil)
|
||
resp, err := app.Test(req)
|
||
require.NoError(t, err, "app.Test(req) should not fail")
|
||
require.Equal(t, fiber.StatusOK, resp.StatusCode, "Expected 200 OK with zero timeout")
|
||
}
|