Skip to content
Open
20 changes: 19 additions & 1 deletion ctx.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ func (c *DefaultCtx) 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.
func (c *DefaultCtx) Context() context.Context {
if c.fasthttp == nil {
return context.Background()
}
if ctx, ok := c.fasthttp.UserValue(userContextKey).(context.Context); ok && ctx != nil {
return ctx
}
Expand All @@ -131,6 +134,9 @@ func (c *DefaultCtx) Context() context.Context {

// SetContext sets a context implementation by user.
func (c *DefaultCtx) SetContext(ctx context.Context) {
if c.fasthttp == nil {
return
}
c.fasthttp.SetUserValue(userContextKey, ctx)
}

Expand Down Expand Up @@ -167,14 +173,22 @@ func (*DefaultCtx) 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
// Returns nil if the context has been released.
func (c *DefaultCtx) Request() *fasthttp.Request {
if c.fasthttp == nil {
return nil
}
return &c.fasthttp.Request
}

// Response return the *fasthttp.Response object
// This allows you to use all fasthttp response methods
// https://godoc.org/github.com/valyala/fasthttp#Response
// Returns nil if the context has been released.
func (c *DefaultCtx) Response() *fasthttp.Response {
if c.fasthttp == nil {
return nil
}
return &c.fasthttp.Response
}

Expand Down Expand Up @@ -590,8 +604,12 @@ func (c *DefaultCtx) String() 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.
// and therefore available to all following routes that match the request. If the context
// has been released and c.fasthttp is nil (for example, after ReleaseCtx), Value returns nil.
func (c *DefaultCtx) Value(key any) any {
if c.fasthttp == nil {
return nil
}
return c.fasthttp.UserValue(key)
}

Expand Down
110 changes: 108 additions & 2 deletions ctx_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3226,6 +3226,85 @@ func Test_Ctx_Value(t *testing.T) {
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
}

// go test -run Test_Ctx_Value_AfterRelease
func Test_Ctx_Value_AfterRelease(t *testing.T) {
t.Parallel()
app := New()
var ctx Ctx
app.Get("/test", func(c Ctx) error {
ctx = c
c.Locals("test", "value")
return nil
})
resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", http.NoBody))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusOK, resp.StatusCode, "Status code")

// After the handler completes, the context is released and fasthttp is nil
// Value should return nil instead of panicking
require.NotPanics(t, func() {
val := ctx.Value("test")
require.Nil(t, val)
})
}

// go test -run Test_Ctx_Value_InGoroutine
func Test_Ctx_Value_InGoroutine(t *testing.T) {
t.Parallel()
app := New()
done := make(chan bool, 1) // Buffered to prevent goroutine leak
errCh := make(chan error, 1) // Channel to communicate errors from goroutine

// Use a synchronization point to avoid race detector complaints
// while still testing the defensive nil behavior
start := make(chan struct{})

app.Get("/test", func(c Ctx) error {
c.Locals("test", "value")

// Simulate a goroutine that uses the context (like minio.GetObject)
go func() {
// Wait for handler to complete and context to be released
<-start

defer func() {
if r := recover(); r != nil {
errCh <- fmt.Errorf("panic in goroutine: %v", r)
return
}
done <- true
}()

// This simulates what happens when minio or other libraries
// use the fiber.Ctx as a context.Context in a goroutine
// The Value method should not panic even if fasthttp is nil
val := c.Value("test")
// The value might be nil if the context was released
_ = val
}()

return nil
})

resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", http.NoBody))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusOK, resp.StatusCode, "Status code")

// Signal goroutine to proceed - context has been released after app.Test returns
// since the handler (and its deferred ReleaseCtx) has completed
close(start)

// Wait for goroutine to complete with timeout
select {
case <-done:
// Success - goroutine completed without panic
case err := <-errCh:
t.Fatalf("error from goroutine: %v", err)
case <-time.After(1 * time.Second):
t.Fatal("test timed out waiting for goroutine")
}
}

// go test -run Test_Ctx_Context
func Test_Ctx_Context(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -3274,8 +3353,35 @@ func Test_Ctx_Context_AfterHandlerPanics(t *testing.T) {
resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", http.NoBody))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusOK, resp.StatusCode, "Status code")
require.Panics(t, func() {
_ = ctx.Context()
// After the fix, Context() returns context.Background() instead of panicking
require.NotPanics(t, func() {
c := ctx.Context()
require.NotNil(t, c)
require.Equal(t, context.Background(), c)
})
}

// go test -run Test_Ctx_Request_Response_AfterRelease
func Test_Ctx_Request_Response_AfterRelease(t *testing.T) {
t.Parallel()
app := New()
var ctx Ctx
app.Get("/test", func(c Ctx) error {
ctx = c
return nil
})
resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", http.NoBody))
require.NoError(t, err, "app.Test(req)")
require.Equal(t, StatusOK, resp.StatusCode, "Status code")

// After the handler completes and context is released,
// Request() and Response() should return nil instead of panicking
require.NotPanics(t, func() {
req := ctx.Request()
require.Nil(t, req)

res := ctx.Response()
require.Nil(t, res)
})
}

Expand Down
44 changes: 44 additions & 0 deletions docs/api/ctx.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,10 @@ Returns the [*fasthttp.Request](https://pkg.go.dev/github.com/valyala/fasthttp#R
func (c fiber.Ctx) Request() *fasthttp.Request
```

:::info
Returns `nil` if the context has been released (e.g., after the handler completes and the context is returned to the pool).
:::

```go title="Example"
app.Get("/", func(c fiber.Ctx) error {
c.Request().Header.Method()
Expand Down Expand Up @@ -519,6 +523,10 @@ Returns the [\*fasthttp.Response](https://pkg.go.dev/github.com/valyala/fasthttp
func (c fiber.Ctx) Response() *fasthttp.Response
```

:::info
Returns `nil` if the context has been released (e.g., after the handler completes and the context is returned to the pool).
:::

```go title="Example"
app.Get("/", func(c fiber.Ctx) error {
c.Response().BodyWriter().Write([]byte("Hello, World!"))
Expand Down Expand Up @@ -2704,13 +2712,49 @@ Sets the response body to a stream of data and adds an optional body size.
func (c fiber.Ctx) SendStream(stream io.Reader, size ...int) error
```

:::info
`SendStream` operates asynchronously. The handler returns immediately after setting up the stream,
but the actual reading and sending of data happens **after** the handler completes. This is handled
by the underlying `fasthttp` library.

If the provided stream implements `io.Closer`, it will be automatically closed by `fasthttp` after
the response is fully sent or if an error occurs.
:::

:::caution
When passing `fiber.Ctx` as a `context.Context` to libraries that spawn goroutines (e.g., for streaming operations),
those goroutines may attempt to access the context after the handler returns. Since `fiber.Ctx` is recycled and
released after the handler completes, this can cause issues.

**Recommended approach**: Use `c.Context()` or `c.RequestCtx()` instead of passing `c` directly to such libraries.
See the [Context Guide](../guide/context.md) for more details.
:::

```go title="Example"
app.Get("/", func(c fiber.Ctx) error {
return c.SendStream(bytes.NewReader([]byte("Hello, World!")))
// => "Hello, World!"
})
```

```go title="Example with file streaming"
app.Get("/download", func(c fiber.Ctx) error {
file, err := os.Open("large-file.zip")
if err != nil {
return err
}
// File will be automatically closed by fasthttp after streaming completes

stat, err := file.Stat()
if err != nil {
file.Close()
return err
}

return c.SendStream(file, int(stat.Size()))
})
```

### SendStreamWriter

Sets the response body stream writer.
Expand Down
Loading