Files
fiber/redirect.go
zeekayandHanzo Dev f60e254acb module: rename to github.com/zap-proto/fiber/v3
Fork of gofiber/fiber v3 based on the v3.2.0 tag. Rewrites all self-imports
github.com/gofiber/fiber/v3 -> github.com/zap-proto/fiber/v3. Upstream
gofiber/schema and gofiber/utils/v2 dependencies are left untouched.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-10 12:49:35 -07:00

434 lines
9.8 KiB
Go

// ⚡️ Fiber is an Express inspired web framework written in Go with ☕️
// 📝 GitHub Repository: https://github.com/gofiber/fiber
// 📌 API Documentation: https://docs.gofiber.io
package fiber
import (
"bytes"
"encoding/hex"
"sync"
"github.com/gofiber/utils/v2"
utilsbytes "github.com/gofiber/utils/v2/bytes"
"github.com/valyala/bytebufferpool"
"github.com/valyala/fasthttp"
"github.com/zap-proto/fiber/v3/binder"
)
// Pool for redirection
var (
redirectPool = sync.Pool{
New: func() any {
return &Redirect{
status: StatusSeeOther,
messages: make(redirectionMsgs, 0),
}
},
}
oldInputPool = sync.Pool{
New: func() any {
return make(map[string]string)
},
}
)
const maxPoolableMapSize = 64
// FlashCookieName Cookie name to send flash messages when to use redirection.
const (
FlashCookieName = "fiber_flash"
)
var flashCookieNeedle = []byte(FlashCookieName + "=")
// hasFlashCookie is on the request hot path and runs on every request/response cycle.
// Keep this cheap for users who don't use flash messages:
// 1) a fast raw-header prefilter to avoid unnecessary cookie parsing,
// 2) an exact cookie lookup to avoid prefix false positives (e.g. fiber_flashX).
func hasFlashCookie(header *fasthttp.RequestHeader) bool {
rawHeaders := header.RawHeaders()
if len(rawHeaders) == 0 {
return false
}
if !bytes.Contains(rawHeaders, flashCookieNeedle) {
return false
}
return header.Cookie(FlashCookieName) != nil
}
// redirectionMsgs is a struct that used to store flash messages and old input data in cookie using MSGP.
// msgp -file="redirect.go" -o="redirect_msgp.go" -unexported
// Cookie payloads are limited to ~4KB, so keep flash message counts bounded but usable.
//
//msgp:limit arrays:256 maps:32 marshal:true
//msgp:ignore Redirect RedirectConfig OldInputData FlashMessage
type redirectionMsg struct {
key string
value string
level uint8
isOldInput bool
}
type redirectionMsgs []redirectionMsg
// OldInputData is a struct that holds the old input data.
type OldInputData struct {
Key string
Value string
}
// FlashMessage is a struct that holds the flash message data.
type FlashMessage struct {
Key string
Value string
Level uint8
}
// Redirect is a struct that holds the redirect data.
type Redirect struct {
c *DefaultCtx // Embed ctx
messages redirectionMsgs // Flash messages and old input data
status int // Status code of redirection. Default: 303 StatusSeeOther
}
// RedirectConfig A config to use with Redirect().Route()
// You can specify queries or route parameters.
// NOTE: We don't use net/url to parse parameters because of it has poor performance. You have to pass map.
type RedirectConfig struct {
Params Map // Route parameters
Queries map[string]string // Query map
}
// AcquireRedirect return default Redirect reference from the redirect pool
func AcquireRedirect() *Redirect {
redirect, ok := redirectPool.Get().(*Redirect)
if !ok {
panic(errRedirectTypeAssertion)
}
return redirect
}
// ReleaseRedirect returns c acquired via Redirect to redirect pool.
//
// It is forbidden accessing req and/or its members after returning
// it to redirect pool.
func ReleaseRedirect(r *Redirect) {
r.release()
redirectPool.Put(r)
}
func (r *Redirect) release() {
r.status = StatusSeeOther
r.messages = r.messages[:0]
r.c = nil
}
func acquireOldInput() map[string]string {
oldInput, ok := oldInputPool.Get().(map[string]string)
if !ok {
return make(map[string]string)
}
return oldInput
}
func releaseOldInput(oldInput map[string]string) {
if len(oldInput) > maxPoolableMapSize {
return
}
clear(oldInput)
oldInputPool.Put(oldInput)
}
// Status sets the status code of redirection.
// If status is not specified, status defaults to 303 See Other.
func (r *Redirect) Status(code int) *Redirect {
r.status = code
return r
}
// With You can send flash messages by using With().
// They will be sent as a cookie.
// You can get them by using: Redirect().Messages(), Redirect().Message()
// Note: You must use escape char before using ',' and ':' chars to avoid wrong parsing.
func (r *Redirect) With(key, value string, level ...uint8) *Redirect {
// Get level
var msgLevel uint8
if len(level) > 0 {
msgLevel = level[0]
}
// Override old message if exists
for i, msg := range r.messages {
if msg.key == key && !msg.isOldInput {
r.messages[i].value = value
r.messages[i].level = msgLevel
return r
}
}
r.messages = append(r.messages, redirectionMsg{
key: key,
value: value,
level: msgLevel,
})
return r
}
// WithInput You can send input data by using WithInput().
// They will be sent as a cookie.
// This method can send form, multipart form, query data to redirected route.
// You can get them by using: Redirect().OldInputs(), Redirect().OldInput()
func (r *Redirect) WithInput() *Redirect {
// Get content-type
ctype := utils.UnsafeString(utilsbytes.UnsafeToLower(r.c.RequestCtx().Request.Header.ContentType()))
ctype = binder.FilterFlags(utils.ParseVendorSpecificContentType(ctype))
oldInput := acquireOldInput()
defer releaseOldInput(oldInput)
switch ctype {
case MIMEApplicationForm, MIMEMultipartForm:
_ = r.c.Bind().Form(oldInput) //nolint:errcheck // not needed
default:
_ = r.c.Bind().Query(oldInput) //nolint:errcheck // not needed
}
// Add old input data
for k, v := range oldInput {
r.messages = append(r.messages, redirectionMsg{
key: k,
value: v,
isOldInput: true,
})
}
return r
}
// Messages Get flash messages.
func (r *Redirect) Messages() []FlashMessage {
if len(r.c.flashMessages) == 0 {
return nil
}
flashMessages := make([]FlashMessage, 0, len(r.c.flashMessages))
writeIdx := 0
for _, msg := range r.c.flashMessages {
if msg.isOldInput {
r.c.flashMessages[writeIdx] = msg
writeIdx++
continue
}
flashMessages = append(flashMessages, FlashMessage{
Key: msg.key,
Value: msg.value,
Level: msg.level,
})
}
for i := writeIdx; i < len(r.c.flashMessages); i++ {
r.c.flashMessages[i] = redirectionMsg{}
}
r.c.flashMessages = r.c.flashMessages[:writeIdx]
return flashMessages
}
// Message Get flash message by key.
func (r *Redirect) Message(key string) FlashMessage {
if len(r.c.flashMessages) == 0 {
return FlashMessage{}
}
var flashMessage FlashMessage
found := false
writeIdx := 0
for _, msg := range r.c.flashMessages {
if msg.isOldInput || found || msg.key != key {
r.c.flashMessages[writeIdx] = msg
writeIdx++
continue
}
flashMessage = FlashMessage{
Key: msg.key,
Value: msg.value,
Level: msg.level,
}
found = true
}
for i := writeIdx; i < len(r.c.flashMessages); i++ {
r.c.flashMessages[i] = redirectionMsg{}
}
r.c.flashMessages = r.c.flashMessages[:writeIdx]
return flashMessage
}
// OldInputs Get old input data.
func (r *Redirect) OldInputs() []OldInputData {
// Count old inputs first to avoid allocation if none exist
count := 0
for _, msg := range r.c.flashMessages {
if msg.isOldInput {
count++
}
}
if count == 0 {
return nil
}
inputs := make([]OldInputData, 0, count)
for _, msg := range r.c.flashMessages {
if msg.isOldInput {
inputs = append(inputs, OldInputData{
Key: msg.key,
Value: msg.value,
})
}
}
return inputs
}
// OldInput Get old input data by key.
func (r *Redirect) OldInput(key string) OldInputData {
msgs := r.c.flashMessages
for _, msg := range msgs {
if msg.key == key && msg.isOldInput {
return OldInputData{
Key: msg.key,
Value: msg.value,
}
}
}
return OldInputData{}
}
// To redirect to the URL derived from the specified path, with specified status.
func (r *Redirect) To(location string) error {
r.c.setCanonical(HeaderLocation, location)
r.c.Status(r.status)
r.processFlashMessages()
return nil
}
// Route redirects to the Route registered in the app with appropriate parameters.
// If you want to send queries or params to route, you should use config parameter.
func (r *Redirect) Route(name string, config ...RedirectConfig) error {
// Check config
cfg := RedirectConfig{}
if len(config) > 0 {
cfg = config[0]
}
// Get location from route name
route := r.c.App().GetRoute(name)
location, err := r.c.getLocationFromRoute(&route, cfg.Params)
if err != nil {
return err
}
// Check queries
if len(cfg.Queries) > 0 {
queryText := bytebufferpool.Get()
defer bytebufferpool.Put(queryText)
first := true
for k, v := range cfg.Queries {
if !first {
queryText.WriteByte('&')
}
first = false
queryText.WriteString(k)
queryText.WriteByte('=')
queryText.WriteString(v)
}
return r.To(location + "?" + r.c.app.toString(queryText.Bytes()))
}
return r.To(location)
}
// Back redirect to the URL to referer.
func (r *Redirect) Back(fallback ...string) error {
location := r.c.Get(HeaderReferer)
if location == "" {
// Check fallback URL
if len(fallback) == 0 {
err := ErrRedirectBackNoFallback
r.c.Status(err.Code)
return err
}
location = fallback[0]
}
return r.To(location)
}
// parseAndClearFlashMessages is a method to get flash messages before they are getting removed
func (r *Redirect) parseAndClearFlashMessages() {
// parse flash messages
cookieValue, err := hex.DecodeString(r.c.Cookies(FlashCookieName))
if err != nil {
return
}
_, err = r.c.flashMessages.UnmarshalMsg(cookieValue)
if err != nil {
return
}
r.c.Cookie(&Cookie{
Name: FlashCookieName,
Value: "",
Path: "/",
MaxAge: -1,
})
}
// processFlashMessages is a helper function to process flash messages and old input data
// and set them as cookies
func (r *Redirect) processFlashMessages() {
if len(r.messages) == 0 {
return
}
val, err := r.messages.MarshalMsg(nil)
if err != nil {
return
}
dst := make([]byte, hex.EncodedLen(len(val)))
hex.Encode(dst, val)
r.c.Cookie(&Cookie{
Name: FlashCookieName,
Value: r.c.app.toString(dst),
SessionOnly: true,
})
}