Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,9 +151,15 @@ func (c *DefaultCtx) SetContext(ctx context.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.
// If no context was set with SetContext, Deadline operates as a nop due to
// current limitations in how fasthttp works.
// See: https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945
func (*DefaultCtx) Deadline() (time.Time, bool) {
func (c *DefaultCtx) Deadline() (time.Time, bool) {
if c.fasthttp != nil {
if ctx, ok := c.fasthttp.UserValue(userContextKey).(context.Context); ok && ctx != nil {
return ctx.Deadline()
}
}
return time.Time{}, false
}
Comment on lines +164 to 171
Comment on lines +164 to 171

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Checking c.isUserContextSet before performing the UserValue lookup and type assertion is a great optimization. Since isUserContextSet is only set to true when a custom context is actually configured, we can completely bypass the overhead of the map/slice lookup and type assertion for the vast majority of requests that do not use a custom context.

Suggested change
func (c *DefaultCtx) Deadline() (time.Time, bool) {
if c.fasthttp != nil {
if ctx, ok := c.fasthttp.UserValue(userContextKey).(context.Context); ok && ctx != nil {
return ctx.Deadline()
}
}
return time.Time{}, false
}
func (c *DefaultCtx) Deadline() (time.Time, bool) {
if c.isUserContextSet && c.fasthttp != nil {
if ctx, ok := c.fasthttp.UserValue(userContextKey).(context.Context); ok && ctx != nil {
return ctx.Deadline()
}
}
return time.Time{}, false
}


Expand All @@ -163,17 +169,29 @@ func (*DefaultCtx) Deadline() (time.Time, bool) {
// 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.
// If no context was set with SetContext, Done operates as a nop due to
// current limitations in how fasthttp works.
// See: https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945
func (*DefaultCtx) Done() <-chan struct{} {
func (c *DefaultCtx) Done() <-chan struct{} {
if c.fasthttp != nil {
if ctx, ok := c.fasthttp.UserValue(userContextKey).(context.Context); ok && ctx != nil {
return ctx.Done()
}
}
return nil
}
Comment on lines +182 to 189
Comment on lines +182 to 189

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Checking c.isUserContextSet before performing the UserValue lookup and type assertion is a great optimization. Since isUserContextSet is only set to true when a custom context is actually configured, we can completely bypass the overhead of the map/slice lookup and type assertion for the vast majority of requests that do not use a custom context.

Suggested change
func (c *DefaultCtx) Done() <-chan struct{} {
if c.fasthttp != nil {
if ctx, ok := c.fasthttp.UserValue(userContextKey).(context.Context); ok && ctx != nil {
return ctx.Done()
}
}
return nil
}
func (c *DefaultCtx) Done() <-chan struct{} {
if c.isUserContextSet && c.fasthttp != nil {
if ctx, ok := c.fasthttp.UserValue(userContextKey).(context.Context); ok && ctx != nil {
return ctx.Done()
}
}
return nil
}


// Err mirrors context.Err, returning nil until cancellation and then the terminal error value.
//
// Due to current limitations in how fasthttp works, Err operates as a nop.
// If no context was set with SetContext, Err operates as a nop due to
// current limitations in how fasthttp works.
// See: https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945
func (*DefaultCtx) Err() error {
func (c *DefaultCtx) Err() error {
if c.fasthttp != nil {
if ctx, ok := c.fasthttp.UserValue(userContextKey).(context.Context); ok && ctx != nil {
return ctx.Err() //nolint:wrapcheck // Err mirrors the stored context.
}
}
return nil
}
Comment on lines +196 to 203
Comment on lines +196 to 203

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Checking c.isUserContextSet before performing the UserValue lookup and type assertion is a great optimization. Since isUserContextSet is only set to true when a custom context is actually configured, we can completely bypass the overhead of the map/slice lookup and type assertion for the vast majority of requests that do not use a custom context.

Suggested change
func (c *DefaultCtx) Err() error {
if c.fasthttp != nil {
if ctx, ok := c.fasthttp.UserValue(userContextKey).(context.Context); ok && ctx != nil {
return ctx.Err() //nolint:wrapcheck // Err mirrors the stored context.
}
}
return nil
}
func (c *DefaultCtx) Err() error {
if c.isUserContextSet && c.fasthttp != nil {
if ctx, ok := c.fasthttp.UserValue(userContextKey).(context.Context); ok && ctx != nil {
return ctx.Err() //nolint:wrapcheck // Err mirrors the stored context.
}
}
return nil
}


Expand Down
9 changes: 6 additions & 3 deletions ctx_interface_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3739,6 +3739,27 @@ func Test_Ctx_Err(t *testing.T) {
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
}

// go test -run Test_Ctx_ContextMethods_UseCustomContext
func Test_Ctx_ContextMethods_UseCustomContext(t *testing.T) {
t.Parallel()
app := New()
deadline := time.Now().Add(time.Hour)
parent, cancel := context.WithDeadline(context.Background(), deadline)
cancel()
c := app.AcquireCtx(&fasthttp.RequestCtx{})
t.Cleanup(func() {
app.ReleaseCtx(c)
})

c.SetContext(parent)

actualDeadline, ok := c.Deadline()
require.True(t, ok)
require.Equal(t, deadline, actualDeadline)
require.Equal(t, parent.Done(), c.Done())
require.ErrorIs(t, c.Err(), context.Canceled)
}

// go test -run Test_Ctx_Value
func Test_Ctx_Value(t *testing.T) {
t.Parallel()
Expand Down
2 changes: 1 addition & 1 deletion docs/api/ctx.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ app.Get("/", func(c fiber.Ctx) error {

### context.Context

`Ctx` 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()` are no-ops. The `fiber.Ctx` instance is reused after the handler returns and must not be used for asynchronous operations once the handler has completed. Call [`Context`](#context) within the handler to obtain a `context.Context` that can be used outside the handler.
`Ctx` implements `context.Context`. `Deadline()`, `Done()` and `Err()` delegate to the context set with [`SetContext`](#setcontext). If no context was set, these methods are no-ops because of [current limitations in how fasthttp](https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945) works. The `fiber.Ctx` instance is reused after the handler returns and must not be used for asynchronous operations once the handler has completed. Call [`Context`](#context) within the handler to obtain a `context.Context` that can be used outside the handler.

```go title="Signature"
func (c fiber.Ctx) Deadline() (deadline time.Time, ok bool)
Expand Down
8 changes: 5 additions & 3 deletions docs/guide/context.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ Fiber's [`Ctx`](../api/ctx.md) implements Go's
You can pass `c` directly to functions that expect a `context.Context`
without adapters.
However, `fasthttp` doesn't support cancellation yet, so
`Deadline`, `Done`, and `Err` are no-ops.
`Deadline`, `Done`, and `Err` are no-ops unless you set a custom context with
`c.SetContext`. When a custom context is set, those methods delegate to it.

:::caution
The `fiber.Ctx` instance is only valid within the lifetime of the handler.
Expand Down Expand Up @@ -262,8 +263,9 @@ This approach provides safe cancellation semantics for goroutine-based work whil

## Summary

- `fiber.Ctx` satisfies `context.Context` but its `Deadline`, `Done`, and `Err`
methods are currently no-ops.
- `fiber.Ctx` satisfies `context.Context`. Its `Deadline`, `Done`, and `Err`
methods delegate to `c.SetContext` when a custom context is set; otherwise
they remain no-ops.
- `RequestCtx` exposes the raw `fasthttp` context. Its `Done` channel
closes only on server shutdown, not on client disconnect.
- Use `fiber.StoreInContext(c, key, value)` to store request-scoped values in both
Expand Down
6 changes: 3 additions & 3 deletions log/default.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ func (l *defaultLogger) privateLog(lv Level, fmtArgs []any) {
buf.Reset()
bytebufferpool.Put(buf)
if lv == LevelFatal {
os.Exit(1) //nolint:revive // we want to exit the program when Fatal is called
os.Exit(1) //revive:disable-line:deep-exit // Fatal logs intentionally terminate the process.
}
}

Expand Down Expand Up @@ -101,7 +101,7 @@ func (l *defaultLogger) privateLogf(lv Level, format string, fmtArgs []any) {
buf.Reset()
bytebufferpool.Put(buf)
if lv == LevelFatal {
os.Exit(1) //nolint:revive // we want to exit the program when Fatal is called
os.Exit(1) //revive:disable-line:deep-exit // Fatal logs intentionally terminate the process.
}
}

Expand Down Expand Up @@ -148,7 +148,7 @@ func (l *defaultLogger) privateLogw(lv Level, format string, keysAndValues []any
buf.Reset()
bytebufferpool.Put(buf)
if lv == LevelFatal {
os.Exit(1) //nolint:revive // we want to exit the program when Fatal is called
os.Exit(1) //revive:disable-line:deep-exit // Fatal logs intentionally terminate the process.
}
}

Expand Down
Loading