Skip to content

Commit 9bd7a1c

Browse files
pjebsgaby
andauthored
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 <[email protected]>
1 parent a5f76a7 commit 9bd7a1c

File tree

7 files changed

+164
-111
lines changed

7 files changed

+164
-111
lines changed

ctx.go

Lines changed: 46 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,14 @@ const (
3939
maxDetectionPaths = 3
4040
)
4141

42+
var (
43+
_ io.Writer = (*DefaultCtx)(nil) // Compile-time check
44+
_ context.Context = (*DefaultCtx)(nil) // Compile-time check
45+
)
46+
4247
// The contextKey type is unexported to prevent collisions with context keys defined in
4348
// other packages.
44-
type contextKey int
45-
46-
// userContextKey define the key name for storing context.Context in *fasthttp.RequestCtx
47-
const userContextKey contextKey = 0 // __local_user_context__
49+
type contextKey int //nolint:unused // need for future (nolintlint)
4850

4951
// DefaultCtx is the default implementation of the Ctx interface
5052
// generation tool `go install github.com/vburenin/ifacemaker@975a95966976eeb2d4365a7fb236e274c54da64c`
@@ -391,23 +393,6 @@ func (c *DefaultCtx) RequestCtx() *fasthttp.RequestCtx {
391393
return c.fasthttp
392394
}
393395

394-
// Context returns a context implementation that was set by
395-
// user earlier or returns a non-nil, empty context,if it was not set earlier.
396-
func (c *DefaultCtx) Context() context.Context {
397-
ctx, ok := c.fasthttp.UserValue(userContextKey).(context.Context)
398-
if !ok {
399-
ctx = context.Background()
400-
c.SetContext(ctx)
401-
}
402-
403-
return ctx
404-
}
405-
406-
// SetContext sets a context implementation by user.
407-
func (c *DefaultCtx) SetContext(ctx context.Context) {
408-
c.fasthttp.SetUserValue(userContextKey, ctx)
409-
}
410-
411396
// Cookie sets a cookie by passing a cookie struct.
412397
func (c *DefaultCtx) Cookie(cookie *Cookie) {
413398
fcookie := fasthttp.AcquireCookie()
@@ -444,6 +429,28 @@ func (c *DefaultCtx) Cookie(cookie *Cookie) {
444429
fasthttp.ReleaseCookie(fcookie)
445430
}
446431

432+
// Deadline returns the time when work done on behalf of this context
433+
// should be canceled. Deadline returns ok==false when no deadline is
434+
// set. Successive calls to Deadline return the same results.
435+
//
436+
// Due to current limitations in how fasthttp works, Deadline operates as a nop.
437+
// See: https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945
438+
func (*DefaultCtx) Deadline() (time.Time, bool) {
439+
return time.Time{}, false
440+
}
441+
442+
// Done returns a channel that's closed when work done on behalf of this
443+
// context should be canceled. Done may return nil if this context can
444+
// never be canceled. Successive calls to Done return the same value.
445+
// The close of the Done channel may happen asynchronously,
446+
// after the cancel function returns.
447+
//
448+
// Due to current limitations in how fasthttp works, Done operates as a nop.
449+
// See: https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945
450+
func (*DefaultCtx) Done() <-chan struct{} {
451+
return nil
452+
}
453+
447454
// Cookies are used for getting a cookie value by key.
448455
// Defaults to the empty string "" if the cookie doesn't exist.
449456
// 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 {
468475
return c.SendFile(file)
469476
}
470477

478+
// If Done is not yet closed, Err returns nil.
479+
// If Done is closed, Err returns a non-nil error explaining why:
480+
// context.DeadlineExceeded if the context's deadline passed,
481+
// or context.Canceled if the context was canceled for some other reason.
482+
// After Err returns a non-nil error, successive calls to Err return the same error.
483+
//
484+
// Due to current limitations in how fasthttp works, Err operates as a nop.
485+
// See: https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945
486+
func (*DefaultCtx) Err() error {
487+
return nil
488+
}
489+
471490
// Request return the *fasthttp.Request object
472491
// This allows you to use all fasthttp request methods
473492
// https://godoc.org/github.com/valyala/fasthttp#Request
@@ -1816,6 +1835,12 @@ func (c *DefaultCtx) Vary(fields ...string) {
18161835
c.Append(HeaderVary, fields...)
18171836
}
18181837

1838+
// Value makes it possible to retrieve values (Locals) under keys scoped to the request
1839+
// and therefore available to all following routes that match the request.
1840+
func (c *DefaultCtx) Value(key any) any {
1841+
return c.fasthttp.UserValue(key)
1842+
}
1843+
18191844
// Write appends p into response body.
18201845
func (c *DefaultCtx) Write(p []byte) (int, error) {
18211846
c.fasthttp.Response.AppendBody(p)

ctx_interface_gen.go

Lines changed: 20 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ctx_test.go

Lines changed: 67 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import (
99
"bytes"
1010
"compress/gzip"
1111
"compress/zlib"
12-
"context"
1312
"crypto/tls"
1413
"embed"
1514
"encoding/hex"
@@ -881,76 +880,6 @@ func Test_Ctx_RequestCtx(t *testing.T) {
881880
require.Equal(t, "*fasthttp.RequestCtx", fmt.Sprintf("%T", c.RequestCtx()))
882881
}
883882

884-
// go test -run Test_Ctx_Context
885-
func Test_Ctx_Context(t *testing.T) {
886-
t.Parallel()
887-
app := New()
888-
c := app.AcquireCtx(&fasthttp.RequestCtx{})
889-
890-
t.Run("Nil_Context", func(t *testing.T) {
891-
t.Parallel()
892-
ctx := c.Context()
893-
require.Equal(t, ctx, context.Background())
894-
})
895-
t.Run("ValueContext", func(t *testing.T) {
896-
t.Parallel()
897-
testKey := struct{}{}
898-
testValue := "Test Value"
899-
ctx := context.WithValue(context.Background(), testKey, testValue) //nolint:staticcheck // not needed for tests
900-
require.Equal(t, testValue, ctx.Value(testKey))
901-
})
902-
}
903-
904-
// go test -run Test_Ctx_SetContext
905-
func Test_Ctx_SetContext(t *testing.T) {
906-
t.Parallel()
907-
app := New()
908-
c := app.AcquireCtx(&fasthttp.RequestCtx{})
909-
910-
testKey := struct{}{}
911-
testValue := "Test Value"
912-
ctx := context.WithValue(context.Background(), testKey, testValue) //nolint:staticcheck // not needed for tests
913-
c.SetContext(ctx)
914-
require.Equal(t, testValue, c.Context().Value(testKey))
915-
}
916-
917-
// go test -run Test_Ctx_Context_Multiple_Requests
918-
func Test_Ctx_Context_Multiple_Requests(t *testing.T) {
919-
t.Parallel()
920-
testKey := struct{}{}
921-
testValue := "foobar-value"
922-
923-
app := New()
924-
app.Get("/", func(c Ctx) error {
925-
ctx := c.Context()
926-
927-
if ctx.Value(testKey) != nil {
928-
return c.SendStatus(StatusInternalServerError)
929-
}
930-
931-
input := utils.CopyString(Query(c, "input", "NO_VALUE"))
932-
ctx = context.WithValue(ctx, testKey, fmt.Sprintf("%s_%s", testValue, input)) //nolint:staticcheck // not needed for tests
933-
c.SetContext(ctx)
934-
935-
return c.Status(StatusOK).SendString(fmt.Sprintf("resp_%s_returned", input))
936-
})
937-
938-
// Consecutive Requests
939-
for i := 1; i <= 10; i++ {
940-
t.Run(fmt.Sprintf("request_%d", i), func(t *testing.T) {
941-
t.Parallel()
942-
resp, err := app.Test(httptest.NewRequest(MethodGet, fmt.Sprintf("/?input=%d", i), nil))
943-
944-
require.NoError(t, err, "Unexpected error from response")
945-
require.Equal(t, StatusOK, resp.StatusCode, "context.Context returned from c.Context() is reused")
946-
947-
b, err := io.ReadAll(resp.Body)
948-
require.NoError(t, err, "Unexpected error from reading response body")
949-
require.Equal(t, fmt.Sprintf("resp_%d_returned", i), string(b), "response text incorrect")
950-
})
951-
}
952-
}
953-
954883
// go test -run Test_Ctx_Cookie
955884
func Test_Ctx_Cookie(t *testing.T) {
956885
t.Parallel()
@@ -2257,6 +2186,73 @@ func Test_Ctx_Locals(t *testing.T) {
22572186
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
22582187
}
22592188

2189+
// go test -run Test_Ctx_Deadline
2190+
func Test_Ctx_Deadline(t *testing.T) {
2191+
t.Parallel()
2192+
app := New()
2193+
app.Use(func(c Ctx) error {
2194+
return c.Next()
2195+
})
2196+
app.Get("/test", func(c Ctx) error {
2197+
deadline, ok := c.Deadline()
2198+
require.Equal(t, time.Time{}, deadline)
2199+
require.False(t, ok)
2200+
return nil
2201+
})
2202+
resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil))
2203+
require.NoError(t, err, "app.Test(req)")
2204+
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
2205+
}
2206+
2207+
// go test -run Test_Ctx_Done
2208+
func Test_Ctx_Done(t *testing.T) {
2209+
t.Parallel()
2210+
app := New()
2211+
app.Use(func(c Ctx) error {
2212+
return c.Next()
2213+
})
2214+
app.Get("/test", func(c Ctx) error {
2215+
require.Equal(t, (<-chan struct{})(nil), c.Done())
2216+
return nil
2217+
})
2218+
resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil))
2219+
require.NoError(t, err, "app.Test(req)")
2220+
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
2221+
}
2222+
2223+
// go test -run Test_Ctx_Err
2224+
func Test_Ctx_Err(t *testing.T) {
2225+
t.Parallel()
2226+
app := New()
2227+
app.Use(func(c Ctx) error {
2228+
return c.Next()
2229+
})
2230+
app.Get("/test", func(c Ctx) error {
2231+
require.NoError(t, c.Err())
2232+
return nil
2233+
})
2234+
resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil))
2235+
require.NoError(t, err, "app.Test(req)")
2236+
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
2237+
}
2238+
2239+
// go test -run Test_Ctx_Value
2240+
func Test_Ctx_Value(t *testing.T) {
2241+
t.Parallel()
2242+
app := New()
2243+
app.Use(func(c Ctx) error {
2244+
c.Locals("john", "doe")
2245+
return c.Next()
2246+
})
2247+
app.Get("/test", func(c Ctx) error {
2248+
require.Equal(t, "doe", c.Value("john"))
2249+
return nil
2250+
})
2251+
resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil))
2252+
require.NoError(t, err, "app.Test(req)")
2253+
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
2254+
}
2255+
22602256
// go test -run Test_Ctx_Locals_Generic
22612257
func Test_Ctx_Locals_Generic(t *testing.T) {
22622258
t.Parallel()

docs/api/ctx.md

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,36 @@ app.Post("/", func(c fiber.Ctx) error {
4141
})
4242
```
4343

44-
### Context
44+
## Context
4545

46-
`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.
46+
`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.
4747

4848
```go title="Signature"
49-
func (c fiber.Ctx) Context() context.Context
49+
func (c fiber.Ctx) Deadline() (deadline time.Time, ok bool)
50+
func (c fiber.Ctx) Done() <-chan struct{}
51+
func (c fiber.Ctx) Err() error
52+
func (c fiber.Ctx) Value(key any) any
5053
```
5154

5255
```go title="Example"
53-
app.Get("/", func(c fiber.Ctx) error {
54-
ctx := c.Context()
55-
// ctx is context implementation set by user
5656

57+
func doSomething(ctx context.Context) {
5758
// ...
59+
}
60+
61+
app.Get("/", func(c fiber.Ctx) error {
62+
doSomething(c)
63+
})
64+
```
65+
66+
### Value
67+
68+
Value can be used to retrieve [**`Locals`**](./#locals).
69+
70+
```go title="Example"
71+
app.Get("/", func(c fiber.Ctx) error {
72+
c.Locals(userKey, "admin")
73+
user := c.Value(userKey) // returns "admin"
5874
})
5975
```
6076

docs/whats_new.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,10 +394,14 @@ testConfig := fiber.TestConfig{
394394
### New Features
395395

396396
- 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.
397+
- Context now implements [context.Context](https://pkg.go.dev/context#Context).
397398

398399
### New Methods
399400

400401
- **AutoFormat**: Similar to Express.js, automatically formats the response based on the request's `Accept` header.
402+
- **Deadline**: For implementing `context.Context`.
403+
- **Done**: For implementing `context.Context`.
404+
- **Err**: For implementing `context.Context`.
401405
- **Host**: Similar to Express.js, returns the host name of the request.
402406
- **Port**: Similar to Express.js, returns the port number of the request.
403407
- **IsProxyTrusted**: Checks the trustworthiness of the remote IP.
@@ -407,6 +411,7 @@ testConfig := fiber.TestConfig{
407411
- **SendStreamWriter**: Sends a stream using a writer function.
408412
- **SendString**: Similar to Express.js, sends a string as the response.
409413
- **String**: Similar to Express.js, converts a value to a string.
414+
- **Value**: For implementing `context.Context`. Returns request-scoped value from Locals.
410415
- **ViewBind**: Binds data to a view, replacing the old `Bind` method.
411416
- **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.
412417
- **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.

0 commit comments

Comments
 (0)