pull/3382/merge
pj 2025-03-31 10:44:06 -03:00 committed by GitHub
commit 93587d8418
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 170 additions and 104 deletions

44
ctx.go
View File

@ -444,6 +444,28 @@ func (c *DefaultCtx) Cookie(cookie *Cookie) {
fasthttp.ReleaseCookie(fcookie)
}
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
//
// Due to current limitations in how fasthttp works, Deadline operates as a nop.
// See: https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945
func (*DefaultCtx) Deadline() (time.Time, bool) {
return time.Time{}, false
}
// Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
// The close of the Done channel may happen asynchronously,
// after the cancel function returns.
//
// Due to current limitations in how fasthttp works, Done operates as a nop.
// See: https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945
func (*DefaultCtx) Done() <-chan struct{} {
return nil
}
// Cookies are used for getting a cookie value by key.
// Defaults to the empty string "" if the cookie doesn't exist.
// If a default value is given, it will return that value if the cookie doesn't exist.
@ -468,6 +490,18 @@ func (c *DefaultCtx) Download(file string, filename ...string) error {
return c.SendFile(file)
}
// If Done is not yet closed, Err returns nil.
// If Done is closed, Err returns a non-nil error explaining why:
// context.DeadlineExceeded if the context's deadline passed,
// or context.Canceled if the context was canceled for some other reason.
// After Err returns a non-nil error, successive calls to Err return the same error.
//
// Due to current limitations in how fasthttp works, Err operates as a nop.
// See: https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945
func (*DefaultCtx) Err() error {
return nil
}
// Request return the *fasthttp.Request object
// This allows you to use all fasthttp request methods
// https://godoc.org/github.com/valyala/fasthttp#Request
@ -1804,6 +1838,16 @@ func (c *DefaultCtx) Vary(fields ...string) {
c.Append(HeaderVary, fields...)
}
// Value makes it possible to pass any values under keys scoped to the request
// and therefore available to all following routes that match the request.
//
// All the values are removed from ctx after returning from the top
// RequestHandler. Additionally, Close method is called on each value
// implementing io.Closer before removing the value from ctx.
func (c *DefaultCtx) Value(key any) any {
return c.fasthttp.UserValue(key)
}
// Write appends p into response body.
func (c *DefaultCtx) Write(p []byte) (int, error) {
c.fasthttp.Response.AppendBody(p)

View File

@ -4,10 +4,10 @@ package fiber
import (
"bufio"
"context"
"crypto/tls"
"io"
"mime/multipart"
"time"
"github.com/valyala/fasthttp"
)
@ -49,11 +49,6 @@ type Ctx interface {
// RequestCtx returns *fasthttp.RequestCtx that carries a deadline
// a cancellation signal, and other values across API boundaries.
RequestCtx() *fasthttp.RequestCtx
// Context returns a context implementation that was set by
// user earlier or returns a non-nil, empty context,if it was not set earlier.
Context() context.Context
// SetContext sets a context implementation by user.
SetContext(ctx context.Context)
// Cookie sets a cookie by passing a cookie struct.
Cookie(cookie *Cookie)
// Cookies are used for getting a cookie value by key.
@ -62,11 +57,53 @@ type Ctx interface {
// The returned value is only valid within the handler. Do not store any references.
// Make copies or use the Immutable setting to use the value outside the Handler.
Cookies(key string, defaultValue ...string) string
// Deadline returns the time when work done on behalf of this context
// should be canceled. Deadline returns ok==false when no deadline is
// set. Successive calls to Deadline return the same results.
Deadline() (deadline time.Time, ok bool)
// Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
// The close of the Done channel may happen asynchronously,
// after the cancel function returns.
//
// WithCancel arranges for Done to be closed when cancel is called;
// WithDeadline arranges for Done to be closed when the deadline
// expires; WithTimeout arranges for Done to be closed when the timeout
// elapses.
//
// Done is provided for use in select statements:
//
// // Stream generates values with DoSomething and sends them to out
// // until DoSomething returns an error or ctx.Done is closed.
// func Stream(ctx context.Context, out chan<- Value) error {
// for {
// v, err := DoSomething(ctx)
// if err != nil {
// return err
// }
// select {
// case <-ctx.Done():
// return ctx.Err()
// case out <- v:
// }
// }
// }
//
// See https://blog.golang.org/pipelines for more examples of how to use
// a Done channel for cancellation.
Done() <-chan struct{}
// Download transfers the file from path as an attachment.
// Typically, browsers will prompt the user for download.
// By default, the Content-Disposition header filename= parameter is the filepath (this typically appears in the browser dialog).
// Override this default with the filename parameter.
Download(file string, filename ...string) error
// If Done is not yet closed, Err returns nil.
// If Done is closed, Err returns a non-nil error explaining why:
// DeadlineExceeded if the context's deadline passed,
// or Canceled if the context was canceled for some other reason.
// After Err returns a non-nil error, successive calls to Err return the same error.
Err() error
// Request return the *fasthttp.Request object
// This allows you to use all fasthttp request methods
// https://godoc.org/github.com/valyala/fasthttp#Request
@ -317,6 +354,13 @@ type Ctx interface {
// Vary adds the given header field to the Vary response header.
// This will append the header, if not already listed, otherwise leaves it listed in the current location.
Vary(fields ...string)
// Value makes it possible to pass any values under keys scoped to the request
// and therefore available to all following routes that match the request.
//
// All the values are removed from ctx after returning from the top
// RequestHandler. Additionally, Close method is called on each value
// implementing io.Closer before removing the value from ctx.
Value(key any) any
// Write appends p into response body.
Write(p []byte) (int, error)
// Writef appends f & a into response body writer.

View File

@ -9,7 +9,6 @@ import (
"bytes"
"compress/gzip"
"compress/zlib"
"context"
"crypto/tls"
"embed"
"encoding/hex"
@ -882,76 +881,6 @@ func Test_Ctx_RequestCtx(t *testing.T) {
require.Equal(t, "*fasthttp.RequestCtx", fmt.Sprintf("%T", c.RequestCtx()))
}
// go test -run Test_Ctx_Context
func Test_Ctx_Context(t *testing.T) {
t.Parallel()
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
t.Run("Nil_Context", func(t *testing.T) {
t.Parallel()
ctx := c.Context()
require.Equal(t, ctx, context.Background())
})
t.Run("ValueContext", func(t *testing.T) {
t.Parallel()
testKey := struct{}{}
testValue := "Test Value"
ctx := context.WithValue(context.Background(), testKey, testValue) //nolint:staticcheck // not needed for tests
require.Equal(t, testValue, ctx.Value(testKey))
})
}
// go test -run Test_Ctx_SetContext
func Test_Ctx_SetContext(t *testing.T) {
t.Parallel()
app := New()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
testKey := struct{}{}
testValue := "Test Value"
ctx := context.WithValue(context.Background(), testKey, testValue) //nolint:staticcheck // not needed for tests
c.SetContext(ctx)
require.Equal(t, testValue, c.Context().Value(testKey))
}
// go test -run Test_Ctx_Context_Multiple_Requests
func Test_Ctx_Context_Multiple_Requests(t *testing.T) {
t.Parallel()
testKey := struct{}{}
testValue := "foobar-value"
app := New()
app.Get("/", func(c Ctx) error {
ctx := c.Context()
if ctx.Value(testKey) != nil {
return c.SendStatus(StatusInternalServerError)
}
input := utils.CopyString(Query(c, "input", "NO_VALUE"))
ctx = context.WithValue(ctx, testKey, fmt.Sprintf("%s_%s", testValue, input)) //nolint:staticcheck // not needed for tests
c.SetContext(ctx)
return c.Status(StatusOK).SendString(fmt.Sprintf("resp_%s_returned", input))
})
// Consecutive Requests
for i := 1; i <= 10; i++ {
t.Run(fmt.Sprintf("request_%d", i), func(t *testing.T) {
t.Parallel()
resp, err := app.Test(httptest.NewRequest(MethodGet, fmt.Sprintf("/?input=%d", i), nil))
require.NoError(t, err, "Unexpected error from response")
require.Equal(t, StatusOK, resp.StatusCode, "context.Context returned from c.Context() is reused")
b, err := io.ReadAll(resp.Body)
require.NoError(t, err, "Unexpected error from reading response body")
require.Equal(t, fmt.Sprintf("resp_%d_returned", i), string(b), "response text incorrect")
})
}
}
// go test -run Test_Ctx_Cookie
func Test_Ctx_Cookie(t *testing.T) {
t.Parallel()
@ -2258,6 +2187,73 @@ func Test_Ctx_Locals(t *testing.T) {
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
}
// go test -run Test_Ctx_Deadline
func Test_Ctx_Deadline(t *testing.T) {
t.Parallel()
app := New()
app.Use(func(c Ctx) error {
return c.Next()
})
app.Get("/test", func(c Ctx) error {
deadline, ok := c.Deadline()
require.Equal(t, time.Time{}, deadline)
require.Equal(t, require.False, ok)
return nil
})
resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
}
// go test -run Test_Ctx_Done
func Test_Ctx_Done(t *testing.T) {
t.Parallel()
app := New()
app.Use(func(c Ctx) error {
return c.Next()
})
app.Get("/test", func(c Ctx) error {
require.Equal(t, (<-chan struct{})(nil), c.Done())
return nil
})
resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
}
// go test -run Test_Ctx_Err
func Test_Ctx_Err(t *testing.T) {
t.Parallel()
app := New()
app.Use(func(c Ctx) error {
return c.Next()
})
app.Get("/test", func(c Ctx) error {
require.Equal(t, require.NoError, c.Err())
return nil
})
resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
}
// go test -run Test_Ctx_Value
func Test_Ctx_Value(t *testing.T) {
t.Parallel()
app := New()
app.Use(func(c Ctx) error {
c.Locals("john", "doe")
return c.Next()
})
app.Get("/test", func(c Ctx) error {
require.Equal(t, "doe", c.Value("john"))
return nil
})
resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
}
// go test -run Test_Ctx_Locals_Generic
func Test_Ctx_Locals_Generic(t *testing.T) {
t.Parallel()

View File

@ -62,8 +62,6 @@ func New(config ...Config) fiber.Handler {
if err == nil && valid {
// Store in both Locals and Context
c.Locals(tokenKey, key)
ctx := context.WithValue(c.Context(), tokenKey, key)
c.SetContext(ctx)
return cfg.SuccessHandler(c)
}
return cfg.ErrorHandler(c, err)
@ -74,14 +72,14 @@ func New(config ...Config) fiber.Handler {
// returns an empty string if the token does not exist
func TokenFromContext(c any) string {
switch ctx := c.(type) {
case context.Context:
if token, ok := ctx.Value(tokenKey).(string); ok {
return token
}
case fiber.Ctx:
if token, ok := ctx.Locals(tokenKey).(string); ok {
return token
}
case context.Context:
if token, ok := ctx.Value(tokenKey).(string); ok {
return token
}
default:
panic("unsupported context type, expected fiber.Ctx or context.Context")
}

View File

@ -544,8 +544,7 @@ func Test_TokenFromContext(t *testing.T) {
}))
// Verify that TokenFromContext works with context.Context
app.Get("/", func(c fiber.Ctx) error {
ctx := c.Context()
token := TokenFromContext(ctx)
token := TokenFromContext(c)
return c.SendString(token)
})

View File

@ -39,10 +39,6 @@ func New(config ...Config) fiber.Handler {
// Add the request ID to locals
c.Locals(requestIDKey, rid)
// Add the request ID to UserContext
ctx := context.WithValue(c.Context(), requestIDKey, rid)
c.SetContext(ctx)
// Continue stack
return c.Next()
}

View File

@ -73,14 +73,6 @@ func Test_RequestID_FromContext(t *testing.T) {
},
},
},
{
name: "From context.Context",
args: args{
inputFunc: func(c fiber.Ctx) any {
return c.Context()
},
},
},
}
for _, tt := range tests {

View File

@ -19,12 +19,9 @@ func New(h fiber.Handler, timeout time.Duration, tErrs ...error) fiber.Handler {
// Create a context with the specified timeout; any operation exceeding
// this deadline will be canceled automatically.
timeoutContext, cancel := context.WithTimeout(ctx.Context(), timeout)
timeoutContext, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
// Replace the default Fiber context with our timeout-bound context.
ctx.SetContext(timeoutContext)
// Run the handler and check for relevant errors.
err := runHandler(ctx, h, tErrs)

View File

@ -41,7 +41,7 @@ func TestTimeout_Success(t *testing.T) {
// 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 {
if err := sleepWithContext(c, 10*time.Millisecond, context.DeadlineExceeded); err != nil {
return err
}
return c.SendString("OK")
@ -60,7 +60,7 @@ func TestTimeout_Exceeded(t *testing.T) {
// 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 {
if err := sleepWithContext(c, 200*time.Millisecond, context.DeadlineExceeded); err != nil {
return err
}
return c.SendString("Should never get here")
@ -81,7 +81,7 @@ func TestTimeout_CustomError(t *testing.T) {
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 {
if err := sleepWithContext(c, 200*time.Millisecond, errCustomTimeout); err != nil {
return fmt.Errorf("wrapped: %w", err)
}
return c.SendString("Should never get here")