mirror of
https://github.com/gofiber/fiber.git
synced 2025-05-31 11:52:41 +00:00
feat: fiber.Context implement context.Context (#3382)
* Ctx implements context.Context * fix up some linting issues * added some tests * no message * fiber.Ctx implements context.Context * no message * implement compile-time check * update formatting * update compile-time checks --------- Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com>
This commit is contained in:
parent
a5f76a7bee
commit
9bd7a1c50d
67
ctx.go
67
ctx.go
@ -39,12 +39,14 @@ const (
|
||||
maxDetectionPaths = 3
|
||||
)
|
||||
|
||||
var (
|
||||
_ io.Writer = (*DefaultCtx)(nil) // Compile-time check
|
||||
_ context.Context = (*DefaultCtx)(nil) // Compile-time check
|
||||
)
|
||||
|
||||
// The contextKey type is unexported to prevent collisions with context keys defined in
|
||||
// other packages.
|
||||
type contextKey int
|
||||
|
||||
// userContextKey define the key name for storing context.Context in *fasthttp.RequestCtx
|
||||
const userContextKey contextKey = 0 // __local_user_context__
|
||||
type contextKey int //nolint:unused // need for future (nolintlint)
|
||||
|
||||
// DefaultCtx is the default implementation of the Ctx interface
|
||||
// generation tool `go install github.com/vburenin/ifacemaker@975a95966976eeb2d4365a7fb236e274c54da64c`
|
||||
@ -391,23 +393,6 @@ func (c *DefaultCtx) RequestCtx() *fasthttp.RequestCtx {
|
||||
return c.fasthttp
|
||||
}
|
||||
|
||||
// Context returns a context implementation that was set by
|
||||
// user earlier or returns a non-nil, empty context,if it was not set earlier.
|
||||
func (c *DefaultCtx) Context() context.Context {
|
||||
ctx, ok := c.fasthttp.UserValue(userContextKey).(context.Context)
|
||||
if !ok {
|
||||
ctx = context.Background()
|
||||
c.SetContext(ctx)
|
||||
}
|
||||
|
||||
return ctx
|
||||
}
|
||||
|
||||
// SetContext sets a context implementation by user.
|
||||
func (c *DefaultCtx) SetContext(ctx context.Context) {
|
||||
c.fasthttp.SetUserValue(userContextKey, ctx)
|
||||
}
|
||||
|
||||
// Cookie sets a cookie by passing a cookie struct.
|
||||
func (c *DefaultCtx) Cookie(cookie *Cookie) {
|
||||
fcookie := fasthttp.AcquireCookie()
|
||||
@ -444,6 +429,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 +475,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
|
||||
@ -1816,6 +1835,12 @@ func (c *DefaultCtx) Vary(fields ...string) {
|
||||
c.Append(HeaderVary, fields...)
|
||||
}
|
||||
|
||||
// Value makes it possible to retrieve values (Locals) under keys scoped to the request
|
||||
// and therefore available to all following routes that match the request.
|
||||
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)
|
||||
|
@ -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,27 @@ 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.
|
||||
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 +328,9 @@ 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 retrieve values (Locals) under keys scoped to the request
|
||||
// and therefore available to all following routes that match the request.
|
||||
Value(key any) any
|
||||
// Write appends p into response body.
|
||||
Write(p []byte) (int, error)
|
||||
// Writef appends f & a into response body writer.
|
||||
|
138
ctx_test.go
138
ctx_test.go
@ -9,7 +9,6 @@ import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"compress/zlib"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"embed"
|
||||
"encoding/hex"
|
||||
@ -881,76 +880,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()
|
||||
@ -2257,6 +2186,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.False(t, 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.NoError(t, 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()
|
||||
|
@ -41,20 +41,36 @@ app.Post("/", func(c fiber.Ctx) error {
|
||||
})
|
||||
```
|
||||
|
||||
### Context
|
||||
## Context
|
||||
|
||||
`Context` returns a context implementation that was set by the user earlier or returns a non-nil, empty context if it was not set earlier.
|
||||
`Context` implements `context.Context`. However due to [current limitations in how fasthttp](https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945) works, `Deadline()`, `Done()` and `Err()` operate as a nop.
|
||||
|
||||
```go title="Signature"
|
||||
func (c fiber.Ctx) Context() context.Context
|
||||
func (c fiber.Ctx) Deadline() (deadline time.Time, ok bool)
|
||||
func (c fiber.Ctx) Done() <-chan struct{}
|
||||
func (c fiber.Ctx) Err() error
|
||||
func (c fiber.Ctx) Value(key any) any
|
||||
```
|
||||
|
||||
```go title="Example"
|
||||
app.Get("/", func(c fiber.Ctx) error {
|
||||
ctx := c.Context()
|
||||
// ctx is context implementation set by user
|
||||
|
||||
func doSomething(ctx context.Context) {
|
||||
// ...
|
||||
}
|
||||
|
||||
app.Get("/", func(c fiber.Ctx) error {
|
||||
doSomething(c)
|
||||
})
|
||||
```
|
||||
|
||||
### Value
|
||||
|
||||
Value can be used to retrieve [**`Locals`**](./#locals).
|
||||
|
||||
```go title="Example"
|
||||
app.Get("/", func(c fiber.Ctx) error {
|
||||
c.Locals(userKey, "admin")
|
||||
user := c.Value(userKey) // returns "admin"
|
||||
})
|
||||
```
|
||||
|
||||
|
@ -394,10 +394,14 @@ testConfig := fiber.TestConfig{
|
||||
### New Features
|
||||
|
||||
- Cookie now allows Partitioned cookies for [CHIPS](https://developers.google.com/privacy-sandbox/3pcd/chips) support. CHIPS (Cookies Having Independent Partitioned State) is a feature that improves privacy by allowing cookies to be partitioned by top-level site, mitigating cross-site tracking.
|
||||
- Context now implements [context.Context](https://pkg.go.dev/context#Context).
|
||||
|
||||
### New Methods
|
||||
|
||||
- **AutoFormat**: Similar to Express.js, automatically formats the response based on the request's `Accept` header.
|
||||
- **Deadline**: For implementing `context.Context`.
|
||||
- **Done**: For implementing `context.Context`.
|
||||
- **Err**: For implementing `context.Context`.
|
||||
- **Host**: Similar to Express.js, returns the host name of the request.
|
||||
- **Port**: Similar to Express.js, returns the port number of the request.
|
||||
- **IsProxyTrusted**: Checks the trustworthiness of the remote IP.
|
||||
@ -407,6 +411,7 @@ testConfig := fiber.TestConfig{
|
||||
- **SendStreamWriter**: Sends a stream using a writer function.
|
||||
- **SendString**: Similar to Express.js, sends a string as the response.
|
||||
- **String**: Similar to Express.js, converts a value to a string.
|
||||
- **Value**: For implementing `context.Context`. Returns request-scoped value from Locals.
|
||||
- **ViewBind**: Binds data to a view, replacing the old `Bind` method.
|
||||
- **CBOR**: Introducing [CBOR](https://cbor.io/) binary encoding format for both request & response body. CBOR is a binary data serialization format which is both compact and efficient, making it ideal for use in web applications.
|
||||
- **Drop**: Terminates the client connection silently without sending any HTTP headers or response body. This can be used for scenarios where you want to block certain requests without notifying the client, such as mitigating DDoS attacks or protecting sensitive endpoints from unauthorized access.
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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")
|
||||
|
Loading…
x
Reference in New Issue
Block a user