diff --git a/app.go b/app.go
index 1c2091783bf..75d83fd2dcb 100644
--- a/app.go
+++ b/app.go
@@ -532,7 +532,10 @@ func New(config ...Config) *App {
// Create Ctx pool
app.pool = sync.Pool{
New: func() any {
- return app.newCtx()
+ if app.newCtxFunc != nil {
+ return app.newCtxFunc(app)
+ }
+ return NewDefaultCtx(app)
},
}
@@ -623,6 +626,15 @@ func New(config ...Config) *App {
return app
}
+// NewWithCustomCtx creates a new Fiber instance and applies the
+// provided function to generate a custom context type. It mirrors the behavior
+// of calling `New()` followed by `app.setCtxFunc(fn)`.
+func NewWithCustomCtx(newCtxFunc func(app *App) CustomCtx, config ...Config) *App {
+ app := New(config...)
+ app.setCtxFunc(newCtxFunc)
+ return app
+}
+
// Adds an ip address to TrustProxyConfig.ranges or TrustProxyConfig.ips based on whether it is an IP range or not
func (app *App) handleTrustedProxy(ipAddress string) {
if strings.Contains(ipAddress, "/") {
@@ -642,13 +654,14 @@ func (app *App) handleTrustedProxy(ipAddress string) {
}
}
-// NewCtxFunc allows to customize ctx methods as we want.
-// Note: It doesn't allow adding new methods, only customizing exist methods.
-func (app *App) NewCtxFunc(function func(app *App) CustomCtx) {
+// setCtxFunc applies the given context factory to the app.
+// It is used internally by NewWithCustomCtx. It doesn't allow adding new methods,
+// only customizing existing ones.
+func (app *App) setCtxFunc(function func(app *App) CustomCtx) {
app.newCtxFunc = function
if app.server != nil {
- app.server.Handler = app.customRequestHandler
+ app.server.Handler = app.requestHandler
}
}
@@ -935,11 +948,7 @@ func (app *App) Config() Config {
func (app *App) Handler() fasthttp.RequestHandler { //revive:disable-line:confusing-naming // Having both a Handler() (uppercase) and a handler() (lowercase) is fine. TODO: Use nolint:revive directive instead. See https://github.com/golangci/golangci-lint/issues/3476
// prepare the server for the start
app.startupProcess()
-
- if app.newCtxFunc != nil {
- return app.customRequestHandler
- }
- return app.defaultRequestHandler
+ return app.requestHandler
}
// Stack returns the raw router stack.
@@ -1150,11 +1159,7 @@ func (app *App) init() *App {
}
// fasthttp server settings
- if app.newCtxFunc != nil {
- app.server.Handler = app.customRequestHandler
- } else {
- app.server.Handler = app.defaultRequestHandler
- }
+ app.server.Handler = app.requestHandler
app.server.Name = app.config.ServerHeader
app.server.Concurrency = app.config.Concurrency
app.server.NoDefaultDate = app.config.DisableDefaultDate
diff --git a/binder/binder_test.go b/binder/binder_test.go
index d078ed02c6b..704ab2373fd 100644
--- a/binder/binder_test.go
+++ b/binder/binder_test.go
@@ -1,9 +1,13 @@
package binder
import (
+ "mime/multipart"
+ "reflect"
+ "strconv"
"testing"
"github.com/stretchr/testify/require"
+ "github.com/valyala/fasthttp"
)
func Test_GetAndPutToThePool(t *testing.T) {
@@ -26,3 +30,108 @@ func Test_GetAndPutToThePool(t *testing.T) {
_ = GetFromThePool[*JSONBinding](&JSONBinderPool)
_ = GetFromThePool[*CBORBinding](&CBORBinderPool)
}
+
+func Test_Binders_ErrorPaths(t *testing.T) {
+ t.Run("query binder invalid key", func(t *testing.T) {
+ b := &QueryBinding{}
+ req := fasthttp.AcquireRequest()
+ req.URI().SetQueryString("invalid[%3Dval&name=john")
+ defer fasthttp.ReleaseRequest(req)
+ err := b.Bind(req, &struct{}{})
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "unmatched brackets")
+ })
+
+ t.Run("form binder invalid key", func(t *testing.T) {
+ b := &FormBinding{}
+ req := fasthttp.AcquireRequest()
+ req.SetBodyString("invalid[=val")
+ req.Header.SetContentType("application/x-www-form-urlencoded")
+ defer fasthttp.ReleaseRequest(req)
+ err := b.Bind(req, &struct{}{})
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "unmatched brackets")
+ })
+
+ t.Run("form binder bad multipart", func(t *testing.T) {
+ b := &FormBinding{}
+ req := fasthttp.AcquireRequest()
+ req.Header.SetContentType(MIMEMultipartForm)
+ defer fasthttp.ReleaseRequest(req)
+ err := b.Bind(req, &struct{}{})
+ require.Error(t, err)
+ })
+}
+
+func Test_GetFieldCache_Panic(t *testing.T) {
+ t.Parallel()
+ require.Panics(t, func() { getFieldCache("unknown") })
+}
+
+func Test_parseToMap_defaultCase(t *testing.T) {
+ t.Parallel()
+ m := map[string]int{}
+ err := parseToMap(m, map[string][]string{"a": {"1"}})
+ require.NoError(t, err)
+ require.Empty(t, m)
+
+ m2 := map[string]string{}
+ err = parseToMap(m2, map[string][]string{"empty": {}})
+ require.NoError(t, err)
+ require.Equal(t, "", m2["empty"])
+}
+
+func Test_parse_function_maps(t *testing.T) {
+ t.Parallel()
+
+ m := map[string][]string{}
+ err := parse("query", &m, map[string][]string{"a": {"b"}})
+ require.NoError(t, err)
+ require.Equal(t, []string{"b"}, m["a"])
+
+ m2 := map[string]string{}
+ err = parse("query", &m2, map[string][]string{"a": {"b"}})
+ require.NoError(t, err)
+ require.Equal(t, "b", m2["a"])
+}
+
+func Test_SetParserDecoder_UnknownKeys(t *testing.T) {
+ SetParserDecoder(ParserConfig{IgnoreUnknownKeys: false})
+ defer SetParserDecoder(ParserConfig{IgnoreUnknownKeys: true, ZeroEmpty: true})
+ type user struct {
+ Name string `query:"name"`
+ }
+ data := map[string][]string{"name": {"john"}, "foo": {"bar"}}
+ err := parseToStruct("query", &user{}, data)
+ require.Error(t, err)
+ SetParserDecoder(ParserConfig{IgnoreUnknownKeys: true, ZeroEmpty: true})
+}
+
+func Test_SetParserDecoder_CustomConverter(t *testing.T) {
+ type myInt int
+ conv := func(s string) reflect.Value {
+ v, _ := strconv.Atoi(s) //nolint:errcheck // not needed
+ mi := myInt(v)
+ return reflect.ValueOf(mi)
+ }
+
+ SetParserDecoder(ParserConfig{ParserType: []ParserType{{CustomType: myInt(0), Converter: conv}}})
+ defer SetParserDecoder(ParserConfig{IgnoreUnknownKeys: true, ZeroEmpty: true})
+
+ type data struct {
+ V myInt `query:"v"`
+ }
+ d := new(data)
+ err := parse("query", d, map[string][]string{"v": {"5"}})
+ require.NoError(t, err)
+ require.Equal(t, myInt(5), d.V)
+}
+
+func Test_formatBindData_typeMismatch(t *testing.T) {
+ t.Parallel()
+ out := struct{}{}
+ files := map[string][]*multipart.FileHeader{}
+ err := formatBindData("query", out, files, "file", 123, false, false)
+ require.Error(t, err)
+ require.Equal(t, "unsupported value type: int", err.Error())
+}
diff --git a/ctx.go b/ctx.go
index 26060be4d64..6d315708111 100644
--- a/ctx.go
+++ b/ctx.go
@@ -23,6 +23,8 @@ import (
"text/template"
"time"
+ "golang.org/x/net/idna"
+
"github.com/gofiber/utils/v2"
"github.com/valyala/bytebufferpool"
"github.com/valyala/fasthttp"
@@ -395,36 +397,80 @@ func (c *DefaultCtx) RequestCtx() *fasthttp.RequestCtx {
// Cookie sets a cookie by passing a cookie struct.
func (c *DefaultCtx) Cookie(cookie *Cookie) {
- fcookie := fasthttp.AcquireCookie()
- fcookie.SetKey(cookie.Name)
- fcookie.SetValue(cookie.Value)
- fcookie.SetPath(cookie.Path)
- fcookie.SetDomain(cookie.Domain)
- // only set max age and expiry when SessionOnly is false
- // i.e. cookie supposed to last beyond browser session
- // refer: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#define_the_lifetime_of_a_cookie
- if !cookie.SessionOnly {
- fcookie.SetMaxAge(cookie.MaxAge)
- fcookie.SetExpire(cookie.Expires)
+ if cookie.Path == "" {
+ cookie.Path = "/"
}
- fcookie.SetSecure(cookie.Secure)
- fcookie.SetHTTPOnly(cookie.HTTPOnly)
+
+ if cookie.SessionOnly {
+ cookie.MaxAge = 0
+ cookie.Expires = time.Time{}
+ }
+
+ var sameSite http.SameSite
switch utils.ToLower(cookie.SameSite) {
case CookieSameSiteStrictMode:
- fcookie.SetSameSite(fasthttp.CookieSameSiteStrictMode)
+ sameSite = http.SameSiteStrictMode
case CookieSameSiteNoneMode:
- fcookie.SetSameSite(fasthttp.CookieSameSiteNoneMode)
+ sameSite = http.SameSiteNoneMode
case CookieSameSiteDisabled:
- fcookie.SetSameSite(fasthttp.CookieSameSiteDisabled)
+ sameSite = 0
+ case CookieSameSiteLaxMode:
+ sameSite = http.SameSiteLaxMode
default:
+ sameSite = http.SameSiteLaxMode
+ }
+
+ // create/validate cookie using net/http
+ hc := &http.Cookie{
+ Name: cookie.Name,
+ Value: cookie.Value,
+ Path: cookie.Path,
+ Domain: cookie.Domain,
+ Expires: cookie.Expires,
+ MaxAge: cookie.MaxAge,
+ Secure: cookie.Secure,
+ HttpOnly: cookie.HTTPOnly,
+ SameSite: sameSite,
+ Partitioned: cookie.Partitioned,
+ }
+
+ if err := hc.Valid(); err != nil {
+ // invalid cookies are ignored, same approach as net/http
+ return
+ }
+
+ // create fasthttp cookie
+ fcookie := fasthttp.AcquireCookie()
+ fcookie.SetKey(hc.Name)
+ fcookie.SetValue(hc.Value)
+ fcookie.SetPath(hc.Path)
+ fcookie.SetDomain(hc.Domain)
+
+ if !cookie.SessionOnly {
+ fcookie.SetMaxAge(hc.MaxAge)
+ fcookie.SetExpire(hc.Expires)
+ }
+
+ fcookie.SetSecure(hc.Secure)
+ fcookie.SetHTTPOnly(hc.HttpOnly)
+
+ switch sameSite {
+ case http.SameSiteLaxMode:
fcookie.SetSameSite(fasthttp.CookieSameSiteLaxMode)
+ case http.SameSiteStrictMode:
+ fcookie.SetSameSite(fasthttp.CookieSameSiteStrictMode)
+ case http.SameSiteNoneMode:
+ fcookie.SetSameSite(fasthttp.CookieSameSiteNoneMode)
+ case http.SameSiteDefaultMode:
+ fcookie.SetSameSite(fasthttp.CookieSameSiteDefaultMode)
+ default:
+ fcookie.SetSameSite(fasthttp.CookieSameSiteDisabled)
}
- // CHIPS allows to partition cookie jar by top-level site.
- // refer: https://developers.google.com/privacy-sandbox/3pcd/chips
- fcookie.SetPartitioned(cookie.Partitioned)
+ fcookie.SetPartitioned(hc.Partitioned)
+ // Set resp header
c.fasthttp.Response.Header.SetCookie(fcookie)
fasthttp.ReleaseCookie(fcookie)
}
@@ -619,7 +665,7 @@ func (c *DefaultCtx) Fresh() bool {
// Always return stale when Cache-Control: no-cache
// to support end-to-end reload requests
- // https://tools.ietf.org/html/rfc2616#section-14.9.4
+ // https://www.rfc-editor.org/rfc/rfc9111#section-5.2.1.4
cacheControl := c.Get(HeaderCacheControl)
if cacheControl != "" && isNoCache(cacheControl) {
return false
@@ -883,10 +929,12 @@ func (c *DefaultCtx) Is(extension string) bool {
return false
}
- return strings.HasPrefix(
- utils.TrimLeft(utils.UnsafeString(c.fasthttp.Request.Header.ContentType()), ' '),
- extensionHeader,
- )
+ ct := c.app.getString(c.fasthttp.Request.Header.ContentType())
+ if i := strings.IndexByte(ct, ';'); i != -1 {
+ ct = ct[:i]
+ }
+ ct = utils.Trim(ct, ' ')
+ return utils.EqualFold(ct, extensionHeader)
}
// JSON converts any interface or string to JSON.
@@ -1069,11 +1117,6 @@ func (c *DefaultCtx) Next() error {
}
// Continue handler stack
- if c.app.newCtxFunc != nil {
- _, err := c.app.nextCustom(c)
- return err
- }
-
_, err := c.app.next(c)
return err
}
@@ -1084,11 +1127,7 @@ func (c *DefaultCtx) RestartRouting() error {
var err error
c.indexRoute = -1
- if c.app.newCtxFunc != nil {
- _, err = c.app.nextCustom(c)
- } else {
- _, err = c.app.next(c)
- }
+ _, err = c.app.next(c)
return err
}
@@ -1281,14 +1320,17 @@ func (c *DefaultCtx) Range(size int) (Range, error) {
rangeData Range
ranges string
)
- rangeStr := c.Get(HeaderRange)
+ rangeStr := utils.Trim(c.Get(HeaderRange), ' ')
i := strings.IndexByte(rangeStr, '=')
if i == -1 || strings.Contains(rangeStr[i+1:], "=") {
return rangeData, ErrRangeMalformed
}
- rangeData.Type = rangeStr[:i]
- ranges = rangeStr[i+1:]
+ rangeData.Type = utils.ToLower(utils.Trim(rangeStr[:i], ' '))
+ if rangeData.Type != "bytes" {
+ return rangeData, ErrRangeMalformed
+ }
+ ranges = utils.Trim(rangeStr[i+1:], ' ')
var (
singleRange string
@@ -1298,11 +1340,13 @@ func (c *DefaultCtx) Range(size int) (Range, error) {
singleRange = moreRanges
if i := strings.IndexByte(moreRanges, ','); i >= 0 {
singleRange = moreRanges[:i]
- moreRanges = moreRanges[i+1:]
+ moreRanges = utils.Trim(moreRanges[i+1:], ' ')
} else {
moreRanges = ""
}
+ singleRange = utils.Trim(singleRange, ' ')
+
var (
startStr, endStr string
i int
@@ -1310,8 +1354,8 @@ func (c *DefaultCtx) Range(size int) (Range, error) {
if i = strings.IndexByte(singleRange, '-'); i == -1 {
return rangeData, ErrRangeMalformed
}
- startStr = singleRange[:i]
- endStr = singleRange[i+1:]
+ startStr = utils.Trim(singleRange[:i], ' ')
+ endStr = utils.Trim(singleRange[i+1:], ' ')
start, startErr := fasthttp.ParseUint(utils.UnsafeBytes(startStr))
end, endErr := fasthttp.ParseUint(utils.UnsafeBytes(endStr))
@@ -1765,8 +1809,30 @@ func (c *DefaultCtx) Subdomains(offset ...int) []string {
return []string{}
}
- // strip “:port” if present
+ // Normalize host according to RFC 3986
host := c.Hostname()
+ // Trim the trailing dot of a fully-qualified domain
+ if strings.HasSuffix(host, ".") {
+ host = utils.TrimRight(host, '.')
+ }
+ host = utils.ToLower(host)
+
+ // Decode punycode labels only when necessary
+ if strings.Contains(host, "xn--") {
+ if u, err := idna.Lookup.ToUnicode(host); err == nil {
+ host = utils.ToLower(u)
+ }
+ }
+
+ // Return nothing for IP addresses
+ ip := host
+ if strings.HasPrefix(ip, "[") && strings.HasSuffix(ip, "]") {
+ ip = ip[1 : len(ip)-1]
+ }
+ if utils.IsIPv4(ip) || utils.IsIPv6(ip) {
+ return []string{}
+ }
+
parts := strings.Split(host, ".")
// offset == 0, caller wants everything.
diff --git a/ctx_interface.go b/ctx_interface.go
index c2eb8bf4d9e..e1b9b569d44 100644
--- a/ctx_interface.go
+++ b/ctx_interface.go
@@ -42,24 +42,12 @@ func NewDefaultCtx(app *App) *DefaultCtx {
return ctx
}
-func (app *App) newCtx() Ctx {
- var c Ctx
-
- if app.newCtxFunc != nil {
- c = app.newCtxFunc(app)
- } else {
- c = NewDefaultCtx(app)
- }
-
- return c
-}
-
// AcquireCtx retrieves a new Ctx from the pool.
-func (app *App) AcquireCtx(fctx *fasthttp.RequestCtx) Ctx {
- ctx, ok := app.pool.Get().(Ctx)
+func (app *App) AcquireCtx(fctx *fasthttp.RequestCtx) CustomCtx {
+ ctx, ok := app.pool.Get().(CustomCtx)
if !ok {
- panic(errors.New("failed to type-assert to Ctx"))
+ panic(errors.New("failed to type-assert to CustomCtx"))
}
ctx.Reset(fctx)
@@ -67,7 +55,7 @@ func (app *App) AcquireCtx(fctx *fasthttp.RequestCtx) Ctx {
}
// ReleaseCtx releases the ctx back into the pool.
-func (app *App) ReleaseCtx(c Ctx) {
+func (app *App) ReleaseCtx(c CustomCtx) {
c.release()
app.pool.Put(c)
}
diff --git a/ctx_test.go b/ctx_test.go
index ce083d6e111..d9d102c7e73 100644
--- a/ctx_test.go
+++ b/ctx_test.go
@@ -106,9 +106,7 @@ func (c *customCtx) Params(key string, defaultValue ...string) string { //revive
func Test_Ctx_CustomCtx(t *testing.T) {
t.Parallel()
- app := New()
-
- app.NewCtxFunc(func(app *App) CustomCtx {
+ app := NewWithCustomCtx(func(app *App) CustomCtx {
return &customCtx{
DefaultCtx: *NewDefaultCtx(app),
}
@@ -130,15 +128,12 @@ func Test_Ctx_CustomCtx_and_Method(t *testing.T) {
// Create app with custom request methods
methods := append(DefaultMethods, "JOHN") //nolint:gocritic // We want a new slice here
- app := New(Config{
- RequestMethods: methods,
- })
-
- // Create custom context
- app.NewCtxFunc(func(app *App) CustomCtx {
+ app := NewWithCustomCtx(func(app *App) CustomCtx {
return &customCtx{
DefaultCtx: *NewDefaultCtx(app),
}
+ }, Config{
+ RequestMethods: methods,
})
// Add route with custom method
@@ -925,6 +920,108 @@ func Test_Ctx_Cookie(t *testing.T) {
require.Equal(t, expect, c.Res().Get(HeaderSetCookie))
}
+// go test -run Test_Ctx_Cookie_PartitionedSecure
+func Test_Ctx_Cookie_PartitionedSecure(t *testing.T) {
+ t.Parallel()
+ app := New()
+ c := app.AcquireCtx(&fasthttp.RequestCtx{})
+
+ ck := &Cookie{
+ Name: "ps",
+ Value: "v",
+ Secure: true,
+ SameSite: CookieSameSiteNoneMode,
+ Partitioned: true,
+ }
+ c.Res().Cookie(ck)
+ require.Equal(t, "ps=v; path=/; secure; SameSite=None; Partitioned", c.Res().Get(HeaderSetCookie))
+}
+
+// go test -run Test_Ctx_Cookie_Invalid
+func Test_Ctx_Cookie_Invalid(t *testing.T) {
+ t.Parallel()
+ app := New()
+
+ cases := []*Cookie{
+ {Name: "", Value: "a"}, // empty name
+ {Name: "foo bar", Value: "a"}, // invalid char in name
+ {Name: "n", Value: "bad\nval"}, // invalid value byte
+ {Name: "d", Value: "b", Domain: "in valid"}, // invalid domain spaces
+ {Name: "d", Value: "b", Domain: "example..com"}, // invalid domain dots
+ {Name: "i", Value: "b", Domain: "2001:db8::1"}, // ipv6 not allowed
+ {Name: "p", Value: "b", Path: "\x00"}, // invalid path byte
+ {Name: "e", Value: "b", Expires: time.Date(1500, 1, 1, 0, 0, 0, 0, time.UTC)}, // invalid expires
+ {Name: "s", Value: "b", Partitioned: true}, // partitioned but not secure
+ }
+
+ for _, invalid := range cases {
+ c := app.AcquireCtx(&fasthttp.RequestCtx{})
+ c.Res().Cookie(invalid)
+ require.Empty(t, c.Res().Get(HeaderSetCookie))
+ c.Response().Header.Reset()
+ app.ReleaseCtx(c)
+ }
+}
+
+// go test -run Test_Ctx_Cookie_DefaultPath
+func Test_Ctx_Cookie_DefaultPath(t *testing.T) {
+ t.Parallel()
+ app := New()
+ c := app.AcquireCtx(&fasthttp.RequestCtx{})
+
+ ck := &Cookie{
+ Name: "p",
+ Value: "v",
+ // Path intentionally empty to verify defaulting
+ }
+
+ c.Res().Cookie(ck)
+ require.Equal(t,
+ "p=v; path=/; SameSite=Lax",
+ c.Res().Get(HeaderSetCookie),
+ )
+}
+
+// go test -run Test_Ctx_Cookie_MaxAgeOnly
+func Test_Ctx_Cookie_MaxAgeOnly(t *testing.T) {
+ t.Parallel()
+ app := New()
+ c := app.AcquireCtx(&fasthttp.RequestCtx{})
+
+ ck := &Cookie{
+ Name: "ttl",
+ Value: "v",
+ MaxAge: 3600,
+ }
+ c.Res().Cookie(ck)
+
+ require.Equal(t,
+ "ttl=v; max-age=3600; path=/; SameSite=Lax",
+ c.Res().Get(HeaderSetCookie),
+ )
+}
+
+// go test -run Test_Ctx_Cookie_StrictPartitioned
+func Test_Ctx_Cookie_StrictPartitioned(t *testing.T) {
+ t.Parallel()
+ app := New()
+ c := app.AcquireCtx(&fasthttp.RequestCtx{})
+
+ ck := &Cookie{
+ Name: "sp",
+ Value: "v",
+ Secure: true,
+ SameSite: CookieSameSiteStrictMode,
+ Partitioned: true,
+ }
+ c.Res().Cookie(ck)
+
+ require.Equal(t,
+ "sp=v; path=/; secure; SameSite=Strict; Partitioned",
+ c.Res().Get(HeaderSetCookie),
+ )
+}
+
// go test -v -run=^$ -bench=Benchmark_Ctx_Cookie -benchmem -count=4
func Benchmark_Ctx_Cookie(b *testing.B) {
app := New()
@@ -2134,6 +2231,16 @@ func Test_Ctx_Is(t *testing.T) {
require.False(t, c.Is("html"))
require.True(t, c.Is("txt"))
require.True(t, c.Is(".txt"))
+
+ // case-insensitive and trimmed
+ c.Request().Header.Set(HeaderContentType, "APPLICATION/JSON; charset=utf-8")
+ require.True(t, c.Is("json"))
+ require.True(t, c.Is(".json"))
+
+ // mismatched subtype should not match
+ c.Request().Header.Set(HeaderContentType, "application/json+xml")
+ require.False(t, c.Is("json"))
+ require.False(t, c.Is(".json"))
}
// go test -v -run=^$ -bench=Benchmark_Ctx_Is -benchmem -count=4
@@ -2836,6 +2943,8 @@ func Test_Ctx_Range(t *testing.T) {
testRange("bytes=0-0,2-1000", RangeSet{Start: 0, End: 0}, RangeSet{Start: 2, End: 999})
testRange("bytes=0-99,450-549,-100", RangeSet{Start: 0, End: 99}, RangeSet{Start: 450, End: 549}, RangeSet{Start: 900, End: 999})
testRange("bytes=500-700,601-999", RangeSet{Start: 500, End: 700}, RangeSet{Start: 601, End: 999})
+ testRange("bytes= 0-1", RangeSet{Start: 0, End: 1})
+ testRange("seconds=0-1")
}
// go test -v -run=^$ -bench=Benchmark_Ctx_Range -benchmem -count=4
@@ -3085,6 +3194,48 @@ func Test_Ctx_Subdomains(t *testing.T) {
offset: []int{2},
want: []string{"foo", "bar"},
},
+ {
+ name: "fully qualified domain trims trailing dot",
+ host: "john.doe.example.com.",
+ offset: nil,
+ want: []string{"john", "doe"},
+ },
+ {
+ name: "punycode domain is decoded",
+ host: "xn--bcher-kva.example.com",
+ offset: nil,
+ want: []string{"bücher"},
+ },
+ {
+ name: "punycode fqdn is decoded",
+ host: "xn--bcher-kva.example.com.",
+ offset: nil,
+ want: []string{"bücher"},
+ },
+ {
+ name: "punycode decode failure uses fallback",
+ host: "xn--bcher--.example.com",
+ offset: nil,
+ want: []string{"xn--bcher--"},
+ },
+ {
+ name: "invalid host keeps original lowercased",
+ host: "Foo Bar",
+ offset: []int{0},
+ want: []string{"foo bar"},
+ },
+ {
+ name: "IPv4 host returns empty",
+ host: "192.168.0.1",
+ offset: nil,
+ want: []string{},
+ },
+ {
+ name: "IPv6 host returns empty",
+ host: "[2001:db8::1]",
+ offset: nil,
+ want: []string{},
+ },
}
for _, tc := range cases {
diff --git a/docs/api/app.md b/docs/api/app.md
index 68e1e67e580..15f9cbc66ac 100644
--- a/docs/api/app.md
+++ b/docs/api/app.md
@@ -512,12 +512,13 @@ func (app *App) Handler() fasthttp.RequestHandler
func (app *App) ErrorHandler(ctx Ctx, err error) error
```
-## NewCtxFunc
+## NewWithCustomCtx
-`NewCtxFunc` allows you to customize the `ctx` struct as needed.
+`NewWithCustomCtx` creates a new `*App` and sets the custom context factory
+function at construction time.
```go title="Signature"
-func (app *App) NewCtxFunc(function func(app *App) CustomCtx)
+func NewWithCustomCtx(fn func(app *App) CustomCtx, config ...Config) *App
```
```go title="Example"
@@ -533,22 +534,18 @@ type CustomCtx struct {
fiber.DefaultCtx
}
-// Custom method
func (c *CustomCtx) Params(key string, defaultValue ...string) string {
return "prefix_" + c.DefaultCtx.Params(key)
}
func main() {
- app := fiber.New()
-
- app.NewCtxFunc(func(app *fiber.App) fiber.CustomCtx {
+ app := fiber.NewWithCustomCtx(func(app *fiber.App) fiber.CustomCtx {
return &CustomCtx{
DefaultCtx: *fiber.NewDefaultCtx(app),
}
})
app.Get("/:id", func(c fiber.Ctx) error {
- // Use custom method - output: prefix_123
return c.SendString(c.Params("id"))
})
diff --git a/docs/api/ctx.md b/docs/api/ctx.md
index 0a4de67d7c6..063542d6041 100644
--- a/docs/api/ctx.md
+++ b/docs/api/ctx.md
@@ -1213,6 +1213,9 @@ The generic `Query` function supports returning the following data types based o
### Range
Returns a struct containing the type and a slice of ranges.
+Only the canonical `bytes` unit is recognized and any optional
+whitespace around range specifiers will be ignored, as specified
+in RFC 9110.
```go title="Signature"
func (c fiber.Ctx) Range(size int) (Range, error)
diff --git a/docs/middleware/basicauth.md b/docs/middleware/basicauth.md
index c6104a79842..74c70865a53 100644
--- a/docs/middleware/basicauth.md
+++ b/docs/middleware/basicauth.md
@@ -6,7 +6,7 @@ id: basicauth
Basic Authentication middleware for [Fiber](https://github.com/gofiber/fiber) that provides an HTTP basic authentication. It calls the next handler for valid credentials and [401 Unauthorized](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/401) or a custom response for missing or invalid credentials.
-The default unauthorized response includes the header `WWW-Authenticate: Basic realm="Restricted"`.
+The default unauthorized response includes the header `WWW-Authenticate: Basic realm="Restricted", charset="UTF-8"` and sets `Cache-Control: no-store`.
## Signatures
@@ -78,6 +78,8 @@ func handler(c fiber.Ctx) error {
| Next | `func(fiber.Ctx) bool` | Next defines a function to skip this middleware when returned true. | `nil` |
| Users | `map[string]string` | Users defines the allowed credentials. | `map[string]string{}` |
| Realm | `string` | Realm is a string to define the realm attribute of BasicAuth. The realm identifies the system to authenticate against and can be used by clients to save credentials. | `"Restricted"` |
+| Charset | `string` | Charset sent in the `WWW-Authenticate` header, so clients know how credentials are encoded. | `"UTF-8"` |
+| StorePassword | `bool` | Store the plaintext password in the context and retrieve it via `PasswordFromContext`. | `false` |
| Authorizer | `func(string, string) bool` | Authorizer defines a function to check the credentials. It will be called with a username and password and is expected to return true or false to indicate approval. | `nil` |
| Unauthorized | `fiber.Handler` | Unauthorized defines the response body for unauthorized responses. | `nil` |
@@ -88,6 +90,8 @@ var ConfigDefault = Config{
Next: nil,
Users: map[string]string{},
Realm: "Restricted",
+ Charset: "UTF-8",
+ StorePassword: false,
Authorizer: nil,
Unauthorized: nil,
}
diff --git a/docs/whats_new.md b/docs/whats_new.md
index 552a1de1fef..ecd0b3fc589 100644
--- a/docs/whats_new.md
+++ b/docs/whats_new.md
@@ -67,10 +67,41 @@ We have made several changes to the Fiber app, including:
- **RegisterCustomBinder**: Allows for the registration of custom binders.
- **RegisterCustomConstraint**: Allows for the registration of custom constraints.
-- **NewCtxFunc**: Introduces a new context function.
+- **NewWithCustomCtx**: Initialize an app with a custom context in one step.
- **State**: Provides a global state for the application, which can be used to store and retrieve data across the application. Check out the [State](./api/state) method for further details.
- **NewErrorf**: Allows variadic parameters when creating formatted errors.
+#### Custom Route Constraints
+
+Custom route constraints enable you to define your own validation rules for route parameters.
+Use `RegisterCustomConstraint` to add a constraint type that implements the `CustomConstraint` interface.
+
+
+Example
+
+```go
+type UlidConstraint struct {
+ fiber.CustomConstraint
+}
+
+func (*UlidConstraint) Name() string {
+ return "ulid"
+}
+
+func (*UlidConstraint) Execute(param string, args ...string) bool {
+ _, err := ulid.Parse(param)
+ return err == nil
+}
+
+app.RegisterCustomConstraint(&UlidConstraint{})
+
+app.Get("/login/:id", func(c fiber.Ctx) error {
+ return c.SendString("User " + c.Params("id"))
+})
+```
+
+
+
### Removed Methods
- **Mount**: Use `app.Use()` instead.
@@ -95,19 +126,17 @@ Fiber v3 introduces a customizable `Ctx` interface, allowing developers to exten
The idea behind custom `Ctx` classes is to give developers the ability to extend the default context with additional methods and properties tailored to the specific requirements of their application. This allows for better request handling and easier implementation of specific logic.
-#### NewCtxFunc
+#### NewWithCustomCtx
-The `NewCtxFunc` method allows you to customize the `Ctx` struct as needed.
+`NewWithCustomCtx` creates the application and sets the custom context factory at initialization time.
```go title="Signature"
-func (app *App) NewCtxFunc(function func(app *App) CustomCtx)
+func NewWithCustomCtx(fn func(app *App) CustomCtx, config ...Config) *App
```
Example
-Here’s an example of how to customize the `Ctx` interface:
-
```go
package main
@@ -120,15 +149,12 @@ type CustomCtx struct {
fiber.Ctx
}
-// Custom method
func (c *CustomCtx) CustomMethod() string {
return "custom value"
}
func main() {
- app := fiber.New()
-
- app.NewCtxFunc(func(app *fiber.App) fiber.Ctx {
+ app := fiber.NewWithCustomCtx(func(app *fiber.App) fiber.Ctx {
return &CustomCtx{
Ctx: *fiber.NewCtx(app),
}
@@ -143,7 +169,7 @@ func main() {
}
```
-In this example, a custom context `CustomCtx` is created with an additional method `CustomMethod`. The `NewCtxFunc` method is used to replace the default context with the custom one.
+This example creates a `CustomCtx` with an extra `CustomMethod` and initializes the app with `NewWithCustomCtx`.
@@ -975,7 +1001,7 @@ The adaptor middleware has been significantly optimized for performance and effi
### BasicAuth
-The BasicAuth middleware was updated for improved robustness in parsing the Authorization header, with enhanced validation and whitespace handling. The default unauthorized response now uses a properly quoted and capitalized `WWW-Authenticate` header.
+The BasicAuth middleware now validates the `Authorization` header more rigorously and sets security-focused response headers. The default challenge includes the `charset="UTF-8"` parameter and disables caching. Passwords are no longer stored in the request context by default; use the new `StorePassword` option to retain them. A `Charset` option controls the value used in the challenge header.
### Cache
diff --git a/go.mod b/go.mod
index df96fee8e84..a0e3067461b 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.24.0
require (
github.com/gofiber/schema v1.5.0
- github.com/gofiber/utils/v2 v2.0.0-beta.8
+ github.com/gofiber/utils/v2 v2.0.0-beta.9
github.com/google/uuid v1.6.0
github.com/mattn/go-colorable v0.1.14
github.com/mattn/go-isatty v0.0.20
@@ -23,7 +23,7 @@ require (
github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
- golang.org/x/net v0.40.0 // indirect
+ golang.org/x/net v0.41.0
golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.26.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
diff --git a/go.sum b/go.sum
index 902d56be73d..469393b964e 100644
--- a/go.sum
+++ b/go.sum
@@ -6,8 +6,8 @@ github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vt
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/gofiber/schema v1.5.0 h1:dcbLol88CXdLFUY3K3TKp3SZ90v8CKIjgJp1/GfzwqU=
github.com/gofiber/schema v1.5.0/go.mod h1:YYwj01w3hVfaNjhtJzaqetymL56VW642YS3qZPhuE6c=
-github.com/gofiber/utils/v2 v2.0.0-beta.8 h1:ZifwbHZqZO3YJsx1ZhDsWnPjaQ7C0YD20LHt+DQeXOU=
-github.com/gofiber/utils/v2 v2.0.0-beta.8/go.mod h1:1lCBo9vEF4RFEtTgWntipnaScJZQiM8rrsYycLZ4n9c=
+github.com/gofiber/utils/v2 v2.0.0-beta.9 h1:IMb2TpF2bb1spuB63GuiOZJXFfq9VJe98ofFJoy0EAY=
+github.com/gofiber/utils/v2 v2.0.0-beta.9/go.mod h1:XjKLrtxE77EyWzzWGWAepv3NLclRSZkAG+Y+GfPcKeQ=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@@ -34,8 +34,8 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
-golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY=
-golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds=
+golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
+golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
diff --git a/helpers.go b/helpers.go
index e2dcaacacf2..db8c8aa0966 100644
--- a/helpers.go
+++ b/helpers.go
@@ -101,95 +101,6 @@ func (app *App) quoteString(raw string) string {
return quoted
}
-// Scan stack if other methods match the request
-func (app *App) methodExist(c *DefaultCtx) bool {
- var exists bool
-
- methods := app.config.RequestMethods
- for i := 0; i < len(methods); i++ {
- // Skip original method
- if c.getMethodInt() == i {
- continue
- }
- // Reset stack index
- c.setIndexRoute(-1)
-
- tree, ok := c.App().treeStack[i][c.treePathHash]
- if !ok {
- tree = c.App().treeStack[i][0]
- }
- // Get stack length
- lenr := len(tree) - 1
- // Loop over the route stack starting from previous index
- for c.getIndexRoute() < lenr {
- // Increment route index
- c.setIndexRoute(c.getIndexRoute() + 1)
- // Get *Route
- route := tree[c.getIndexRoute()]
- // Skip use routes
- if route.use {
- continue
- }
- // Check if it matches the request path
- match := route.match(c.getDetectionPath(), c.Path(), c.getValues())
- // No match, next route
- if match {
- // We matched
- exists = true
- // Add method to Allow header
- c.Append(HeaderAllow, methods[i])
- // Break stack loop
- break
- }
- }
- }
- return exists
-}
-
-// Scan stack if other methods match the request
-func (app *App) methodExistCustom(c CustomCtx) bool {
- var exists bool
- methods := app.config.RequestMethods
- for i := 0; i < len(methods); i++ {
- // Skip original method
- if c.getMethodInt() == i {
- continue
- }
- // Reset stack index
- c.setIndexRoute(-1)
-
- tree, ok := c.App().treeStack[i][c.getTreePathHash()]
- if !ok {
- tree = c.App().treeStack[i][0]
- }
- // Get stack length
- lenr := len(tree) - 1
- // Loop over the route stack starting from previous index
- for c.getIndexRoute() < lenr {
- // Increment route index
- c.setIndexRoute(c.getIndexRoute() + 1)
- // Get *Route
- route := tree[c.getIndexRoute()]
- // Skip use routes
- if route.use {
- continue
- }
- // Check if it matches the request path
- match := route.match(c.getDetectionPath(), c.Path(), c.getValues())
- // No match, next route
- if match {
- // We matched
- exists = true
- // Add method to Allow header
- c.Append(HeaderAllow, methods[i])
- // Break stack loop
- break
- }
- }
- }
- return exists
-}
-
// uniqueRouteStack drop all not unique routes from the slice
func uniqueRouteStack(stack []*Route) []*Route {
var unique []*Route
@@ -605,14 +516,8 @@ const noCacheValue = "no-cache"
func isNoCache(cacheControl string) bool {
n := len(cacheControl)
ncLen := len(noCacheValue)
- for i := 0; i < n; i++ {
- if cacheControl[i] != 'n' {
- continue
- }
- if i+ncLen > n {
- return false
- }
- if cacheControl[i:i+ncLen] != noCacheValue {
+ for i := 0; i <= n-ncLen; i++ {
+ if !utils.EqualFold(cacheControl[i:i+ncLen], noCacheValue) {
continue
}
if i > 0 {
diff --git a/helpers_test.go b/helpers_test.go
index 334065a7915..fcb9d7a90eb 100644
--- a/helpers_test.go
+++ b/helpers_test.go
@@ -596,6 +596,8 @@ func Test_Utils_IsNoCache(t *testing.T) {
{string: "no-cache, public", bool: true},
{string: "Xno-cache, public", bool: false},
{string: "max-age=30, no-cache,public", bool: true},
+ {string: "NO-CACHE", bool: true},
+ {string: "public, NO-CACHE", bool: true},
}
for _, c := range testCases {
diff --git a/middleware/basicauth/basicauth.go b/middleware/basicauth/basicauth.go
index ee22d4f02b1..17cf041f6b9 100644
--- a/middleware/basicauth/basicauth.go
+++ b/middleware/basicauth/basicauth.go
@@ -18,6 +18,8 @@ const (
passwordKey
)
+const basicScheme = "Basic"
+
// New creates a new middleware handler
func New(config Config) fiber.Handler {
// Set default config
@@ -30,12 +32,14 @@ func New(config Config) fiber.Handler {
return c.Next()
}
- // Get authorization header
+ // Get authorization header and ensure it matches the Basic scheme
auth := utils.Trim(c.Get(fiber.HeaderAuthorization), ' ')
+ if auth == "" {
+ return cfg.Unauthorized(c)
+ }
- // Expect a scheme token followed by credentials
parts := strings.Fields(auth)
- if len(parts) != 2 || !utils.EqualFold(parts[0], "basic") {
+ if len(parts) != 2 || !utils.EqualFold(parts[0], basicScheme) {
return cfg.Unauthorized(c)
}
@@ -66,7 +70,9 @@ func New(config Config) fiber.Handler {
if cfg.Authorizer(username, password) {
c.Locals(usernameKey, username)
- c.Locals(passwordKey, password)
+ if cfg.StorePassword {
+ c.Locals(passwordKey, password)
+ }
return c.Next()
}
diff --git a/middleware/basicauth/basicauth_test.go b/middleware/basicauth/basicauth_test.go
index 97dfdac6591..ff32eebdb22 100644
--- a/middleware/basicauth/basicauth_test.go
+++ b/middleware/basicauth/basicauth_test.go
@@ -36,6 +36,7 @@ func Test_Middleware_BasicAuth(t *testing.T) {
"john": "doe",
"admin": "123456",
},
+ StorePassword: true,
}))
app.Get("/testauth", func(c fiber.Ctx) error {
@@ -91,6 +92,27 @@ func Test_Middleware_BasicAuth(t *testing.T) {
}
}
+func Test_BasicAuth_NoStorePassword(t *testing.T) {
+ t.Parallel()
+ app := fiber.New()
+
+ app.Use(New(Config{
+ Users: map[string]string{"john": "doe"},
+ }))
+
+ app.Get("/", func(c fiber.Ctx) error {
+ require.Empty(t, PasswordFromContext(c))
+ return c.SendStatus(fiber.StatusOK)
+ })
+
+ creds := base64.StdEncoding.EncodeToString([]byte("john:doe"))
+ req := httptest.NewRequest(fiber.MethodGet, "/", nil)
+ req.Header.Set(fiber.HeaderAuthorization, "Basic "+creds)
+ resp, err := app.Test(req)
+ require.NoError(t, err)
+ require.Equal(t, fiber.StatusOK, resp.StatusCode)
+}
+
func Test_BasicAuth_WWWAuthenticateHeader(t *testing.T) {
t.Parallel()
app := fiber.New()
@@ -100,7 +122,7 @@ func Test_BasicAuth_WWWAuthenticateHeader(t *testing.T) {
resp, err := app.Test(httptest.NewRequest(fiber.MethodGet, "/", nil))
require.NoError(t, err)
require.Equal(t, fiber.StatusUnauthorized, resp.StatusCode)
- require.Equal(t, `Basic realm="Restricted"`, resp.Header.Get(fiber.HeaderWWWAuthenticate))
+ require.Equal(t, `Basic realm="Restricted", charset="UTF-8"`, resp.Header.Get(fiber.HeaderWWWAuthenticate))
}
func Test_BasicAuth_InvalidHeader(t *testing.T) {
diff --git a/middleware/basicauth/config.go b/middleware/basicauth/config.go
index 1d839ba06b9..a6493fcea31 100644
--- a/middleware/basicauth/config.go
+++ b/middleware/basicauth/config.go
@@ -41,15 +41,30 @@ type Config struct {
//
// Optional. Default: "Restricted".
Realm string
+
+ // Charset defines the value for the charset parameter in the
+ // WWW-Authenticate header. According to RFC 7617 clients can use
+ // this value to interpret credentials correctly.
+ //
+ // Optional. Default: "UTF-8".
+ Charset string
+
+ // StorePassword determines if the plaintext password should be stored
+ // in the context for later retrieval via PasswordFromContext.
+ //
+ // Optional. Default: false.
+ StorePassword bool
}
// ConfigDefault is the default config
var ConfigDefault = Config{
- Next: nil,
- Users: map[string]string{},
- Realm: "Restricted",
- Authorizer: nil,
- Unauthorized: nil,
+ Next: nil,
+ Users: map[string]string{},
+ Realm: "Restricted",
+ Charset: "UTF-8",
+ StorePassword: false,
+ Authorizer: nil,
+ Unauthorized: nil,
}
// Helper function to set default values
@@ -72,6 +87,9 @@ func configDefault(config ...Config) Config {
if cfg.Realm == "" {
cfg.Realm = ConfigDefault.Realm
}
+ if cfg.Charset == "" {
+ cfg.Charset = ConfigDefault.Charset
+ }
if cfg.Authorizer == nil {
cfg.Authorizer = func(user, pass string) bool {
userPwd, exist := cfg.Users[user]
@@ -80,7 +98,13 @@ func configDefault(config ...Config) Config {
}
if cfg.Unauthorized == nil {
cfg.Unauthorized = func(c fiber.Ctx) error {
- c.Set(fiber.HeaderWWWAuthenticate, "Basic realm="+strconv.Quote(cfg.Realm))
+ header := "Basic realm=" + strconv.Quote(cfg.Realm)
+ if cfg.Charset != "" {
+ header += ", charset=" + strconv.Quote(cfg.Charset)
+ }
+ c.Set(fiber.HeaderWWWAuthenticate, header)
+ c.Set(fiber.HeaderCacheControl, "no-store")
+ c.Set(fiber.HeaderVary, fiber.HeaderAuthorization)
return c.SendStatus(fiber.StatusUnauthorized)
}
}
diff --git a/middleware/cache/cache.go b/middleware/cache/cache.go
index 9be176e44d8..67e9b88eca1 100644
--- a/middleware/cache/cache.go
+++ b/middleware/cache/cache.go
@@ -297,7 +297,25 @@ func New(config ...Config) fiber.Handler {
// Check if request has directive
func hasRequestDirective(c fiber.Ctx, directive string) bool {
- return strings.Contains(c.Get(fiber.HeaderCacheControl), directive)
+ cc := c.Get(fiber.HeaderCacheControl)
+ ccLen := len(cc)
+ dirLen := len(directive)
+ for i := 0; i <= ccLen-dirLen; i++ {
+ if !utils.EqualFold(cc[i:i+dirLen], directive) {
+ continue
+ }
+ if i > 0 {
+ prev := cc[i-1]
+ if prev != ' ' && prev != ',' {
+ continue
+ }
+ }
+ if i+dirLen == ccLen || cc[i+dirLen] == ',' {
+ return true
+ }
+ }
+
+ return false
}
// parseMaxAge extracts the max-age directive from a Cache-Control header.
diff --git a/middleware/cache/cache_test.go b/middleware/cache/cache_test.go
index fa9d65cfe8b..53118091a97 100644
--- a/middleware/cache/cache_test.go
+++ b/middleware/cache/cache_test.go
@@ -163,6 +163,38 @@ func Test_Cache_WithNoCacheRequestDirective(t *testing.T) {
require.Equal(t, []byte("2"), noCacheBody1)
// Response cached, returns updated response, entry = 2
+ // Request id = 3 with Cache-Control: NO-CACHE
+ noCacheReqUpper := httptest.NewRequest(fiber.MethodGet, "/?id=3", nil)
+ noCacheReqUpper.Header.Set(fiber.HeaderCacheControl, "NO-CACHE")
+ noCacheRespUpper, err := app.Test(noCacheReqUpper)
+ require.NoError(t, err)
+ noCacheBodyUpper, err := io.ReadAll(noCacheRespUpper.Body)
+ require.NoError(t, err)
+ require.Equal(t, cacheMiss, noCacheRespUpper.Header.Get("X-Cache"))
+ require.Equal(t, []byte("3"), noCacheBodyUpper)
+ // Response cached, returns updated response, entry = 3
+
+ // Request id = 4 with Cache-Control: my-no-cache
+ invalidReq := httptest.NewRequest(fiber.MethodGet, "/?id=4", nil)
+ invalidReq.Header.Set(fiber.HeaderCacheControl, "my-no-cache")
+ invalidResp, err := app.Test(invalidReq)
+ require.NoError(t, err)
+ invalidBody, err := io.ReadAll(invalidResp.Body)
+ require.NoError(t, err)
+ require.Equal(t, cacheHit, invalidResp.Header.Get("X-Cache"))
+ require.Equal(t, []byte("3"), invalidBody)
+ // Response served from cache, existing entry = 3
+
+ // Request id = 4 again without Cache-Control: no-cache
+ cachedInvalidReq := httptest.NewRequest(fiber.MethodGet, "/?id=4", nil)
+ cachedInvalidResp, err := app.Test(cachedInvalidReq)
+ require.NoError(t, err)
+ cachedInvalidBody, err := io.ReadAll(cachedInvalidResp.Body)
+ require.NoError(t, err)
+ require.Equal(t, cacheHit, cachedInvalidResp.Header.Get("X-Cache"))
+ require.Equal(t, []byte("3"), cachedInvalidBody)
+ // Response cached, returns cached response, entry id = 3
+
// Request id = 1 without Cache-Control: no-cache
cachedReq1 := httptest.NewRequest(fiber.MethodGet, "/", nil)
cachedResp1, err := app.Test(cachedReq1)
@@ -170,8 +202,8 @@ func Test_Cache_WithNoCacheRequestDirective(t *testing.T) {
cachedBody1, err := io.ReadAll(cachedResp1.Body)
require.NoError(t, err)
require.Equal(t, cacheHit, cachedResp1.Header.Get("X-Cache"))
- require.Equal(t, []byte("2"), cachedBody1)
- // Response not cached, returns cached response, entry id = 2
+ require.Equal(t, []byte("3"), cachedBody1)
+ // Response not cached, returns cached response, entry id = 3
}
// go test -run Test_Cache_WithETagAndNoCacheRequestDirective
@@ -221,6 +253,16 @@ func Test_Cache_WithETagAndNoCacheRequestDirective(t *testing.T) {
// If response status 200
etagToken = noCacheResp.Header.Get("Etag")
+ // Request id = 3 with ETag and Cache-Control: NO-CACHE
+ noCacheReqUpper := httptest.NewRequest(fiber.MethodGet, "/?id=3", nil)
+ noCacheReqUpper.Header.Set(fiber.HeaderCacheControl, "NO-CACHE")
+ noCacheReqUpper.Header.Set(fiber.HeaderIfNoneMatch, etagToken)
+ noCacheRespUpper, err := app.Test(noCacheReqUpper)
+ require.NoError(t, err)
+ require.Equal(t, cacheMiss, noCacheRespUpper.Header.Get("X-Cache"))
+ require.Equal(t, fiber.StatusOK, noCacheRespUpper.StatusCode)
+ // Response cached, returns updated response, entry id = 3
+
// Request id = 2 with ETag and Cache-Control: no-cache again
noCacheReq1 := httptest.NewRequest(fiber.MethodGet, "/?id=2", nil)
noCacheReq1.Header.Set(fiber.HeaderCacheControl, noCache)
@@ -260,6 +302,37 @@ func Test_Cache_WithNoStoreRequestDirective(t *testing.T) {
require.NoError(t, err)
require.Equal(t, []byte("2"), noStoreBody)
// Response not cached, returns updated response
+
+ // Request id = 3 with Cache-Control: NO-STORE
+ noStoreReqUpper := httptest.NewRequest(fiber.MethodGet, "/?id=3", nil)
+ noStoreReqUpper.Header.Set(fiber.HeaderCacheControl, "NO-STORE")
+ noStoreRespUpper, err := app.Test(noStoreReqUpper)
+ require.NoError(t, err)
+ noStoreBodyUpper, err := io.ReadAll(noStoreRespUpper.Body)
+ require.NoError(t, err)
+ require.Equal(t, []byte("3"), noStoreBodyUpper)
+ // Response not cached, returns updated response
+
+ // Request id = 4 with Cache-Control: my-no-store
+ invalidReq := httptest.NewRequest(fiber.MethodGet, "/?id=4", nil)
+ invalidReq.Header.Set(fiber.HeaderCacheControl, "my-no-store")
+ invalidResp, err := app.Test(invalidReq)
+ require.NoError(t, err)
+ invalidBody, err := io.ReadAll(invalidResp.Body)
+ require.NoError(t, err)
+ require.Equal(t, cacheMiss, invalidResp.Header.Get("X-Cache"))
+ require.Equal(t, []byte("4"), invalidBody)
+ // Response cached, returns updated response, entry = 4
+
+ // Request id = 4 again without Cache-Control
+ cachedInvalidReq := httptest.NewRequest(fiber.MethodGet, "/?id=4", nil)
+ cachedInvalidResp, err := app.Test(cachedInvalidReq)
+ require.NoError(t, err)
+ cachedInvalidBody, err := io.ReadAll(cachedInvalidResp.Body)
+ require.NoError(t, err)
+ require.Equal(t, cacheHit, cachedInvalidResp.Header.Get("X-Cache"))
+ require.Equal(t, []byte("4"), cachedInvalidBody)
+ // Response cached previously, served from cache
}
func Test_Cache_WithSeveralRequests(t *testing.T) {
diff --git a/middleware/cors/cors_test.go b/middleware/cors/cors_test.go
index c182f22c720..63c1a326c78 100644
--- a/middleware/cors/cors_test.go
+++ b/middleware/cors/cors_test.go
@@ -1,11 +1,14 @@
package cors
import (
+ "bytes"
"net/http/httptest"
+ "os"
"strings"
"testing"
"github.com/gofiber/fiber/v3"
+ "github.com/gofiber/fiber/v3/log"
"github.com/stretchr/testify/require"
"github.com/valyala/fasthttp"
)
@@ -261,6 +264,21 @@ func Test_CORS_Wildcard_AllowCredentials_Panic(t *testing.T) {
}
}
+// Test that a warning is logged when AllowOrigins allows all origins and
+// AllowOriginsFunc is also provided.
+func Test_CORS_Warn_AllowAllOrigins_WithFunc(t *testing.T) {
+ var buf bytes.Buffer
+ log.SetOutput(&buf)
+ t.Cleanup(func() { log.SetOutput(os.Stderr) })
+
+ fiber.New().Use(New(Config{
+ AllowOrigins: []string{"*"},
+ AllowOriginsFunc: func(string) bool { return true },
+ }))
+
+ require.Contains(t, buf.String(), "AllowOriginsFunc' will not be used")
+}
+
// go test -run -v Test_CORS_Invalid_Origin_Panic
func Test_CORS_Invalid_Origins_Panic(t *testing.T) {
t.Parallel()
diff --git a/middleware/csrf/csrf_test.go b/middleware/csrf/csrf_test.go
index f137b72348b..b420427c83a 100644
--- a/middleware/csrf/csrf_test.go
+++ b/middleware/csrf/csrf_test.go
@@ -1,13 +1,16 @@
package csrf
import (
+ "bytes"
"net/http"
"net/http/httptest"
+ "os"
"strings"
"testing"
"time"
"github.com/gofiber/fiber/v3"
+ "github.com/gofiber/fiber/v3/log"
"github.com/gofiber/fiber/v3/middleware/session"
"github.com/gofiber/utils/v2"
"github.com/stretchr/testify/require"
@@ -1594,3 +1597,43 @@ func Test_CSRF_FromContextMethods_Invalid(t *testing.T) {
require.NoError(t, err)
require.Equal(t, fiber.StatusOK, resp.StatusCode)
}
+
+func Test_configDefault_WarnCookieSameSite(t *testing.T) {
+ var buf bytes.Buffer
+ log.SetOutput(&buf)
+ t.Cleanup(func() { log.SetOutput(os.Stderr) })
+
+ cfg := configDefault(Config{
+ KeyLookup: "cookie:csrf",
+ CookieSameSite: "None",
+ })
+
+ require.Equal(t, "csrf", cfg.CookieName)
+ require.Contains(t, buf.String(), "Cookie extractor is only recommended for use with SameSite=Lax or SameSite=Strict")
+}
+
+func Test_deleteTokenFromStorage(t *testing.T) {
+ t.Parallel()
+
+ app := fiber.New()
+ ctx := app.AcquireCtx(&fasthttp.RequestCtx{})
+ defer app.ReleaseCtx(ctx)
+
+ token := "token123"
+ dummy := []byte("dummy")
+
+ store := session.NewStore()
+ sm := newSessionManager(store)
+ stm := newStorageManager(nil)
+
+ sm.setRaw(ctx, token, dummy, time.Minute)
+ deleteTokenFromStorage(ctx, token, Config{Session: store}, sm, stm)
+ require.Nil(t, sm.getRaw(ctx, token, dummy))
+
+ sm2 := newSessionManager(nil)
+ stm2 := newStorageManager(nil)
+
+ stm2.setRaw(token, dummy, time.Minute)
+ deleteTokenFromStorage(ctx, token, Config{}, sm2, stm2)
+ require.Nil(t, stm2.getRaw(token))
+}
diff --git a/router.go b/router.go
index 502f4f650f3..869e105cfc3 100644
--- a/router.go
+++ b/router.go
@@ -6,7 +6,6 @@ package fiber
import (
"bytes"
- "errors"
"fmt"
"html"
"slices"
@@ -107,163 +106,238 @@ func (r *Route) match(detectionPath, path string, params *[maxParams]string) boo
return false
}
-func (app *App) nextCustom(c CustomCtx) (bool, error) { //nolint:unparam // bool param might be useful for testing
+func (app *App) next(c *DefaultCtx) (bool, error) {
+ methodInt := c.methodInt
+ treeHash := c.treePathHash
// Get stack length
- tree, ok := app.treeStack[c.getMethodInt()][c.getTreePathHash()]
+ tree, ok := app.treeStack[methodInt][treeHash]
if !ok {
- tree = app.treeStack[c.getMethodInt()][0]
+ tree = app.treeStack[methodInt][0]
}
lenr := len(tree) - 1
+ indexRoute := c.indexRoute
+ var err error
+
// Loop over the route stack starting from previous index
- for c.getIndexRoute() < lenr {
+ for indexRoute < lenr {
// Increment route index
- c.setIndexRoute(c.getIndexRoute() + 1)
+ indexRoute++
// Get *Route
- route := tree[c.getIndexRoute()]
+ route := tree[indexRoute]
- // Check if it matches the request path
- match := route.match(c.getDetectionPath(), c.Path(), c.getValues())
+ if route.mount {
+ continue
+ }
- // No match, next route
- if !match {
+ // Check if it matches the request path
+ if !route.match(utils.UnsafeString(c.detectionPath), utils.UnsafeString(c.path), &c.values) {
continue
}
- // Pass route reference and param values
- c.setRoute(route)
+ // Pass route reference and param values
+ c.route = route
// Non use handler matched
- if !c.getMatched() && !route.use {
- c.setMatched(true)
+ if !route.use {
+ c.matched = true
}
-
// Execute first handler of route
- c.setIndexHandler(0)
- err := route.Handlers[0](c)
- return match, err // Stop scanning the stack
+ if len(route.Handlers) > 0 {
+ c.indexHandler = 0
+ c.indexRoute = indexRoute
+ return true, route.Handlers[0](c)
+ }
+
+ return true, nil // Stop scanning the stack
}
// If c.Next() does not match, return 404
- err := NewError(StatusNotFound, "Cannot "+c.Method()+" "+c.getPathOriginal())
+ err = NewError(StatusNotFound, "Cannot "+c.Method()+" "+html.EscapeString(c.getPathOriginal()))
// If no match, scan stack again if other methods match the request
// Moved from app.handler because middleware may break the route chain
- if !c.getMatched() && app.methodExistCustom(c) {
+ if c.matched {
+ return false, err
+ }
+
+ exists := false
+ methods := app.config.RequestMethods
+ for i := 0; i < len(methods); i++ {
+ // Skip original method
+ if methodInt == i {
+ continue
+ }
+ // Reset stack index
+ indexRoute := -1
+
+ tree, ok := app.treeStack[i][treeHash]
+ if !ok {
+ tree = app.treeStack[i][0]
+ }
+ // Get stack length
+ lenr := len(tree) - 1
+ // Loop over the route stack starting from previous index
+ for indexRoute < lenr {
+ // Increment route index
+ indexRoute++
+ // Get *Route
+ route := tree[indexRoute]
+ // Skip use routes
+ if route.use {
+ continue
+ }
+ // Check if it matches the request path
+ // No match, next route
+ if route.match(utils.UnsafeString(c.detectionPath), utils.UnsafeString(c.path), &c.values) {
+ // We matched
+ exists = true
+ // Add method to Allow header
+ c.Append(HeaderAllow, methods[i])
+ // Break stack loop
+ break
+ }
+ }
+ c.indexRoute = indexRoute
+ }
+ if exists {
err = ErrMethodNotAllowed
}
return false, err
}
-func (app *App) next(c *DefaultCtx) (bool, error) {
+func (app *App) nextCustom(c CustomCtx) (bool, error) {
+ methodInt := c.getMethodInt()
+ treeHash := c.getTreePathHash()
// Get stack length
- tree, ok := app.treeStack[c.methodInt][c.treePathHash]
+ tree, ok := app.treeStack[methodInt][treeHash]
if !ok {
- tree = app.treeStack[c.methodInt][0]
+ tree = app.treeStack[methodInt][0]
}
- lenTree := len(tree) - 1
+ lenr := len(tree) - 1
+
+ indexRoute := c.getIndexRoute()
+ var err error
// Loop over the route stack starting from previous index
- for c.indexRoute < lenTree {
+ for indexRoute < lenr {
// Increment route index
- c.indexRoute++
+ indexRoute++
// Get *Route
- route := tree[c.indexRoute]
+ route := tree[indexRoute]
- var match bool
- var err error
- // skip for mounted apps
if route.mount {
continue
}
// Check if it matches the request path
- match = route.match(utils.UnsafeString(c.detectionPath), utils.UnsafeString(c.path), &c.values)
- if !match {
- // No match, next route
+ if !route.match(c.getDetectionPath(), c.Path(), c.getValues()) {
continue
}
// Pass route reference and param values
- c.route = route
-
+ c.setRoute(route)
// Non use handler matched
- if !c.matched && !route.use {
- c.matched = true
+ if !route.use {
+ c.setMatched(true)
}
-
// Execute first handler of route
- c.indexHandler = 0
if len(route.Handlers) > 0 {
- err = route.Handlers[0](c)
+ c.setIndexHandler(0)
+ c.setIndexRoute(indexRoute)
+ return true, route.Handlers[0](c)
}
- return match, err // Stop scanning the stack
+ return true, nil // Stop scanning the stack
}
// If c.Next() does not match, return 404
- err := NewError(StatusNotFound, "Cannot "+c.Method()+" "+html.EscapeString(c.pathOriginal))
- if !c.matched && app.methodExist(c) {
- // If no match, scan stack again if other methods match the request
- // Moved from app.handler because middleware may break the route chain
- err = ErrMethodNotAllowed
- }
- return false, err
-}
-
-func (app *App) defaultRequestHandler(rctx *fasthttp.RequestCtx) {
- // Acquire DefaultCtx from the pool
- ctx, ok := app.AcquireCtx(rctx).(*DefaultCtx)
- if !ok {
- panic(errors.New("requestHandler: failed to type-assert to *DefaultCtx"))
- }
-
- defer app.ReleaseCtx(ctx)
+ err = NewError(StatusNotFound, "Cannot "+c.Method()+" "+html.EscapeString(c.getPathOriginal()))
- // Check if the HTTP method is valid
- if ctx.methodInt == -1 {
- _ = ctx.SendStatus(StatusNotImplemented) //nolint:errcheck // Always return nil
- return
+ // If no match, scan stack again if other methods match the request
+ // Moved from app.handler because middleware may break the route chain
+ if c.getMatched() {
+ return false, err
}
- // Optional: Check flash messages
- rawHeaders := ctx.Request().Header.RawHeaders()
- if len(rawHeaders) > 0 && bytes.Contains(rawHeaders, []byte(FlashCookieName)) {
- ctx.Redirect().parseAndClearFlashMessages()
- }
+ exists := false
+ methods := app.config.RequestMethods
+ for i := 0; i < len(methods); i++ {
+ // Skip original method
+ if methodInt == i {
+ continue
+ }
+ // Reset stack index
+ indexRoute := -1
- // Attempt to match a route and execute the chain
- _, err := app.next(ctx)
- if err != nil {
- if catch := ctx.App().ErrorHandler(ctx, err); catch != nil {
- _ = ctx.SendStatus(StatusInternalServerError) //nolint:errcheck // Always return nil
+ tree, ok := app.treeStack[i][treeHash]
+ if !ok {
+ tree = app.treeStack[i][0]
}
- // TODO: Do we need to return here?
+ // Get stack length
+ lenr := len(tree) - 1
+ // Loop over the route stack starting from previous index
+ for indexRoute < lenr {
+ // Increment route index
+ indexRoute++
+ // Get *Route
+ route := tree[indexRoute]
+ // Skip use routes
+ if route.use {
+ continue
+ }
+ // Check if it matches the request path
+ // No match, next route
+ if route.match(c.getDetectionPath(), c.Path(), c.getValues()) {
+ // We matched
+ exists = true
+ // Add method to Allow header
+ c.Append(HeaderAllow, methods[i])
+ // Break stack loop
+ break
+ }
+ }
+ c.setIndexRoute(indexRoute)
}
-}
-
-func (app *App) customRequestHandler(rctx *fasthttp.RequestCtx) {
- // Acquire CustomCtx from the pool
- ctx, ok := app.AcquireCtx(rctx).(CustomCtx)
- if !ok {
- panic(errors.New("requestHandler: failed to type-assert to CustomCtx"))
+ if exists {
+ err = ErrMethodNotAllowed
}
+ return false, err
+}
+func (app *App) requestHandler(rctx *fasthttp.RequestCtx) {
+ // Acquire context from the pool
+ ctx := app.AcquireCtx(rctx)
defer app.ReleaseCtx(ctx)
- // Check if the HTTP method is valid
- if app.methodInt(ctx.Method()) == -1 {
- _ = ctx.SendStatus(StatusNotImplemented) //nolint:errcheck // Always return nil
- return
- }
+ var err error
+ // Attempt to match a route and execute the chain
+ if d, isDefault := ctx.(*DefaultCtx); isDefault {
+ // Check if the HTTP method is valid
+ if d.methodInt == -1 {
+ _ = d.SendStatus(StatusNotImplemented) //nolint:errcheck // Always return nil
+ return
+ }
- // Optional: Check flash messages
- rawHeaders := ctx.Request().Header.RawHeaders()
- if len(rawHeaders) > 0 && bytes.Contains(rawHeaders, []byte(FlashCookieName)) {
- ctx.Redirect().parseAndClearFlashMessages()
- }
+ // Optional: Check flash messages
+ rawHeaders := d.Request().Header.RawHeaders()
+ if len(rawHeaders) > 0 && bytes.Contains(rawHeaders, []byte(FlashCookieName)) {
+ d.Redirect().parseAndClearFlashMessages()
+ }
+ _, err = app.next(d)
+ } else {
+ // Check if the HTTP method is valid
+ if ctx.getMethodInt() == -1 {
+ _ = ctx.SendStatus(StatusNotImplemented) //nolint:errcheck // Always return nil
+ return
+ }
- // Attempt to match a route and execute the chain
- _, err := app.nextCustom(ctx)
+ // Optional: Check flash messages
+ rawHeaders := ctx.Request().Header.RawHeaders()
+ if len(rawHeaders) > 0 && bytes.Contains(rawHeaders, []byte(FlashCookieName)) {
+ ctx.Redirect().parseAndClearFlashMessages()
+ }
+ _, err = app.nextCustom(ctx)
+ }
if err != nil {
if catch := ctx.App().ErrorHandler(ctx, err); catch != nil {
_ = ctx.SendStatus(StatusInternalServerError) //nolint:errcheck // Always return nil
diff --git a/state.go b/state.go
index 36b7ff110fa..786f98177fd 100644
--- a/state.go
+++ b/state.go
@@ -135,205 +135,103 @@ func GetStateWithDefault[T any](s *State, key string, defaultVal T) T {
// GetString retrieves a string value from the State.
// It returns the string and a boolean indicating successful type assertion.
func (s *State) GetString(key string) (string, bool) {
- dep, ok := s.Get(key)
- if ok {
- depString, okCast := dep.(string)
- return depString, okCast
- }
-
- return "", false
+ return GetState[string](s, key)
}
// GetInt retrieves an integer value from the State.
// It returns the int and a boolean indicating successful type assertion.
func (s *State) GetInt(key string) (int, bool) {
- dep, ok := s.Get(key)
- if ok {
- depInt, okCast := dep.(int)
- return depInt, okCast
- }
-
- return 0, false
+ return GetState[int](s, key)
}
// GetBool retrieves a boolean value from the State.
// It returns the bool and a boolean indicating successful type assertion.
func (s *State) GetBool(key string) (value, ok bool) { //nolint:nonamedreturns // Better idea to use named returns here
- dep, ok := s.Get(key)
- if ok {
- depBool, okCast := dep.(bool)
- return depBool, okCast
- }
-
- return false, false
+ return GetState[bool](s, key)
}
// GetFloat64 retrieves a float64 value from the State.
// It returns the float64 and a boolean indicating successful type assertion.
func (s *State) GetFloat64(key string) (float64, bool) {
- dep, ok := s.Get(key)
- if ok {
- depFloat64, okCast := dep.(float64)
- return depFloat64, okCast
- }
-
- return 0, false
+ return GetState[float64](s, key)
}
// GetUint retrieves a uint value from the State.
-// It returns the float64 and a boolean indicating successful type assertion.
+// It returns the uint and a boolean indicating successful type assertion.
func (s *State) GetUint(key string) (uint, bool) {
- dep, ok := s.Get(key)
- if ok {
- if depUint, okCast := dep.(uint); okCast {
- return depUint, true
- }
- }
- return 0, false
+ return GetState[uint](s, key)
}
// GetInt8 retrieves an int8 value from the State.
-// It returns the float64 and a boolean indicating successful type assertion.
+// It returns the int8 and a boolean indicating successful type assertion.
func (s *State) GetInt8(key string) (int8, bool) {
- dep, ok := s.Get(key)
- if ok {
- if depInt8, okCast := dep.(int8); okCast {
- return depInt8, true
- }
- }
- return 0, false
+ return GetState[int8](s, key)
}
// GetInt16 retrieves an int16 value from the State.
-// It returns the float64 and a boolean indicating successful type assertion.
+// It returns the int16 and a boolean indicating successful type assertion.
func (s *State) GetInt16(key string) (int16, bool) {
- dep, ok := s.Get(key)
- if ok {
- if depInt16, okCast := dep.(int16); okCast {
- return depInt16, true
- }
- }
- return 0, false
+ return GetState[int16](s, key)
}
// GetInt32 retrieves an int32 value from the State.
-// It returns the float64 and a boolean indicating successful type assertion.
+// It returns the int32 and a boolean indicating successful type assertion.
func (s *State) GetInt32(key string) (int32, bool) {
- dep, ok := s.Get(key)
- if ok {
- if depInt32, okCast := dep.(int32); okCast {
- return depInt32, true
- }
- }
- return 0, false
+ return GetState[int32](s, key)
}
// GetInt64 retrieves an int64 value from the State.
-// It returns the float64 and a boolean indicating successful type assertion.
+// It returns the int64 and a boolean indicating successful type assertion.
func (s *State) GetInt64(key string) (int64, bool) {
- dep, ok := s.Get(key)
- if ok {
- if depInt64, okCast := dep.(int64); okCast {
- return depInt64, true
- }
- }
- return 0, false
+ return GetState[int64](s, key)
}
// GetUint8 retrieves a uint8 value from the State.
-// It returns the float64 and a boolean indicating successful type assertion.
+// It returns the uint8 and a boolean indicating successful type assertion.
func (s *State) GetUint8(key string) (uint8, bool) {
- dep, ok := s.Get(key)
- if ok {
- if depUint8, okCast := dep.(uint8); okCast {
- return depUint8, true
- }
- }
- return 0, false
+ return GetState[uint8](s, key)
}
// GetUint16 retrieves a uint16 value from the State.
-// It returns the float64 and a boolean indicating successful type assertion.
+// It returns the uint16 and a boolean indicating successful type assertion.
func (s *State) GetUint16(key string) (uint16, bool) {
- dep, ok := s.Get(key)
- if ok {
- if depUint16, okCast := dep.(uint16); okCast {
- return depUint16, true
- }
- }
- return 0, false
+ return GetState[uint16](s, key)
}
// GetUint32 retrieves a uint32 value from the State.
-// It returns the float64 and a boolean indicating successful type assertion.
+// It returns the uint32 and a boolean indicating successful type assertion.
func (s *State) GetUint32(key string) (uint32, bool) {
- dep, ok := s.Get(key)
- if ok {
- if depUint32, okCast := dep.(uint32); okCast {
- return depUint32, true
- }
- }
- return 0, false
+ return GetState[uint32](s, key)
}
// GetUint64 retrieves a uint64 value from the State.
-// It returns the float64 and a boolean indicating successful type assertion.
+// It returns the uint64 and a boolean indicating successful type assertion.
func (s *State) GetUint64(key string) (uint64, bool) {
- dep, ok := s.Get(key)
- if ok {
- if depUint64, okCast := dep.(uint64); okCast {
- return depUint64, true
- }
- }
- return 0, false
+ return GetState[uint64](s, key)
}
// GetUintptr retrieves a uintptr value from the State.
-// It returns the float64 and a boolean indicating successful type assertion.
+// It returns the uintptr and a boolean indicating successful type assertion.
func (s *State) GetUintptr(key string) (uintptr, bool) {
- dep, ok := s.Get(key)
- if ok {
- if depUintptr, okCast := dep.(uintptr); okCast {
- return depUintptr, true
- }
- }
- return 0, false
+ return GetState[uintptr](s, key)
}
// GetFloat32 retrieves a float32 value from the State.
-// It returns the float64 and a boolean indicating successful type assertion.
+// It returns the float32 and a boolean indicating successful type assertion.
func (s *State) GetFloat32(key string) (float32, bool) {
- dep, ok := s.Get(key)
- if ok {
- if depFloat32, okCast := dep.(float32); okCast {
- return depFloat32, true
- }
- }
- return 0, false
+ return GetState[float32](s, key)
}
// GetComplex64 retrieves a complex64 value from the State.
-// It returns the float64 and a boolean indicating successful type assertion.
+// It returns the complex64 and a boolean indicating successful type assertion.
func (s *State) GetComplex64(key string) (complex64, bool) {
- dep, ok := s.Get(key)
- if ok {
- if depComplex64, okCast := dep.(complex64); okCast {
- return depComplex64, true
- }
- }
- return 0, false
+ return GetState[complex64](s, key)
}
// GetComplex128 retrieves a complex128 value from the State.
-// It returns the float64 and a boolean indicating successful type assertion.
+// It returns the complex128 and a boolean indicating successful type assertion.
func (s *State) GetComplex128(key string) (complex128, bool) {
- dep, ok := s.Get(key)
- if ok {
- if depComplex128, okCast := dep.(complex128); okCast {
- return depComplex128, true
- }
- }
- return 0, false
+ return GetState[complex128](s, key)
}
// serviceKey returns a key for a service in the State.