From fea24d46f793def8773ec49bc1df0304415b44d2 Mon Sep 17 00:00:00 2001 From: pjebs Date: Sun, 30 Mar 2025 15:02:16 +1100 Subject: [PATCH 1/4] Ctx implements context.Context --- ctx.go | 44 ++++++++++++++++ ctx_interface_gen.go | 56 +++++++++++++++++--- ctx_test.go | 71 -------------------------- middleware/keyauth/keyauth.go | 2 - middleware/keyauth/keyauth_test.go | 3 +- middleware/requestid/requestid.go | 4 -- middleware/requestid/requestid_test.go | 8 --- middleware/timeout/timeout.go | 5 +- middleware/timeout/timeout_test.go | 6 +-- 9 files changed, 99 insertions(+), 100 deletions(-) diff --git a/ctx.go b/ctx.go index 1816185e..ae30cf19 100644 --- a/ctx.go +++ b/ctx.go @@ -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 (c *DefaultCtx) Deadline() (deadline time.Time, ok bool) { + return +} + +// 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 (c *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 (c *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) diff --git a/ctx_interface_gen.go b/ctx_interface_gen.go index df537e26..8b517b7a 100644 --- a/ctx_interface_gen.go +++ b/ctx_interface_gen.go @@ -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. diff --git a/ctx_test.go b/ctx_test.go index 5040f4f8..7226c7ad 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -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() diff --git a/middleware/keyauth/keyauth.go b/middleware/keyauth/keyauth.go index 54ecdbe5..0736a629 100644 --- a/middleware/keyauth/keyauth.go +++ b/middleware/keyauth/keyauth.go @@ -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) diff --git a/middleware/keyauth/keyauth_test.go b/middleware/keyauth/keyauth_test.go index 27c4e5a0..f99bdaa8 100644 --- a/middleware/keyauth/keyauth_test.go +++ b/middleware/keyauth/keyauth_test.go @@ -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) }) diff --git a/middleware/requestid/requestid.go b/middleware/requestid/requestid.go index ef67e6f2..028715ae 100644 --- a/middleware/requestid/requestid.go +++ b/middleware/requestid/requestid.go @@ -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() } diff --git a/middleware/requestid/requestid_test.go b/middleware/requestid/requestid_test.go index ad36884a..e48a1bbf 100644 --- a/middleware/requestid/requestid_test.go +++ b/middleware/requestid/requestid_test.go @@ -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 { diff --git a/middleware/timeout/timeout.go b/middleware/timeout/timeout.go index 127fff87..5c7e9465 100644 --- a/middleware/timeout/timeout.go +++ b/middleware/timeout/timeout.go @@ -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) diff --git a/middleware/timeout/timeout_test.go b/middleware/timeout/timeout_test.go index 161296a7..cb58e9a9 100644 --- a/middleware/timeout/timeout_test.go +++ b/middleware/timeout/timeout_test.go @@ -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") From 9733eabf63e75aaa07f9c5aeabe0e2e553e1f401 Mon Sep 17 00:00:00 2001 From: pj Date: Sun, 30 Mar 2025 15:29:31 +1100 Subject: [PATCH 2/4] fix up some linting issues --- ctx.go | 8 ++++---- ctx_test.go | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/ctx.go b/ctx.go index ae30cf19..94e02ac3 100644 --- a/ctx.go +++ b/ctx.go @@ -450,8 +450,8 @@ func (c *DefaultCtx) Cookie(cookie *Cookie) { // // Due to current limitations in how fasthttp works, Deadline operates as a nop. // See: https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945 -func (c *DefaultCtx) Deadline() (deadline time.Time, ok bool) { - return +func (*DefaultCtx) Deadline() (deadline time.Time, ok bool) { + return time.Time{}, false } // Done returns a channel that's closed when work done on behalf of this @@ -462,7 +462,7 @@ func (c *DefaultCtx) Deadline() (deadline time.Time, ok bool) { // // Due to current limitations in how fasthttp works, Done operates as a nop. // See: https://github.com/valyala/fasthttp/issues/965#issuecomment-777268945 -func (c *DefaultCtx) Done() <-chan struct{} { +func (*DefaultCtx) Done() <-chan struct{} { return nil } @@ -498,7 +498,7 @@ func (c *DefaultCtx) Download(file string, filename ...string) 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 (c *DefaultCtx) Err() error { +func (*DefaultCtx) Err() error { return nil } diff --git a/ctx_test.go b/ctx_test.go index 7226c7ad..e10d2263 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -2187,6 +2187,23 @@ func Test_Ctx_Locals(t *testing.T) { 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() From 7376a707ef6195b98ca6abeca00eb2e9095b844d Mon Sep 17 00:00:00 2001 From: pj Date: Sun, 30 Mar 2025 16:09:06 +1100 Subject: [PATCH 3/4] added some tests --- ctx.go | 2 +- ctx_test.go | 50 +++++++++++++++++++++++++++++++++++ middleware/keyauth/keyauth.go | 8 +++--- 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/ctx.go b/ctx.go index 94e02ac3..04341820 100644 --- a/ctx.go +++ b/ctx.go @@ -451,7 +451,7 @@ func (c *DefaultCtx) Cookie(cookie *Cookie) { // 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() (deadline time.Time, ok bool) { - return time.Time{}, false + return deadline, false } // Done returns a channel that's closed when work done on behalf of this diff --git a/ctx_test.go b/ctx_test.go index e10d2263..220b8380 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -2187,6 +2187,56 @@ 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, 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, nil, 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() diff --git a/middleware/keyauth/keyauth.go b/middleware/keyauth/keyauth.go index 0736a629..e3988578 100644 --- a/middleware/keyauth/keyauth.go +++ b/middleware/keyauth/keyauth.go @@ -72,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") } From 0d2286814eff61463760a9bfccf4d6770d1f8869 Mon Sep 17 00:00:00 2001 From: pj Date: Sun, 30 Mar 2025 22:47:52 +1100 Subject: [PATCH 4/4] no message --- ctx.go | 4 ++-- ctx_test.go | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ctx.go b/ctx.go index 04341820..876ecf87 100644 --- a/ctx.go +++ b/ctx.go @@ -450,8 +450,8 @@ func (c *DefaultCtx) Cookie(cookie *Cookie) { // // 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() (deadline time.Time, ok bool) { - return deadline, false +func (*DefaultCtx) Deadline() (time.Time, bool) { + return time.Time{}, false } // Done returns a channel that's closed when work done on behalf of this diff --git a/ctx_test.go b/ctx_test.go index 220b8380..6a22b94d 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -2197,7 +2197,7 @@ func Test_Ctx_Deadline(t *testing.T) { app.Get("/test", func(c Ctx) error { deadline, ok := c.Deadline() require.Equal(t, time.Time{}, deadline) - require.Equal(t, false, ok) + require.Equal(t, require.False, ok) return nil }) resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil)) @@ -2213,7 +2213,7 @@ func Test_Ctx_Done(t *testing.T) { return c.Next() }) app.Get("/test", func(c Ctx) error { - require.Equal(t, (<-chan struct {})(nil), c.Done()) + require.Equal(t, (<-chan struct{})(nil), c.Done()) return nil }) resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil)) @@ -2229,7 +2229,7 @@ func Test_Ctx_Err(t *testing.T) { return c.Next() }) app.Get("/test", func(c Ctx) error { - require.Equal(t, nil, c.Err()) + require.Equal(t, require.NoError, c.Err()) return nil }) resp, err := app.Test(httptest.NewRequest(MethodGet, "/test", nil))