From d801659cf99142c6349b99a84f08eeb5ac1edca5 Mon Sep 17 00:00:00 2001 From: Nicholas Jackson Date: Sun, 21 Apr 2024 11:52:36 -0700 Subject: [PATCH] feat: Create Ctx.Res API --- app_test.go | 4 +- bind.go | 2 +- bind_test.go | 54 +-- client/response_test.go | 2 +- ctx.go | 532 +++------------------- ctx_interface.go | 16 +- ctx_test.go | 290 ++++++------ middleware/cache/cache.go | 22 +- middleware/cache/cache_test.go | 10 +- middleware/encryptcookie/encryptcookie.go | 6 +- middleware/etag/etag.go | 10 +- middleware/filesystem/filesystem.go | 12 +- middleware/idempotency/idempotency.go | 4 +- middleware/limiter/limiter_fixed.go | 4 +- middleware/limiter/limiter_sliding.go | 4 +- middleware/logger/default_logger.go | 4 +- middleware/logger/logger_test.go | 2 +- middleware/logger/tags.go | 10 +- middleware/proxy/proxy.go | 4 +- middleware/proxy/proxy_test.go | 4 +- middleware/session/session.go | 10 +- middleware/session/session_test.go | 14 +- middleware/session/store.go | 2 +- redirect.go | 20 +- redirect_test.go | 48 +- request.go | 6 +- response.go | 528 +++++++++++++++++++++ 27 files changed, 884 insertions(+), 740 deletions(-) create mode 100644 response.go diff --git a/app_test.go b/app_test.go index 13c04987420..892ef1192e4 100644 --- a/app_test.go +++ b/app_test.go @@ -981,8 +981,8 @@ func Test_App_Static_Custom_CacheControl(t *testing.T) { app := New() app.Static("/", "./.github", Static{ModifyResponse: func(c Ctx) error { - if strings.Contains(c.GetRespHeader("Content-Type"), "text/html") { - c.Response().Header.Set("Cache-Control", "no-cache, no-store, must-revalidate") + if strings.Contains(c.Res().Get("Content-Type"), "text/html") { + c.Set("Cache-Control", "no-cache, no-store, must-revalidate") } return nil }}) diff --git a/bind.go b/bind.go index 81816580389..be1eaadcb0a 100644 --- a/bind.go +++ b/bind.go @@ -85,7 +85,7 @@ func (b *Bind) Header(out any) error { // RespHeader binds the response header strings into the struct, map[string]string and map[string][]string. func (b *Bind) RespHeader(out any) error { - if err := b.returnErr(binder.RespHeaderBinder.Bind(b.ctx.Response(), out)); err != nil { + if err := b.returnErr(binder.RespHeaderBinder.Bind(&b.ctx.Context().Response, out)); err != nil { return err } diff --git a/bind_test.go b/bind_test.go index 8c7d7709c00..ac839b020a2 100644 --- a/bind_test.go +++ b/bind_test.go @@ -540,21 +540,21 @@ func Test_Bind_RespHeader(t *testing.T) { c.Context().Request.SetBody([]byte(``)) c.Context().Request.Header.SetContentType("") - c.Response().Header.Add("id", "1") - c.Response().Header.Add("Name", "John Doe") - c.Response().Header.Add("Hobby", "golang,fiber") + c.Context().Response.Header.Add("id", "1") + c.Context().Response.Header.Add("Name", "John Doe") + c.Context().Response.Header.Add("Hobby", "golang,fiber") q := new(Header) require.NoError(t, c.Bind().RespHeader(q)) require.Len(t, q.Hobby, 2) - c.Response().Header.Del("hobby") - c.Response().Header.Add("Hobby", "golang,fiber,go") + c.Context().Response.Header.Del("hobby") + c.Context().Response.Header.Add("Hobby", "golang,fiber,go") q = new(Header) require.NoError(t, c.Bind().RespHeader(q)) require.Len(t, q.Hobby, 3) empty := new(Header) - c.Response().Header.Del("hobby") + c.Context().Response.Header.Del("hobby") require.NoError(t, c.Bind().Query(empty)) require.Empty(t, empty.Hobby) @@ -569,13 +569,13 @@ func Test_Bind_RespHeader(t *testing.T) { No []int64 } - c.Response().Header.Add("id", "2") - c.Response().Header.Add("Name", "Jane Doe") - c.Response().Header.Del("hobby") - c.Response().Header.Add("Hobby", "go,fiber") - c.Response().Header.Add("favouriteDrinks", "milo,coke,pepsi") - c.Response().Header.Add("alloc", "") - c.Response().Header.Add("no", "1") + c.Context().Response.Header.Add("id", "2") + c.Context().Response.Header.Add("Name", "Jane Doe") + c.Context().Response.Header.Del("hobby") + c.Context().Response.Header.Add("Hobby", "go,fiber") + c.Context().Response.Header.Add("favouriteDrinks", "milo,coke,pepsi") + c.Context().Response.Header.Add("alloc", "") + c.Context().Response.Header.Add("no", "1") h2 := new(Header2) h2.Bool = true @@ -594,7 +594,7 @@ func Test_Bind_RespHeader(t *testing.T) { Name string `respHeader:"name,required"` } rh := new(RequiredHeader) - c.Response().Header.Del("name") + c.Context().Response.Header.Del("name") require.Equal(t, "name is empty", c.Bind().RespHeader(rh).Error()) } @@ -608,21 +608,21 @@ func Test_Bind_RespHeader_Map(t *testing.T) { c.Context().Request.SetBody([]byte(``)) c.Context().Request.Header.SetContentType("") - c.Response().Header.Add("id", "1") - c.Response().Header.Add("Name", "John Doe") - c.Response().Header.Add("Hobby", "golang,fiber") + c.Context().Response.Header.Add("id", "1") + c.Context().Response.Header.Add("Name", "John Doe") + c.Context().Response.Header.Add("Hobby", "golang,fiber") q := make(map[string][]string, 0) require.NoError(t, c.Bind().RespHeader(&q)) require.Len(t, q["Hobby"], 2) - c.Response().Header.Del("hobby") - c.Response().Header.Add("Hobby", "golang,fiber,go") + c.Context().Response.Header.Del("hobby") + c.Context().Response.Header.Add("Hobby", "golang,fiber,go") q = make(map[string][]string, 0) require.NoError(t, c.Bind().RespHeader(&q)) require.Len(t, q["Hobby"], 3) empty := make(map[string][]string, 0) - c.Response().Header.Del("hobby") + c.Context().Response.Header.Del("hobby") require.NoError(t, c.Bind().Query(&empty)) require.Empty(t, empty["Hobby"]) } @@ -789,9 +789,9 @@ func Benchmark_Bind_RespHeader(b *testing.B) { c.Context().Request.SetBody([]byte(``)) c.Context().Request.Header.SetContentType("") - c.Response().Header.Add("id", "1") - c.Response().Header.Add("Name", "John Doe") - c.Response().Header.Add("Hobby", "golang,fiber") + c.Context().Response.Header.Add("id", "1") + c.Context().Response.Header.Add("Name", "John Doe") + c.Context().Response.Header.Add("Hobby", "golang,fiber") q := new(ReqHeader) b.ReportAllocs() @@ -811,9 +811,9 @@ func Benchmark_Bind_RespHeader_Map(b *testing.B) { c.Context().Request.SetBody([]byte(``)) c.Context().Request.Header.SetContentType("") - c.Response().Header.Add("id", "1") - c.Response().Header.Add("Name", "John Doe") - c.Response().Header.Add("Hobby", "golang,fiber") + c.Context().Response.Header.Add("id", "1") + c.Context().Response.Header.Add("Name", "John Doe") + c.Context().Response.Header.Add("Hobby", "golang,fiber") q := make(map[string][]string) b.ReportAllocs() @@ -1540,7 +1540,7 @@ func Test_Bind_Must(t *testing.T) { rq := new(RequiredQuery) c.Context().URI().SetQueryString("") err := c.Bind().Must().Query(rq) - require.Equal(t, StatusBadRequest, c.Response().StatusCode()) + require.Equal(t, StatusBadRequest, c.Context().Response.StatusCode()) require.Equal(t, "Bad request: name is empty", err.Error()) } diff --git a/client/response_test.go b/client/response_test.go index ab22ab388a8..b370e016ac5 100644 --- a/client/response_test.go +++ b/client/response_test.go @@ -182,7 +182,7 @@ func Test_Response_Header(t *testing.T) { server := startTestServer(t, func(app *fiber.App) { app.Get("/", func(c fiber.Ctx) error { - c.Response().Header.Add("foo", "bar") + c.Set("foo", "bar") return c.SendString("helo world") }) }) diff --git a/ctx.go b/ctx.go index 4b1a0399058..96e5fee167d 100644 --- a/ctx.go +++ b/ctx.go @@ -12,11 +12,7 @@ import ( "io" "mime/multipart" "net" - "path/filepath" "strconv" - "strings" - "sync" - "text/template" "time" "github.com/gofiber/utils/v2" @@ -40,17 +36,15 @@ type contextKey int const userContextKey contextKey = 0 // __local_user_context__ type DefaultCtx struct { - app *App // Reference to *App - req Request // Reference to *Request - // res *Response // Reference to *Response - indexRoute int // Index of the current route - indexHandler int // Index of the current handler - fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx - matched bool // Non use route matched - viewBindMap sync.Map // Default view map to bind template engine - bind *Bind // Default bind reference - redirect *Redirect // Default redirect reference - redirectionMessages []string // Messages of the previous redirect + app *App // Reference to *App + req Request // Reference to *Request + res Response // Reference to *Response + indexRoute int // Index of the current route + indexHandler int // Index of the current handler + fasthttp *fasthttp.RequestCtx // Reference to *fasthttp.RequestCtx + matched bool // Non use route matched + bind *Bind // Default bind reference + redirect *Redirect // Default redirect reference } // TLSHandler object @@ -98,12 +92,6 @@ type Views interface { Render(out io.Writer, name string, binding any, layout ...string) error } -// ResFmt associates a Content Type to a fiber.Handler for c.Format -type ResFmt struct { - MediaType string - Handler func(Ctx) error -} - // Accepts is an alias of [Request.Accepts] func (c *DefaultCtx) Accepts(offers ...string) string { return c.req.Accepts(offers...) @@ -129,37 +117,14 @@ func (c *DefaultCtx) App() *App { return c.app } -// Append the specified value to the HTTP response header field. -// If the header is not already set, it creates the header with the specified value. +// Append is an alias of [Response.Append]. func (c *DefaultCtx) Append(field string, values ...string) { - if len(values) == 0 { - return - } - h := c.app.getString(c.fasthttp.Response.Header.Peek(field)) - originalH := h - for _, value := range values { - if len(h) == 0 { - h = value - } else if h != value && !strings.HasPrefix(h, value+",") && !strings.HasSuffix(h, " "+value) && - !strings.Contains(h, " "+value+",") { - h += ", " + value - } - } - if originalH != h { - c.Set(field, h) - } + c.res.Append(field, values...) } -// Attachment sets the HTTP response Content-Disposition header field to attachment. +// Attachment is an alias of [Response.Attachment]. func (c *DefaultCtx) Attachment(filename ...string) { - if len(filename) > 0 { - fname := filepath.Base(filename[0]) - c.Type(filepath.Ext(fname)) - - c.setCanonical(HeaderContentDisposition, `attachment; filename="`+c.app.quoteString(fname)+`"`) - return - } - c.setCanonical(HeaderContentDisposition, "attachment") + c.res.Attachment(filename...) } // BaseURL is an alias of [Request.BaseURL]. @@ -177,18 +142,9 @@ func (c *DefaultCtx) Body() []byte { return c.req.Body() } -// ClearCookie expires a specific cookie by key on the client side. -// If no key is provided it expires all cookies that came with the request. +// ClearCookie is an alias of [Response.ClearCookie]. func (c *DefaultCtx) ClearCookie(key ...string) { - if len(key) > 0 { - for i := range key { - c.fasthttp.Response.Header.DelClientCookie(key[i]) - } - return - } - c.fasthttp.Request.Header.VisitAllCookie(func(k, _ []byte) { - c.fasthttp.Response.Header.DelClientCookieBytes(k) - }) + c.res.ClearCookie(key...) } // Context returns *fasthttp.RequestCtx that carries a deadline @@ -214,36 +170,9 @@ func (c *DefaultCtx) SetUserContext(ctx context.Context) { c.fasthttp.SetUserValue(userContextKey, ctx) } -// Cookie sets a cookie by passing a cookie struct. +// Cookie is an alias of [Response.Cookie]. 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) - } - fcookie.SetSecure(cookie.Secure) - fcookie.SetHTTPOnly(cookie.HTTPOnly) - - switch utils.ToLower(cookie.SameSite) { - case CookieSameSiteStrictMode: - fcookie.SetSameSite(fasthttp.CookieSameSiteStrictMode) - case CookieSameSiteNoneMode: - fcookie.SetSameSite(fasthttp.CookieSameSiteNoneMode) - case CookieSameSiteDisabled: - fcookie.SetSameSite(fasthttp.CookieSameSiteDisabled) - default: - fcookie.SetSameSite(fasthttp.CookieSameSiteLaxMode) - } - - c.fasthttp.Response.Header.SetCookie(fcookie) - fasthttp.ReleaseCookie(fcookie) + c.res.Cookie(cookie) } // Cookies is an alias of [Request.Cookies] @@ -251,28 +180,21 @@ func (c *DefaultCtx) Cookies(key string, defaultValue ...string) string { return c.req.Cookies(key, defaultValue...) } -// 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 is an alias of [Response.Download]. func (c *DefaultCtx) Download(file string, filename ...string) error { - var fname string - if len(filename) > 0 { - fname = filename[0] - } else { - fname = filepath.Base(file) - } - c.setCanonical(HeaderContentDisposition, `attachment; filename="`+c.app.quoteString(fname)+`"`) - return c.SendFile(file) + return c.res.Download(file, filename...) } -// Req return the *fasthttp.Req object -// This allows you to use all fasthttp request methods -// https://godoc.org/github.com/valyala/fasthttp#Req +// Req returns the Request object for the current context. func (c *DefaultCtx) Req() *Request { return &c.req } +// Res returns the Response object for the current context. +func (c *DefaultCtx) Res() *Response { + return &c.res +} + // Response return the *fasthttp.Response object // This allows you to use all fasthttp response methods // https://godoc.org/github.com/valyala/fasthttp#Response @@ -280,54 +202,9 @@ func (c *DefaultCtx) Response() *fasthttp.Response { return &c.fasthttp.Response } -// Format performs content-negotiation on the Accept HTTP header. -// It uses Accepts to select a proper format and calls the matching -// user-provided handler function. -// If no accepted format is found, and a format with MediaType "default" is given, -// that default handler is called. If no format is found and no default is given, -// StatusNotAcceptable is sent. +// Format is an alias of [Response.Format] func (c *DefaultCtx) Format(handlers ...ResFmt) error { - if len(handlers) == 0 { - return ErrNoHandlers - } - - c.Vary(HeaderAccept) - - if c.Get(HeaderAccept) == "" { - c.Response().Header.SetContentType(handlers[0].MediaType) - return handlers[0].Handler(c) - } - - // Using an int literal as the slice capacity allows for the slice to be - // allocated on the stack. The number was chosen arbitrarily as an - // approximation of the maximum number of content types a user might handle. - // If the user goes over, it just causes allocations, so it's not a problem. - types := make([]string, 0, 8) - var defaultHandler Handler - for _, h := range handlers { - if h.MediaType == "default" { - defaultHandler = h.Handler - continue - } - types = append(types, h.MediaType) - } - accept := c.Accepts(types...) - - if accept == "" { - if defaultHandler == nil { - return c.SendStatus(StatusNotAcceptable) - } - return defaultHandler(c) - } - - for _, h := range handlers { - if h.MediaType == accept { - c.Response().Header.SetContentType(h.MediaType) - return h.Handler(c) - } - } - - return fmt.Errorf("%w: format: an Accept was found but no handler was called", errUnreachable) + return c.res.Format(handlers...) } // AutoFormat performs content-negotiation on the Accept HTTP header. @@ -397,12 +274,9 @@ func GetReqHeader[V GenericType](c Ctx, key string, defaultValue ...V) V { return genericParseType[V](c.Req().Get(key), v, defaultValue...) } -// GetRespHeader returns the HTTP response header specified by field. -// Field names are case-insensitive -// Returned value is only valid within the handler. Do not store any references. -// Make copies or use the Immutable setting instead. +// GetRespHeader is an alias of [Response.Get]. func (c *DefaultCtx) GetRespHeader(key string, defaultValue ...string) string { - return defaultString(c.app.getString(c.fasthttp.Response.Header.Peek(key)), defaultValue) + return c.res.Get(key, defaultValue...) } // GetRespHeaders returns the HTTP response headers. @@ -463,80 +337,24 @@ func (c *DefaultCtx) Is(extension string) bool { return c.req.Is(extension) } -// JSON converts any interface or string to JSON. -// Array and slice values encode as JSON arrays, -// except that []byte encodes as a base64-encoded string, -// and a nil slice encodes as the null JSON value. -// If the ctype parameter is given, this method will set the -// Content-Type header equal to ctype. If ctype is not given, -// The Content-Type header will be set to application/json. +// JSON is an alias of [Response.JSON]. func (c *DefaultCtx) JSON(data any, ctype ...string) error { - raw, err := c.app.config.JSONEncoder(data) - if err != nil { - return err - } - c.fasthttp.Response.SetBodyRaw(raw) - if len(ctype) > 0 { - c.fasthttp.Response.Header.SetContentType(ctype[0]) - } else { - c.fasthttp.Response.Header.SetContentType(MIMEApplicationJSON) - } - return nil + return c.res.JSON(data, ctype...) } -// JSONP sends a JSON response with JSONP support. -// This method is identical to JSON, except that it opts-in to JSONP callback support. -// By default, the callback name is simply callback. +// JSONP is an alias of [Response.JSONP]. func (c *DefaultCtx) JSONP(data any, callback ...string) error { - raw, err := c.app.config.JSONEncoder(data) - if err != nil { - return err - } - - var result, cb string - - if len(callback) > 0 { - cb = callback[0] - } else { - cb = "callback" - } - - result = cb + "(" + c.app.getString(raw) + ");" - - c.setCanonical(HeaderXContentTypeOptions, "nosniff") - c.fasthttp.Response.Header.SetContentType(MIMETextJavaScriptCharsetUTF8) - return c.SendString(result) + return c.res.JSONP(data, callback...) } -// XML converts any interface or string to XML. -// This method also sets the content header to application/xml. +// XML is an alias of [Response.XML]. func (c *DefaultCtx) XML(data any) error { - raw, err := c.app.config.XMLEncoder(data) - if err != nil { - return err - } - c.fasthttp.Response.SetBodyRaw(raw) - c.fasthttp.Response.Header.SetContentType(MIMEApplicationXML) - return nil + return c.res.XML(data) } -// Links joins the links followed by the property to populate the response's Link HTTP header field. +// Links is an alias of [Response.Links]. func (c *DefaultCtx) Links(link ...string) { - if len(link) == 0 { - return - } - bb := bytebufferpool.Get() - for i := range link { - if i%2 == 0 { - bb.WriteByte('<') - bb.WriteString(link[i]) - bb.WriteByte('>') - } else { - bb.WriteString(`; rel="` + link[i] + `",`) - } - } - c.setCanonical(HeaderLink, strings.TrimRight(c.app.getString(bb.Bytes()), ",")) - bytebufferpool.Put(bb) + c.res.Links(link...) } // Locals makes it possible to pass any values under keys scoped to the request @@ -565,14 +383,14 @@ func Locals[V any](c Ctx, key any, value ...V) V { return v } -// Location sets the response Location HTTP header to the specified path parameter. +// Location is an alias of [Response.Location]. func (c *DefaultCtx) Location(path string) { - c.setCanonical(HeaderLocation, path) + c.res.Location(path) } -// Method is an alias of [Request.Method] +// Method is an alias of [Request.Method]. func (c *DefaultCtx) Method(override ...string) string { - return c.Req().Method(override...) + return c.req.Method(override...) } // MultipartForm parse form entries from binary. @@ -725,14 +543,9 @@ func (c *DefaultCtx) Redirect() *Redirect { return c.redirect } -// ViewBind Add vars to default view var map binding to template engine. -// Variables are read by the Render method and may be overwritten. +// ViewBind is an alias of [Response.ViewBind]. func (c *DefaultCtx) ViewBind(vars Map) error { - // init viewBindMap - lazy map - for k, v := range vars { - c.viewBindMap.Store(k, v) - } - return nil + return c.res.ViewBind(vars) } // getLocationFromRoute get URL location from route using parameters @@ -769,100 +582,9 @@ func (c *DefaultCtx) GetRouteURL(routeName string, params Map) (string, error) { return c.getLocationFromRoute(c.App().GetRoute(routeName), params) } -// Render a template with data and sends a text/html response. -// We support the following engines: https://github.com/gofiber/template +// Render is an alias of [Response.Render]. func (c *DefaultCtx) Render(name string, bind Map, layouts ...string) error { - // Get new buffer from pool - buf := bytebufferpool.Get() - defer bytebufferpool.Put(buf) - - // Initialize empty bind map if bind is nil - if bind == nil { - bind = make(Map) - } - - // Pass-locals-to-views, bind, appListKeys - c.renderExtensions(bind) - - var rendered bool - for i := len(c.app.mountFields.appListKeys) - 1; i >= 0; i-- { - prefix := c.app.mountFields.appListKeys[i] - app := c.app.mountFields.appList[prefix] - if prefix == "" || strings.Contains(c.OriginalURL(), prefix) { - if len(layouts) == 0 && app.config.ViewsLayout != "" { - layouts = []string{ - app.config.ViewsLayout, - } - } - - // Render template from Views - if app.config.Views != nil { - if err := app.config.Views.Render(buf, name, bind, layouts...); err != nil { - return fmt.Errorf("failed to render: %w", err) - } - - rendered = true - break - } - } - } - - if !rendered { - // Render raw template using 'name' as filepath if no engine is set - var tmpl *template.Template - if _, err := readContent(buf, name); err != nil { - return err - } - // Parse template - tmpl, err := template.New("").Parse(c.app.getString(buf.Bytes())) - if err != nil { - return fmt.Errorf("failed to parse: %w", err) - } - buf.Reset() - // Render template - if err := tmpl.Execute(buf, bind); err != nil { - return fmt.Errorf("failed to execute: %w", err) - } - } - - // Set Content-Type to text/html - c.fasthttp.Response.Header.SetContentType(MIMETextHTMLCharsetUTF8) - // Set rendered template to body - c.fasthttp.Response.SetBody(buf.Bytes()) - - return nil -} - -func (c *DefaultCtx) renderExtensions(bind any) { - if bindMap, ok := bind.(Map); ok { - // Bind view map - c.viewBindMap.Range(func(key, value any) bool { - keyValue, ok := key.(string) - if !ok { - return true - } - if _, ok := bindMap[keyValue]; !ok { - bindMap[keyValue] = value - } - return true - }) - - // Check if the PassLocalsToViews option is enabled (by default it is disabled) - if c.app.config.PassLocalsToViews { - // Loop through each local and set it in the map - c.fasthttp.VisitUserValues(func(key []byte, val any) { - // check if bindMap doesn't contain the key - if _, ok := bindMap[c.app.getString(key)]; !ok { - // Set the key and value in the bindMap - bindMap[c.app.getString(key)] = val - } - }) - } - } - - if len(c.app.mountFields.appListKeys) == 0 { - c.app.generateAppListKeys() - } + return c.res.Render(name, bind, layouts...) } // Route is an alias of [Request.Route]. @@ -894,117 +616,29 @@ func (*DefaultCtx) SaveFileToStorage(fileheader *multipart.FileHeader, path stri return nil } -// Secure returns whether a secure connection was established. +// Secure is an alias of [Request.Secure]. func (c *DefaultCtx) Secure() bool { - return c.Protocol() == schemeHTTPS + return c.req.Secure() } -// Send sets the HTTP response body without copying it. -// From this point onward the body argument must not be changed. +// Send is an alias of [Response.Send]. func (c *DefaultCtx) Send(body []byte) error { - // Write response body - c.fasthttp.Response.SetBodyRaw(body) - return nil + return c.res.Send(body) } -var ( - sendFileOnce sync.Once - sendFileFS *fasthttp.FS - sendFileHandler fasthttp.RequestHandler -) - -// SendFile transfers the file from the given path. -// The file is not compressed by default, enable this by passing a 'true' argument -// Sets the Content-Type response HTTP header field based on the filenames extension. +// SendFile is an alias of [Response.SendFile]. func (c *DefaultCtx) SendFile(file string, compress ...bool) error { - // Save the filename, we will need it in the error message if the file isn't found - filename := file - - // https://github.com/valyala/fasthttp/blob/c7576cc10cabfc9c993317a2d3f8355497bea156/fs.go#L129-L134 - sendFileOnce.Do(func() { - const cacheDuration = 10 * time.Second - sendFileFS = &fasthttp.FS{ - Root: "", - AllowEmptyRoot: true, - GenerateIndexPages: false, - AcceptByteRange: true, - Compress: true, - CompressedFileSuffix: c.app.config.CompressedFileSuffix, - CacheDuration: cacheDuration, - IndexNames: []string{"index.html"}, - PathNotFound: func(ctx *fasthttp.RequestCtx) { - ctx.Response.SetStatusCode(StatusNotFound) - }, - } - sendFileHandler = sendFileFS.NewRequestHandler() - }) - - // Keep original path for mutable params - c.req.pathOriginal = utils.CopyString(c.req.pathOriginal) - // Disable compression - if len(compress) == 0 || !compress[0] { - // https://github.com/valyala/fasthttp/blob/7cc6f4c513f9e0d3686142e0a1a5aa2f76b3194a/fs.go#L55 - c.fasthttp.Request.Header.Del(HeaderAcceptEncoding) - } - // copy of https://github.com/valyala/fasthttp/blob/7cc6f4c513f9e0d3686142e0a1a5aa2f76b3194a/fs.go#L103-L121 with small adjustments - if len(file) == 0 || !filepath.IsAbs(file) { - // extend relative path to absolute path - hasTrailingSlash := len(file) > 0 && (file[len(file)-1] == '/' || file[len(file)-1] == '\\') - - var err error - file = filepath.FromSlash(file) - if file, err = filepath.Abs(file); err != nil { - return fmt.Errorf("failed to determine abs file path: %w", err) - } - if hasTrailingSlash { - file += "/" - } - } - // convert the path to forward slashes regardless the OS in order to set the URI properly - // the handler will convert back to OS path separator before opening the file - file = filepath.ToSlash(file) - - // Restore the original requested URL - originalURL := utils.CopyString(c.OriginalURL()) - defer c.fasthttp.Request.SetRequestURI(originalURL) - // Set new URI for fileHandler - c.fasthttp.Request.SetRequestURI(file) - // Save status code - status := c.fasthttp.Response.StatusCode() - // Serve file - sendFileHandler(c.fasthttp) - // Get the status code which is set by fasthttp - fsStatus := c.fasthttp.Response.StatusCode() - // Set the status code set by the user if it is different from the fasthttp status code and 200 - if status != fsStatus && status != StatusOK { - c.Status(status) - } - // Check for error - if status != StatusNotFound && fsStatus == StatusNotFound { - return NewError(StatusNotFound, fmt.Sprintf("sendfile: file %s not found", filename)) - } - return nil + return c.res.SendFile(file, compress...) } -// SendStatus sets the HTTP status code and if the response body is empty, -// it sets the correct status message in the body. +// SendStatus is an alias of [Response.SendStatus]. func (c *DefaultCtx) SendStatus(status int) error { - c.Status(status) - - // Only set status body when there is no response body - if len(c.fasthttp.Response.Body()) == 0 { - return c.SendString(utils.StatusMessage(status)) - } - - return nil + return c.res.SendStatus(status) } -// SendString sets the HTTP response body for string types. -// This means no type assertion, recommended for faster performance +// SendString is an alias of [Response.SendString]. func (c *DefaultCtx) SendString(body string) error { - c.fasthttp.Response.SetBodyString(body) - - return nil + return c.res.SendString(body) } // SendStream sets response body stream and optional body size. @@ -1018,30 +652,14 @@ func (c *DefaultCtx) SendStream(stream io.Reader, size ...int) error { return nil } -// Set sets the response's HTTP header field to the specified key, value. +// Set is an alias of [Response.Set]. func (c *DefaultCtx) Set(key, val string) { - c.fasthttp.Response.Header.Set(key, val) -} - -func (c *DefaultCtx) setCanonical(key, val string) { - c.fasthttp.Response.Header.SetCanonical(utils.UnsafeBytes(key), utils.UnsafeBytes(val)) + c.res.Set(key, val) } -// Subdomains returns a string slice of subdomains in the domain name of the request. -// The subdomain offset, which defaults to 2, is used for determining the beginning of the subdomain segments. +// Subdomains is an alias of [Request.Subdomains]. func (c *DefaultCtx) Subdomains(offset ...int) []string { - o := 2 - if len(offset) > 0 { - o = offset[0] - } - subdomains := strings.Split(c.Host(), ".") - l := len(subdomains) - o - // Check index to avoid slice bounds out of range panic - if l < 0 { - l = len(subdomains) - } - subdomains = subdomains[:l] - return subdomains + return c.req.Subdomains(offset...) } // Stale is an alias of [Request.Stale]. @@ -1049,10 +667,10 @@ func (c *DefaultCtx) Stale() bool { return c.req.Stale() } -// Status sets the HTTP status for the response. +// Status is an alias of [Response.Status]. // This method is chainable. func (c *DefaultCtx) Status(status int) Ctx { - c.fasthttp.Response.SetStatusCode(status) + c.res.Status(status) return c } @@ -1095,44 +713,36 @@ func (c *DefaultCtx) String() string { return str } -// Type sets the Content-Type HTTP header to the MIME type specified by the file extension. +// Type is an alias of [Response.Type]. func (c *DefaultCtx) Type(extension string, charset ...string) Ctx { - if len(charset) > 0 { - c.fasthttp.Response.Header.SetContentType(utils.GetMIME(extension) + "; charset=" + charset[0]) - } else { - c.fasthttp.Response.Header.SetContentType(utils.GetMIME(extension)) - } + c.res.Type(extension, charset...) return c } -// 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 is an alias of [Response.Vary]. func (c *DefaultCtx) Vary(fields ...string) { c.Append(HeaderVary, fields...) } -// Write appends p into response body. +// Write is an alias of [Response.Write]. func (c *DefaultCtx) Write(p []byte) (int, error) { c.fasthttp.Response.AppendBody(p) return len(p), nil } -// Writef appends f & a into response body writer. +// Writef is an alias of [Response.Writef]. func (c *DefaultCtx) Writef(f string, a ...any) (int, error) { - //nolint:wrapcheck // This must not be wrapped - return fmt.Fprintf(c.fasthttp.Response.BodyWriter(), f, a...) + return c.res.Writef(f, a...) } -// WriteString appends s to response body. +// WriteString is an alias of [Response.WriteString]. func (c *DefaultCtx) WriteString(s string) (int, error) { - c.fasthttp.Response.AppendBodyString(s) - return len(s), nil + return c.res.WriteString(s) } -// XHR returns a Boolean property, that is true, if the request's X-Requested-With header field is XMLHttpRequest, -// indicating that the request was issued by a client library (such as jQuery). +// XHR is an alis of [Request.XHR]. func (c *DefaultCtx) XHR() bool { - return utils.EqualFold(c.app.getBytes(c.Get(HeaderXRequestedWith)), []byte("xmlhttprequest")) + return c.req.XHR() } // IsProxyTrusted checks trustworthiness of remote ip. diff --git a/ctx_interface.go b/ctx_interface.go index 1677e9b8eb4..213fc906182 100644 --- a/ctx_interface.go +++ b/ctx_interface.go @@ -90,10 +90,9 @@ type Ctx interface { // To access the underlying fasthttp request object, use [Ctx.Context]. Req() *Request - // Response return the *fasthttp.Response object - // This allows you to use all fasthttp response methods - // https://godoc.org/github.com/valyala/fasthttp#Response - Response() *fasthttp.Response + // Res returns the [Response] object for the current request context. + // To access the underlying fasthttp response object, use [Ctx.Context]. + Res() *Response // Format performs content-negotiation on the Accept HTTP header. // It uses Accepts to select a proper format and calls the matching @@ -131,6 +130,7 @@ type Ctx interface { // Field names are case-insensitive // Returned value is only valid within the handler. Do not store any references. // Make copies or use the Immutable setting instead. + // Deprecated: Use c.Res().Get() GetRespHeader(key string, defaultValue ...string) string // GetRespHeaders returns the HTTP response headers. @@ -410,8 +410,12 @@ func NewDefaultCtx(app *App) *DefaultCtx { req: Request{ app: app, }, + res: Response{ + app: app, + }, } ctx.req.ctx = ctx + ctx.res.ctx = ctx return ctx } @@ -457,6 +461,7 @@ func (c *DefaultCtx) Reset(fctx *fasthttp.RequestCtx) { // Attach *fasthttp.RequestCtx to ctx c.fasthttp = fctx c.req.fasthttp = &fctx.Request + c.res.fasthttp = &fctx.Response // Set method c.req.method = c.app.getString(fctx.Request.Header.Method()) c.req.methodINT = c.app.methodInt(c.req.method) @@ -471,8 +476,7 @@ func (c *DefaultCtx) release() { c.req.route = nil c.fasthttp = nil c.bind = nil - c.redirectionMessages = c.redirectionMessages[:0] - c.viewBindMap = sync.Map{} + c.res.viewBindMap = sync.Map{} if c.redirect != nil { ReleaseRedirect(c.redirect) c.redirect = nil diff --git a/ctx_test.go b/ctx_test.go index fe269831e55..6653ca21422 100644 --- a/ctx_test.go +++ b/ctx_test.go @@ -263,11 +263,11 @@ func Test_Ctx_Append(t *testing.T) { // without append value c.Append("X-Custom-Header") - require.Equal(t, "Hello, World", string(c.Response().Header.Peek("X-Test"))) - require.Equal(t, "World, XHello, Hello", string(c.Response().Header.Peek("X2-Test"))) - require.Equal(t, "XHello, World, Hello", string(c.Response().Header.Peek("X3-Test"))) - require.Equal(t, "XHello, Hello, HelloZ, YHello", string(c.Response().Header.Peek("X4-Test"))) - require.Equal(t, "", string(c.Response().Header.Peek("x-custom-header"))) + require.Equal(t, "Hello, World", c.Res().Get("X-Test")) + require.Equal(t, "World, XHello, Hello", c.Res().Get("X2-Test")) + require.Equal(t, "XHello, World, Hello", c.Res().Get("X3-Test")) + require.Equal(t, "XHello, Hello, HelloZ, YHello", c.Res().Get("X4-Test")) + require.Equal(t, "", c.Res().Get("x-custom-header")) } // go test -v -run=^$ -bench=Benchmark_Ctx_Append -benchmem -count=4 @@ -282,7 +282,7 @@ func Benchmark_Ctx_Append(b *testing.B) { c.Append("X-Custom-Header", "World") c.Append("X-Custom-Header", "Hello") } - require.Equal(b, "Hello, World", app.getString(c.Response().Header.Peek("X-Custom-Header"))) + require.Equal(b, "Hello, World", c.Res().Get("X-Custom-Header")) } // go test -run Test_Ctx_Attachment @@ -293,14 +293,14 @@ func Test_Ctx_Attachment(t *testing.T) { // empty c.Attachment() - require.Equal(t, `attachment`, string(c.Response().Header.Peek(HeaderContentDisposition))) + require.Equal(t, `attachment`, c.Res().Get(HeaderContentDisposition)) // real filename c.Attachment("./static/img/logo.png") - require.Equal(t, `attachment; filename="logo.png"`, string(c.Response().Header.Peek(HeaderContentDisposition))) - require.Equal(t, "image/png", string(c.Response().Header.Peek(HeaderContentType))) + require.Equal(t, `attachment; filename="logo.png"`, c.Res().Get(HeaderContentDisposition)) + require.Equal(t, "image/png", c.Res().Get(HeaderContentType)) // check quoting c.Attachment("another document.pdf\"\r\nBla: \"fasel") - require.Equal(t, `attachment; filename="another+document.pdf%22%0D%0ABla%3A+%22fasel"`, string(c.Response().Header.Peek(HeaderContentDisposition))) + require.Equal(t, `attachment; filename="another+document.pdf%22%0D%0ABla%3A+%22fasel"`, c.Res().Get(HeaderContentDisposition)) } // go test -v -run=^$ -bench=Benchmark_Ctx_Attachment -benchmem -count=4 @@ -314,7 +314,7 @@ func Benchmark_Ctx_Attachment(b *testing.B) { // example with quote params c.Attachment("another document.pdf\"\r\nBla: \"fasel") } - require.Equal(b, `attachment; filename="another+document.pdf%22%0D%0ABla%3A+%22fasel"`, string(c.Response().Header.Peek(HeaderContentDisposition))) + require.Equal(b, `attachment; filename="another+document.pdf%22%0D%0ABla%3A+%22fasel"`, c.Res().Get(HeaderContentDisposition)) } // go test -run Test_Ctx_BaseURL @@ -886,23 +886,23 @@ func Test_Ctx_Cookie(t *testing.T) { } c.Cookie(cookie) expect := "username=john; expires=" + httpdate + "; path=/; SameSite=Lax" - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) expect = "username=john; expires=" + httpdate + "; path=/" cookie.SameSite = CookieSameSiteDisabled c.Cookie(cookie) - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) expect = "username=john; expires=" + httpdate + "; path=/; SameSite=Strict" cookie.SameSite = CookieSameSiteStrictMode c.Cookie(cookie) - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) expect = "username=john; expires=" + httpdate + "; path=/; secure; SameSite=None" cookie.Secure = true cookie.SameSite = CookieSameSiteNoneMode c.Cookie(cookie) - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) expect = "username=john; path=/; secure; SameSite=None" // should remove expires and max-age headers @@ -910,7 +910,7 @@ func Test_Ctx_Cookie(t *testing.T) { cookie.Expires = expire cookie.MaxAge = 10000 c.Cookie(cookie) - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) expect = "username=john; path=/; secure; SameSite=None" // should remove expires and max-age headers when no expire and no MaxAge (default time) @@ -918,7 +918,7 @@ func Test_Ctx_Cookie(t *testing.T) { cookie.Expires = time.Time{} cookie.MaxAge = 0 c.Cookie(cookie) - require.Equal(t, expect, string(c.Response().Header.Peek(HeaderSetCookie))) + require.Equal(t, expect, c.Res().Get(HeaderSetCookie)) } // go test -v -run=^$ -bench=Benchmark_Ctx_Cookie -benchmem -count=4 @@ -934,7 +934,7 @@ func Benchmark_Ctx_Cookie(b *testing.B) { Value: "Doe", }) } - require.Equal(b, "John=Doe; path=/; SameSite=Lax", app.getString(c.Response().Header.Peek("Set-Cookie"))) + require.Equal(b, "John=Doe; path=/; SameSite=Lax", c.Res().Get("Set-Cookie")) } // go test -run Test_Ctx_Cookies @@ -973,13 +973,13 @@ func Test_Ctx_Format(t *testing.T) { require.Equal(t, "application/xhtml+xml", accepted) require.Equal(t, "application/xhtml+xml", c.GetRespHeader(HeaderContentType)) require.NoError(t, err) - require.NotEqual(t, StatusNotAcceptable, c.Response().StatusCode()) + require.NotEqual(t, StatusNotAcceptable, c.Context().Response.StatusCode()) err = c.Format(formatHandlers("foo/bar;a=b")...) require.Equal(t, "foo/bar;a=b", accepted) require.Equal(t, "foo/bar;a=b", c.GetRespHeader(HeaderContentType)) require.NoError(t, err) - require.NotEqual(t, StatusNotAcceptable, c.Response().StatusCode()) + require.NotEqual(t, StatusNotAcceptable, c.Context().Response.StatusCode()) myError := errors.New("this is an error") err = c.Format(ResFmt{"text/html", func(_ Ctx) error { return myError }}) @@ -987,7 +987,7 @@ func Test_Ctx_Format(t *testing.T) { c.Context().Request.Header.Set(HeaderAccept, "application/json") err = c.Format(ResFmt{"text/html", func(c Ctx) error { return c.SendStatus(StatusOK) }}) - require.Equal(t, StatusNotAcceptable, c.Response().StatusCode()) + require.Equal(t, StatusNotAcceptable, c.Context().Response.StatusCode()) require.NoError(t, err) err = c.Format(formatHandlers("text/html", "default")...) @@ -1073,27 +1073,27 @@ func Test_Ctx_AutoFormat(t *testing.T) { c.Context().Request.Header.Set(HeaderAccept, MIMETextPlain) err := c.AutoFormat([]byte("Hello, World!")) require.NoError(t, err) - require.Equal(t, "Hello, World!", string(c.Response().Body())) + require.Equal(t, "Hello, World!", string(c.Context().Response.Body())) c.Context().Request.Header.Set(HeaderAccept, MIMETextHTML) err = c.AutoFormat("Hello, World!") require.NoError(t, err) - require.Equal(t, "

Hello, World!

", string(c.Response().Body())) + require.Equal(t, "

Hello, World!

", string(c.Context().Response.Body())) c.Context().Request.Header.Set(HeaderAccept, MIMEApplicationJSON) err = c.AutoFormat("Hello, World!") require.NoError(t, err) - require.Equal(t, `"Hello, World!"`, string(c.Response().Body())) + require.Equal(t, `"Hello, World!"`, string(c.Context().Response.Body())) c.Context().Request.Header.Set(HeaderAccept, MIMETextPlain) err = c.AutoFormat(complex(1, 1)) require.NoError(t, err) - require.Equal(t, "(1+1i)", string(c.Response().Body())) + require.Equal(t, "(1+1i)", string(c.Context().Response.Body())) c.Context().Request.Header.Set(HeaderAccept, MIMEApplicationXML) err = c.AutoFormat("Hello, World!") require.NoError(t, err) - require.Equal(t, `Hello, World!`, string(c.Response().Body())) + require.Equal(t, `Hello, World!`, string(c.Context().Response.Body())) err = c.AutoFormat(complex(1, 1)) require.Error(t, err) @@ -1101,14 +1101,14 @@ func Test_Ctx_AutoFormat(t *testing.T) { c.Context().Request.Header.Set(HeaderAccept, MIMETextPlain) err = c.AutoFormat(Map{}) require.NoError(t, err) - require.Equal(t, "map[]", string(c.Response().Body())) + require.Equal(t, "map[]", string(c.Context().Response.Body())) type broken string c.Context().Request.Header.Set(HeaderAccept, "broken/accept") require.NoError(t, err) err = c.AutoFormat(broken("Hello, World!")) require.NoError(t, err) - require.Equal(t, `Hello, World!`, string(c.Response().Body())) + require.Equal(t, `Hello, World!`, string(c.Context().Response.Body())) } func Test_Ctx_AutoFormat_Struct(t *testing.T) { @@ -1132,7 +1132,7 @@ func Test_Ctx_AutoFormat_Struct(t *testing.T) { require.NoError(t, err) require.Equal(t, `{"Recipients":["Alice","Bob"],"Sender":"Carol","Urgency":3}`, - string(c.Response().Body()), + string(c.Context().Response.Body()), ) c.Context().Request.Header.Set(HeaderAccept, MIMEApplicationXML) @@ -1140,7 +1140,7 @@ func Test_Ctx_AutoFormat_Struct(t *testing.T) { require.NoError(t, err) require.Equal(t, `AliceBob`, - string(c.Response().Body()), + string(c.Context().Response.Body()), ) } @@ -1158,7 +1158,7 @@ func Benchmark_Ctx_AutoFormat(b *testing.B) { err = c.AutoFormat("Hello, World!") } require.NoError(b, err) - require.Equal(b, `Hello, World!`, string(c.Response().Body())) + require.Equal(b, `Hello, World!`, string(c.Context().Response.Body())) } // go test -v -run=^$ -bench=Benchmark_Ctx_AutoFormat_HTML -benchmem -count=4 @@ -1175,7 +1175,7 @@ func Benchmark_Ctx_AutoFormat_HTML(b *testing.B) { err = c.AutoFormat("Hello, World!") } require.NoError(b, err) - require.Equal(b, "

Hello, World!

", string(c.Response().Body())) + require.Equal(b, "

Hello, World!

", string(c.Context().Response.Body())) } // go test -v -run=^$ -bench=Benchmark_Ctx_AutoFormat_JSON -benchmem -count=4 @@ -1192,7 +1192,7 @@ func Benchmark_Ctx_AutoFormat_JSON(b *testing.B) { err = c.AutoFormat("Hello, World!") } require.NoError(b, err) - require.Equal(b, `"Hello, World!"`, string(c.Response().Body())) + require.Equal(b, `"Hello, World!"`, string(c.Context().Response.Body())) } // go test -v -run=^$ -bench=Benchmark_Ctx_AutoFormat_XML -benchmem -count=4 @@ -1209,7 +1209,7 @@ func Benchmark_Ctx_AutoFormat_XML(b *testing.B) { err = c.AutoFormat("Hello, World!") } require.NoError(b, err) - require.Equal(b, `Hello, World!`, string(c.Response().Body())) + require.Equal(b, `Hello, World!`, string(c.Context().Response.Body())) } // go test -run Test_Ctx_FormFile @@ -1324,17 +1324,17 @@ func Test_Ctx_Fresh(t *testing.T) { require.False(t, c.Fresh()) c.Context().Request.Header.Set(HeaderIfNoneMatch, "a, b") - c.Response().Header.Set(HeaderETag, "c") + c.Set(HeaderETag, "c") require.False(t, c.Fresh()) - c.Response().Header.Set(HeaderETag, "a") + c.Set(HeaderETag, "a") require.True(t, c.Fresh()) c.Context().Request.Header.Set(HeaderIfModifiedSince, "xxWed, 21 Oct 2015 07:28:00 GMT") - c.Response().Header.Set(HeaderLastModified, "xxWed, 21 Oct 2015 07:28:00 GMT") + c.Set(HeaderLastModified, "xxWed, 21 Oct 2015 07:28:00 GMT") require.False(t, c.Fresh()) - c.Response().Header.Set(HeaderLastModified, "Wed, 21 Oct 2015 07:28:00 GMT") + c.Set(HeaderLastModified, "Wed, 21 Oct 2015 07:28:00 GMT") require.False(t, c.Fresh()) c.Context().Request.Header.Set(HeaderIfModifiedSince, "Wed, 21 Oct 2015 07:28:00 GMT") @@ -2894,13 +2894,13 @@ func Test_Ctx_ClearCookie(t *testing.T) { c.Context().Request.Header.Set(HeaderCookie, "john=doe") c.ClearCookie("john") - require.True(t, strings.HasPrefix(string(c.Response().Header.Peek(HeaderSetCookie)), "john=; expires=")) + require.True(t, strings.HasPrefix(c.Res().Get(HeaderSetCookie), "john=; expires=")) c.Context().Request.Header.Set(HeaderCookie, "test1=dummy") c.Context().Request.Header.Set(HeaderCookie, "test2=dummy") c.ClearCookie() - require.Contains(t, string(c.Response().Header.Peek(HeaderSetCookie)), "test1=; expires=") - require.Contains(t, string(c.Response().Header.Peek(HeaderSetCookie)), "test2=; expires=") + require.Contains(t, c.Res().Get(HeaderSetCookie), "test1=; expires=") + require.Contains(t, c.Res().Get(HeaderSetCookie), "test2=; expires=") } // go test -race -run Test_Ctx_Download @@ -2919,11 +2919,11 @@ func Test_Ctx_Download(t *testing.T) { expect, err := io.ReadAll(f) require.NoError(t, err) - require.Equal(t, expect, c.Response().Body()) - require.Equal(t, `attachment; filename="Awesome+File%21"`, string(c.Response().Header.Peek(HeaderContentDisposition))) + require.Equal(t, expect, c.Context().Response.Body()) + require.Equal(t, `attachment; filename="Awesome+File%21"`, c.Res().Get(HeaderContentDisposition)) require.NoError(t, c.Download("ctx.go")) - require.Equal(t, `attachment; filename="ctx.go"`, string(c.Response().Header.Peek(HeaderContentDisposition))) + require.Equal(t, `attachment; filename="ctx.go"`, c.Res().Get(HeaderContentDisposition)) } // go test -race -run Test_Ctx_SendFile @@ -2948,8 +2948,8 @@ func Test_Ctx_SendFile(t *testing.T) { err = c.SendFile("ctx.go") // check expectation require.NoError(t, err) - require.Equal(t, expectFileContent, c.Response().Body()) - require.Equal(t, StatusOK, c.Response().StatusCode()) + require.Equal(t, expectFileContent, c.Context().Response.Body()) + require.Equal(t, StatusOK, c.Context().Response.StatusCode()) app.ReleaseCtx(c) // test with custom error code @@ -2957,8 +2957,8 @@ func Test_Ctx_SendFile(t *testing.T) { err = c.Status(StatusInternalServerError).SendFile("ctx.go") // check expectation require.NoError(t, err) - require.Equal(t, expectFileContent, c.Response().Body()) - require.Equal(t, StatusInternalServerError, c.Response().StatusCode()) + require.Equal(t, expectFileContent, c.Context().Response.Body()) + require.Equal(t, StatusInternalServerError, c.Context().Response.StatusCode()) app.ReleaseCtx(c) // test not modified @@ -2967,8 +2967,8 @@ func Test_Ctx_SendFile(t *testing.T) { err = c.SendFile("ctx.go") // check expectation require.NoError(t, err) - require.Equal(t, StatusNotModified, c.Response().StatusCode()) - require.Equal(t, []byte(nil), c.Response().Body()) + require.Equal(t, StatusNotModified, c.Context().Response.StatusCode()) + require.Equal(t, []byte(nil), c.Context().Response.Body()) app.ReleaseCtx(c) } @@ -3066,8 +3066,8 @@ func Test_Ctx_JSON(t *testing.T) { "Age": 20, }) require.NoError(t, err) - require.Equal(t, `{"Age":20,"Name":"Grame"}`, string(c.Response().Body())) - require.Equal(t, "application/json", string(c.Response().Header.Peek("content-type"))) + require.Equal(t, `{"Age":20,"Name":"Grame"}`, string(c.Context().Response.Body())) + require.Equal(t, "application/json", c.Res().Get("content-type")) // Test with ctype err = c.JSON(Map{ // map has no order @@ -3075,13 +3075,13 @@ func Test_Ctx_JSON(t *testing.T) { "Age": 20, }, "application/problem+json") require.NoError(t, err) - require.Equal(t, `{"Age":20,"Name":"Grame"}`, string(c.Response().Body())) - require.Equal(t, "application/problem+json", string(c.Response().Header.Peek("content-type"))) + require.Equal(t, `{"Age":20,"Name":"Grame"}`, string(c.Context().Response.Body())) + require.Equal(t, "application/problem+json", c.Res().Get("content-type")) testEmpty := func(v any, r string) { err := c.JSON(v) require.NoError(t, err) - require.Equal(t, r, string(c.Response().Body())) + require.Equal(t, r, string(c.Context().Response.Body())) } testEmpty(nil, "null") @@ -3104,8 +3104,8 @@ func Test_Ctx_JSON(t *testing.T) { "Age": 20, }) require.NoError(t, err) - require.Equal(t, `["custom","json"]`, string(c.Response().Body())) - require.Equal(t, "application/json", string(c.Response().Header.Peek("content-type"))) + require.Equal(t, `["custom","json"]`, string(c.Context().Response.Body())) + require.Equal(t, "application/json", c.Res().Get("content-type")) }) } @@ -3129,7 +3129,7 @@ func Benchmark_Ctx_JSON(b *testing.B) { err = c.JSON(data) } require.NoError(b, err) - require.Equal(b, `{"Name":"Grame","Age":20}`, string(c.Response().Body())) + require.Equal(b, `{"Name":"Grame","Age":20}`, string(c.Context().Response.Body())) } // go test -run=^$ -bench=Benchmark_Ctx_JSON_Ctype -benchmem -count=4 @@ -3152,8 +3152,8 @@ func Benchmark_Ctx_JSON_Ctype(b *testing.B) { err = c.JSON(data, "application/problem+json") } require.NoError(b, err) - require.Equal(b, `{"Name":"Grame","Age":20}`, string(c.Response().Body())) - require.Equal(b, "application/problem+json", string(c.Response().Header.Peek("content-type"))) + require.Equal(b, `{"Name":"Grame","Age":20}`, string(c.Context().Response.Body())) + require.Equal(b, "application/problem+json", c.Res().Get("content-type")) } // go test -run Test_Ctx_JSONP @@ -3169,16 +3169,16 @@ func Test_Ctx_JSONP(t *testing.T) { "Age": 20, }) require.NoError(t, err) - require.Equal(t, `callback({"Age":20,"Name":"Grame"});`, string(c.Response().Body())) - require.Equal(t, "text/javascript; charset=utf-8", string(c.Response().Header.Peek("content-type"))) + require.Equal(t, `callback({"Age":20,"Name":"Grame"});`, string(c.Context().Response.Body())) + require.Equal(t, "text/javascript; charset=utf-8", c.Res().Get("content-type")) err = c.JSONP(Map{ "Name": "Grame", "Age": 20, }, "john") require.NoError(t, err) - require.Equal(t, `john({"Age":20,"Name":"Grame"});`, string(c.Response().Body())) - require.Equal(t, "text/javascript; charset=utf-8", string(c.Response().Header.Peek("content-type"))) + require.Equal(t, `john({"Age":20,"Name":"Grame"});`, string(c.Context().Response.Body())) + require.Equal(t, "text/javascript; charset=utf-8", c.Res().Get("content-type")) t.Run("custom json encoder", func(t *testing.T) { t.Parallel() @@ -3195,8 +3195,8 @@ func Test_Ctx_JSONP(t *testing.T) { "Age": 20, }) require.NoError(t, err) - require.Equal(t, `callback(["custom","json"]);`, string(c.Response().Body())) - require.Equal(t, "text/javascript; charset=utf-8", string(c.Response().Header.Peek("content-type"))) + require.Equal(t, `callback(["custom","json"]);`, string(c.Context().Response.Body())) + require.Equal(t, "text/javascript; charset=utf-8", c.Res().Get("content-type")) }) } @@ -3221,7 +3221,7 @@ func Benchmark_Ctx_JSONP(b *testing.B) { err = c.JSONP(data, callback) } require.NoError(b, err) - require.Equal(b, `emit({"Name":"Grame","Age":20});`, string(c.Response().Body())) + require.Equal(b, `emit({"Name":"Grame","Age":20});`, string(c.Context().Response.Body())) } // go test -run Test_Ctx_XML @@ -3243,13 +3243,13 @@ func Test_Ctx_XML(t *testing.T) { Ages: []int{1, 12, 20}, }) require.NoError(t, err) - require.Equal(t, `GrameJohn11220`, string(c.Response().Body())) - require.Equal(t, "application/xml", string(c.Response().Header.Peek("content-type"))) + require.Equal(t, `GrameJohn11220`, string(c.Context().Response.Body())) + require.Equal(t, "application/xml", c.Res().Get("content-type")) testEmpty := func(v any, r string) { err := c.XML(v) require.NoError(t, err) - require.Equal(t, r, string(c.Response().Body())) + require.Equal(t, r, string(c.Context().Response.Body())) } testEmpty(nil, "") @@ -3279,8 +3279,8 @@ func Test_Ctx_XML(t *testing.T) { }) require.NoError(t, err) - require.Equal(t, `xml`, string(c.Response().Body())) - require.Equal(t, "application/xml", string(c.Response().Header.Peek("content-type"))) + require.Equal(t, `xml`, string(c.Context().Response.Body())) + require.Equal(t, "application/xml", c.Res().Get("content-type")) }) } @@ -3304,7 +3304,7 @@ func Benchmark_Ctx_XML(b *testing.B) { } require.NoError(b, err) - require.Equal(b, `Grame20`, string(c.Response().Body())) + require.Equal(b, `Grame20`, string(c.Context().Response.Body())) } // go test -run Test_Ctx_Links @@ -3314,13 +3314,13 @@ func Test_Ctx_Links(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Links() - require.Equal(t, "", string(c.Response().Header.Peek(HeaderLink))) + require.Equal(t, "", c.Res().Get(HeaderLink)) c.Links( "http://api.example.com/users?page=2", "next", "http://api.example.com/users?page=5", "last", ) - require.Equal(t, `; rel="next",; rel="last"`, string(c.Response().Header.Peek(HeaderLink))) + require.Equal(t, `; rel="next",; rel="last"`, c.Res().Get(HeaderLink)) } // go test -v -run=^$ -bench=Benchmark_Ctx_Links -benchmem -count=4 @@ -3345,7 +3345,7 @@ func Test_Ctx_Location(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Location("http://example.com") - require.Equal(t, "http://example.com", string(c.Response().Header.Peek(HeaderLocation))) + require.Equal(t, "http://example.com", c.Res().Get(HeaderLocation)) } // go test -run Test_Ctx_Next @@ -3391,7 +3391,7 @@ func Test_Ctx_Render(t *testing.T) { }) require.NoError(t, err) - require.Equal(t, "

Hello, World!

", string(c.Response().Body())) + require.Equal(t, "

Hello, World!

", string(c.Context().Response.Body())) err = c.Render("./.github/testdata/template-non-exists.html", nil) require.Error(t, err) @@ -3411,7 +3411,7 @@ func Test_Ctx_RenderWithoutLocals(t *testing.T) { err := c.Render("./.github/testdata/index.tmpl", Map{}) require.NoError(t, err) - require.Equal(t, "

", string(c.Response().Body())) + require.Equal(t, "

", string(c.Context().Response.Body())) } func Test_Ctx_RenderWithLocals(t *testing.T) { @@ -3428,7 +3428,7 @@ func Test_Ctx_RenderWithLocals(t *testing.T) { err := c.Render("./.github/testdata/index.tmpl", Map{}) require.NoError(t, err) - require.Equal(t, "

Hello, World!

", string(c.Response().Body())) + require.Equal(t, "

Hello, World!

", string(c.Context().Response.Body())) }) t.Run("NilBind", func(t *testing.T) { @@ -3439,7 +3439,7 @@ func Test_Ctx_RenderWithLocals(t *testing.T) { err := c.Render("./.github/testdata/index.tmpl", nil) require.NoError(t, err) - require.Equal(t, "

Hello, World!

", string(c.Response().Body())) + require.Equal(t, "

Hello, World!

", string(c.Context().Response.Body())) }) } @@ -3461,7 +3461,7 @@ func Test_Ctx_RenderWithViewBind(t *testing.T) { defer bytebufferpool.Put(buf) require.NoError(t, err) - require.Equal(t, "

Hello, World!

", string(c.Response().Body())) + require.Equal(t, "

Hello, World!

", string(c.Context().Response.Body())) } func Test_Ctx_RenderWithOverwrittenViewBind(t *testing.T) { @@ -3483,7 +3483,7 @@ func Test_Ctx_RenderWithOverwrittenViewBind(t *testing.T) { buf.WriteString("overwrite") defer bytebufferpool.Put(buf) - require.Equal(t, "

Hello from Fiber!

", string(c.Response().Body())) + require.Equal(t, "

Hello from Fiber!

", string(c.Context().Response.Body())) } func Test_Ctx_RenderWithViewBindLocals(t *testing.T) { @@ -3503,9 +3503,9 @@ func Test_Ctx_RenderWithViewBindLocals(t *testing.T) { err = c.Render("./.github/testdata/template.tmpl", Map{}) require.NoError(t, err) - require.Equal(t, "

Hello, World! Test

", string(c.Response().Body())) + require.Equal(t, "

Hello, World! Test

", string(c.Context().Response.Body())) - require.Equal(t, "

Hello, World! Test

", string(c.Response().Body())) + require.Equal(t, "

Hello, World! Test

", string(c.Context().Response.Body())) } func Test_Ctx_RenderWithLocalsAndBinding(t *testing.T) { @@ -3527,7 +3527,7 @@ func Test_Ctx_RenderWithLocalsAndBinding(t *testing.T) { }) require.NoError(t, err) - require.Equal(t, "

Hello, World!

", string(c.Response().Body())) + require.Equal(t, "

Hello, World!

", string(c.Context().Response.Body())) } func Benchmark_Ctx_RenderWithLocalsAndViewBind(b *testing.B) { @@ -3554,7 +3554,7 @@ func Benchmark_Ctx_RenderWithLocalsAndViewBind(b *testing.B) { } require.NoError(b, err) - require.Equal(b, "

Hello, World! Test

", string(c.Response().Body())) + require.Equal(b, "

Hello, World! Test

", string(c.Context().Response.Body())) } func Benchmark_Ctx_RenderLocals(b *testing.B) { @@ -3577,7 +3577,7 @@ func Benchmark_Ctx_RenderLocals(b *testing.B) { } require.NoError(b, err) - require.Equal(b, "

Hello, World!

", string(c.Response().Body())) + require.Equal(b, "

Hello, World!

", string(c.Context().Response.Body())) } func Benchmark_Ctx_RenderViewBind(b *testing.B) { @@ -3601,7 +3601,7 @@ func Benchmark_Ctx_RenderViewBind(b *testing.B) { } require.NoError(b, err) - require.Equal(b, "

Hello, World!

", string(c.Response().Body())) + require.Equal(b, "

Hello, World!

", string(c.Context().Response.Body())) } // go test -run Test_Ctx_RestartRouting @@ -3711,7 +3711,7 @@ func Test_Ctx_Render_Engine(t *testing.T) { "Title": "Hello, World!", }) require.NoError(t, err) - require.Equal(t, "

Hello, World!

", string(c.Response().Body())) + require.Equal(t, "

Hello, World!

", string(c.Context().Response.Body())) } // go test -run Test_Ctx_Render_Engine_With_View_Layout @@ -3727,7 +3727,7 @@ func Test_Ctx_Render_Engine_With_View_Layout(t *testing.T) { "Title": "Hello, World!", }) require.NoError(t, err) - require.Equal(t, "

Hello, World!

I'm main

", string(c.Response().Body())) + require.Equal(t, "

Hello, World!

I'm main

", string(c.Context().Response.Body())) } // go test -v -run=^$ -bench=Benchmark_Ctx_Render_Engine -benchmem -count=4 @@ -3747,7 +3747,7 @@ func Benchmark_Ctx_Render_Engine(b *testing.B) { }) } require.NoError(b, err) - require.Equal(b, "

Hello, World!

", string(c.Response().Body())) + require.Equal(b, "

Hello, World!

", string(c.Context().Response.Body())) } // go test -v -run=^$ -bench=Benchmark_Ctx_Get_Location_From_Route -benchmem -count=4 @@ -3887,7 +3887,7 @@ func Test_Ctx_Render_Go_Template(t *testing.T) { err = c.Render(file.Name(), nil) require.NoError(t, err) - require.Equal(t, "template", string(c.Response().Body())) + require.Equal(t, "template", string(c.Context().Response.Body())) } // go test -run Test_Ctx_Send @@ -3899,7 +3899,7 @@ func Test_Ctx_Send(t *testing.T) { require.NoError(t, c.Send([]byte("Hello, World"))) require.NoError(t, c.Send([]byte("Don't crash please"))) require.NoError(t, c.Send([]byte("1337"))) - require.Equal(t, "1337", string(c.Response().Body())) + require.Equal(t, "1337", string(c.Context().Response.Body())) } // go test -v -run=^$ -bench=Benchmark_Ctx_Send -benchmem -count=4 @@ -3916,7 +3916,7 @@ func Benchmark_Ctx_Send(b *testing.B) { err = c.Send(byt) } require.NoError(b, err) - require.Equal(b, "Hello, World!", string(c.Response().Body())) + require.Equal(b, "Hello, World!", string(c.Context().Response.Body())) } // go test -run Test_Ctx_SendStatus @@ -3927,8 +3927,8 @@ func Test_Ctx_SendStatus(t *testing.T) { err := c.SendStatus(415) require.NoError(t, err) - require.Equal(t, 415, c.Response().StatusCode()) - require.Equal(t, "Unsupported Media Type", string(c.Response().Body())) + require.Equal(t, 415, c.Context().Response.StatusCode()) + require.Equal(t, "Unsupported Media Type", string(c.Context().Response.Body())) } // go test -run Test_Ctx_SendString @@ -3939,7 +3939,7 @@ func Test_Ctx_SendString(t *testing.T) { err := c.SendString("Don't crash please") require.NoError(t, err) - require.Equal(t, "Don't crash please", string(c.Response().Body())) + require.Equal(t, "Don't crash please", string(c.Context().Response.Body())) } // go test -run Test_Ctx_SendStream @@ -3950,15 +3950,15 @@ func Test_Ctx_SendStream(t *testing.T) { err := c.SendStream(bytes.NewReader([]byte("Don't crash please"))) require.NoError(t, err) - require.Equal(t, "Don't crash please", string(c.Response().Body())) + require.Equal(t, "Don't crash please", string(c.Context().Response.Body())) err = c.SendStream(bytes.NewReader([]byte("Don't crash please")), len([]byte("Don't crash please"))) require.NoError(t, err) - require.Equal(t, "Don't crash please", string(c.Response().Body())) + require.Equal(t, "Don't crash please", string(c.Context().Response.Body())) err = c.SendStream(bufio.NewReader(bytes.NewReader([]byte("Hello bufio")))) require.NoError(t, err) - require.Equal(t, "Hello bufio", string(c.Response().Body())) + require.Equal(t, "Hello bufio", string(c.Context().Response.Body())) } // go test -run Test_Ctx_Set @@ -3971,9 +3971,9 @@ func Test_Ctx_Set(t *testing.T) { c.Set("X-2", "2") c.Set("X-3", "3") c.Set("X-3", "1337") - require.Equal(t, "1", string(c.Response().Header.Peek("x-1"))) - require.Equal(t, "2", string(c.Response().Header.Peek("x-2"))) - require.Equal(t, "1337", string(c.Response().Header.Peek("x-3"))) + require.Equal(t, "1", c.Res().Get("x-1")) + require.Equal(t, "2", c.Res().Get("x-2")) + require.Equal(t, "1337", c.Res().Get("x-3")) } // go test -run Test_Ctx_Set_Splitter @@ -3983,11 +3983,11 @@ func Test_Ctx_Set_Splitter(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Set("Location", "foo\r\nSet-Cookie:%20SESSIONID=MaliciousValue\r\n") - h := string(c.Response().Header.Peek("Location")) + h := c.Res().Get("Location") require.NotContains(t, h, "\r\n") c.Set("Location", "foo\nSet-Cookie:%20SESSIONID=MaliciousValue\n") - h = string(c.Response().Header.Peek("Location")) + h = c.Res().Get("Location") require.NotContains(t, h, "\n") } @@ -4011,11 +4011,11 @@ func Test_Ctx_Status(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Status(400) - require.Equal(t, 400, c.Response().StatusCode()) + require.Equal(t, 400, c.Context().Response.StatusCode()) err := c.Status(415).Send([]byte("Hello, World")) require.NoError(t, err) - require.Equal(t, 415, c.Response().StatusCode()) - require.Equal(t, "Hello, World", string(c.Response().Body())) + require.Equal(t, 415, c.Context().Response.StatusCode()) + require.Equal(t, "Hello, World", string(c.Context().Response.Body())) } // go test -run Test_Ctx_Type @@ -4025,16 +4025,16 @@ func Test_Ctx_Type(t *testing.T) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) c.Type(".json") - require.Equal(t, "application/json", string(c.Response().Header.Peek("Content-Type"))) + require.Equal(t, "application/json", c.Res().Get("Content-Type")) c.Type("json", "utf-8") - require.Equal(t, "application/json; charset=utf-8", string(c.Response().Header.Peek("Content-Type"))) + require.Equal(t, "application/json; charset=utf-8", c.Res().Get("Content-Type")) c.Type(".html") - require.Equal(t, "text/html", string(c.Response().Header.Peek("Content-Type"))) + require.Equal(t, "text/html", c.Res().Get("Content-Type")) c.Type("html", "utf-8") - require.Equal(t, "text/html; charset=utf-8", string(c.Response().Header.Peek("Content-Type"))) + require.Equal(t, "text/html; charset=utf-8", c.Res().Get("Content-Type")) } // go test -v -run=^$ -bench=Benchmark_Ctx_Type -benchmem -count=4 @@ -4072,7 +4072,7 @@ func Test_Ctx_Vary(t *testing.T) { c.Vary("Origin") c.Vary("User-Agent") c.Vary("Accept-Encoding", "Accept") - require.Equal(t, "Origin, User-Agent, Accept-Encoding, Accept", string(c.Response().Header.Peek("Vary"))) + require.Equal(t, "Origin, User-Agent, Accept-Encoding, Accept", c.Res().Get("Vary")) } // go test -v -run=^$ -bench=Benchmark_Ctx_Vary -benchmem -count=4 @@ -4097,7 +4097,7 @@ func Test_Ctx_Write(t *testing.T) { require.NoError(t, err) _, err = c.Write([]byte("World!")) require.NoError(t, err) - require.Equal(t, "Hello, World!", string(c.Response().Body())) + require.Equal(t, "Hello, World!", string(c.Context().Response.Body())) } // go test -v -run=^$ -bench=Benchmark_Ctx_Write -benchmem -count=4 @@ -4125,7 +4125,7 @@ func Test_Ctx_Writef(t *testing.T) { world := "World!" _, err := c.Writef("Hello, %s", world) require.NoError(t, err) - require.Equal(t, "Hello, World!", string(c.Response().Body())) + require.Equal(t, "Hello, World!", string(c.Context().Response.Body())) } // go test -v -run=^$ -bench=Benchmark_Ctx_Writef -benchmem -count=4 @@ -4154,7 +4154,7 @@ func Test_Ctx_WriteString(t *testing.T) { require.NoError(t, err) _, err = c.WriteString("World!") require.NoError(t, err) - require.Equal(t, "Hello, World!", string(c.Response().Body())) + require.Equal(t, "Hello, World!", string(c.Context().Response.Body())) } // go test -run Test_Ctx_XHR @@ -4196,7 +4196,7 @@ func Benchmark_Ctx_SendString_B(b *testing.B) { err = c.SendString(body) } require.NoError(b, err) - require.Equal(b, []byte("Hello, world!"), c.Response().Body()) + require.Equal(b, []byte("Hello, world!"), c.Context().Response.Body()) } // go test -run Test_Ctx_Queries -v @@ -4485,9 +4485,9 @@ func Test_Ctx_GetRespHeaders(t *testing.T) { c.Set("test", "Hello, World 👋!") c.Set("foo", "bar") - c.Response().Header.Set("multi", "one") - c.Response().Header.Add("multi", "two") - c.Response().Header.Set(HeaderContentType, "application/json") + c.Context().Response.Header.Set("multi", "one") + c.Context().Response.Header.Add("multi", "two") + c.Context().Response.Header.Set(HeaderContentType, "application/json") require.Equal(t, map[string][]string{ "Content-Type": {"application/json"}, @@ -4501,9 +4501,9 @@ func Benchmark_Ctx_GetRespHeaders(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Response().Header.Set("test", "Hello, World 👋!") - c.Response().Header.Set("foo", "bar") - c.Response().Header.Set(HeaderContentType, "application/json") + c.Set("test", "Hello, World 👋!") + c.Set("foo", "bar") + c.Set(HeaderContentType, "application/json") b.ReportAllocs() b.ResetTimer() @@ -5764,7 +5764,7 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { b.Run("NoProxyCheck", func(b *testing.B) { app := New() c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().SetRequestURI("http://google.com:8080/test") + c.Context().Request.SetRequestURI("http://google.com:8080/test") b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { @@ -5780,7 +5780,7 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { b.ResetTimer() b.RunParallel(func(pb *testing.PB) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().SetRequestURI("http://google.com:8080/test") + c.Context().Request.SetRequestURI("http://google.com:8080/test") for pb.Next() { c.IsProxyTrusted() } @@ -5794,8 +5794,8 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { EnableTrustedProxyCheck: true, }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().SetRequestURI("http://google.com/test") - c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + c.Context().Request.SetRequestURI("http://google.com/test") + c.Context().Request.Header.Set(HeaderXForwardedHost, "google1.com") b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { @@ -5813,8 +5813,8 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { b.ResetTimer() b.RunParallel(func(pb *testing.PB) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().SetRequestURI("http://google.com/") - c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + c.Context().Request.SetRequestURI("http://google.com/") + c.Context().Request.Header.Set(HeaderXForwardedHost, "google1.com") for pb.Next() { c.IsProxyTrusted() } @@ -5829,8 +5829,8 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { TrustedProxies: []string{"0.0.0.0"}, }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().SetRequestURI("http://google.com/test") - c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + c.Context().Request.SetRequestURI("http://google.com/test") + c.Context().Request.Header.Set(HeaderXForwardedHost, "google1.com") b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { @@ -5849,8 +5849,8 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { b.ResetTimer() b.RunParallel(func(pb *testing.PB) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().SetRequestURI("http://google.com/") - c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + c.Context().Request.SetRequestURI("http://google.com/") + c.Context().Request.Header.Set(HeaderXForwardedHost, "google1.com") for pb.Next() { c.IsProxyTrusted() } @@ -5865,8 +5865,8 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { TrustedProxies: []string{"0.0.0.0/8"}, }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().SetRequestURI("http://google.com/test") - c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + c.Context().Request.SetRequestURI("http://google.com/test") + c.Context().Request.Header.Set(HeaderXForwardedHost, "google1.com") b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { @@ -5885,8 +5885,8 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { b.ResetTimer() b.RunParallel(func(pb *testing.PB) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().SetRequestURI("http://google.com/") - c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + c.Context().Request.SetRequestURI("http://google.com/") + c.Context().Request.Header.Set(HeaderXForwardedHost, "google1.com") for pb.Next() { c.IsProxyTrusted() } @@ -5901,8 +5901,8 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { TrustedProxies: []string{"192.168.0.0/24", "10.0.0.0/16", "0.0.0.0/8"}, }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().SetRequestURI("http://google.com/test") - c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + c.Context().Request.SetRequestURI("http://google.com/test") + c.Context().Request.Header.Set(HeaderXForwardedHost, "google1.com") b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { @@ -5921,8 +5921,8 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { b.ResetTimer() b.RunParallel(func(pb *testing.PB) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().SetRequestURI("http://google.com/") - c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + c.Context().Request.SetRequestURI("http://google.com/") + c.Context().Request.Header.Set(HeaderXForwardedHost, "google1.com") for pb.Next() { c.IsProxyTrusted() } @@ -5947,8 +5947,8 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { }, }) c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().SetRequestURI("http://google.com/test") - c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + c.Context().Request.SetRequestURI("http://google.com/test") + c.Context().Request.Header.Set(HeaderXForwardedHost, "google1.com") b.ReportAllocs() b.ResetTimer() for n := 0; n < b.N; n++ { @@ -5977,8 +5977,8 @@ func Benchmark_Ctx_IsProxyTrusted(b *testing.B) { b.ResetTimer() b.RunParallel(func(pb *testing.PB) { c := app.AcquireCtx(&fasthttp.RequestCtx{}) - c.Request().SetRequestURI("http://google.com/") - c.Request().Header.Set(HeaderXForwardedHost, "google1.com") + c.Context().Request.SetRequestURI("http://google.com/") + c.Context().Request.Header.Set(HeaderXForwardedHost, "google1.com") for pb.Next() { c.IsProxyTrusted() } diff --git a/middleware/cache/cache.go b/middleware/cache/cache.go index 0ace704e357..adec84e6298 100644 --- a/middleware/cache/cache.go +++ b/middleware/cache/cache.go @@ -135,14 +135,14 @@ func New(config ...Config) fiber.Handler { e.body = manager.getRaw(key + "_body") } // Set response headers from cache - c.Response().SetBodyRaw(e.body) - c.Response().SetStatusCode(e.status) - c.Response().Header.SetContentTypeBytes(e.ctype) + c.Context().Response.SetBodyRaw(e.body) + c.Context().Response.SetStatusCode(e.status) + c.Context().Response.Header.SetContentTypeBytes(e.ctype) if len(e.cencoding) > 0 { - c.Response().Header.SetBytesV(fiber.HeaderContentEncoding, e.cencoding) + c.Context().Response.Header.SetBytesV(fiber.HeaderContentEncoding, e.cencoding) } for k, v := range e.headers { - c.Response().Header.SetBytesV(k, v) + c.Context().Response.Header.SetBytesV(k, v) } // Set Cache-Control header if enabled if cfg.CacheControl { @@ -177,7 +177,7 @@ func New(config ...Config) fiber.Handler { } // Don't try to cache if body won't fit into cache - bodySize := uint(len(c.Response().Body())) + bodySize := uint(len(c.Context().Response.Body())) if cfg.MaxBytes > 0 && bodySize > cfg.MaxBytes { c.Set(cfg.CacheHeader, cacheUnreachable) return nil @@ -193,16 +193,16 @@ func New(config ...Config) fiber.Handler { } // Cache response - e.body = utils.CopyBytes(c.Response().Body()) - e.status = c.Response().StatusCode() - e.ctype = utils.CopyBytes(c.Response().Header.ContentType()) - e.cencoding = utils.CopyBytes(c.Response().Header.Peek(fiber.HeaderContentEncoding)) + e.body = utils.CopyBytes(c.Context().Response.Body()) + e.status = c.Context().Response.StatusCode() + e.ctype = utils.CopyBytes(c.Context().Response.Header.ContentType()) + e.cencoding = utils.CopyBytes(c.Context().Response.Header.Peek(fiber.HeaderContentEncoding)) // Store all response headers // (more: https://datatracker.ietf.org/doc/html/rfc2616#section-13.5.1) if cfg.StoreResponseHeaders { e.headers = make(map[string][]byte) - c.Response().Header.VisitAll( + c.Context().Response.Header.VisitAll( func(key, value []byte) { // create real copy keyS := string(key) diff --git a/middleware/cache/cache_test.go b/middleware/cache/cache_test.go index 8966ec7ac29..1c122c5060a 100644 --- a/middleware/cache/cache_test.go +++ b/middleware/cache/cache_test.go @@ -442,7 +442,7 @@ func Test_Cache_CustomNext(t *testing.T) { app.Use(New(Config{ Next: func(c fiber.Ctx) bool { - return c.Response().StatusCode() != fiber.StatusOK + return c.Context().Response.StatusCode() != fiber.StatusOK }, CacheControl: true, })) @@ -509,7 +509,7 @@ func Test_CustomExpiration(t *testing.T) { }})) app.Get("/", func(c fiber.Ctx) error { - c.Response().Header.Add("Cache-Time", "1") + c.Set("Cache-Time", "1") return c.SendString(strconv.FormatInt(time.Now().UnixNano(), 10)) }) @@ -553,7 +553,7 @@ func Test_AdditionalE2EResponseHeaders(t *testing.T) { })) app.Get("/", func(c fiber.Ctx) error { - c.Response().Header.Add("X-Foobar", "foobar") + c.Set("X-Foobar", "foobar") return c.SendString("hi") }) @@ -576,7 +576,7 @@ func Test_CacheHeader(t *testing.T) { app.Use(New(Config{ Expiration: 10 * time.Second, Next: func(c fiber.Ctx) bool { - return c.Response().StatusCode() != fiber.StatusOK + return c.Context().Response.StatusCode() != fiber.StatusOK }, })) @@ -843,7 +843,7 @@ func Benchmark_Cache_AdditionalHeaders(b *testing.B) { })) app.Get("/demo", func(c fiber.Ctx) error { - c.Response().Header.Add("X-Foobar", "foobar") + c.Set("X-Foobar", "foobar") return c.SendStatus(418) }) diff --git a/middleware/encryptcookie/encryptcookie.go b/middleware/encryptcookie/encryptcookie.go index 6f905168cd7..ba14208e8b6 100644 --- a/middleware/encryptcookie/encryptcookie.go +++ b/middleware/encryptcookie/encryptcookie.go @@ -34,19 +34,19 @@ func New(config ...Config) fiber.Handler { err := c.Next() // Encrypt response cookies - c.Response().Header.VisitAllCookie(func(key, _ []byte) { + c.Context().Response.Header.VisitAllCookie(func(key, _ []byte) { keyString := string(key) if !isDisabled(keyString, cfg.Except) { cookieValue := fasthttp.Cookie{} cookieValue.SetKeyBytes(key) - if c.Response().Header.Cookie(&cookieValue) { + if c.Context().Response.Header.Cookie(&cookieValue) { encryptedValue, err := cfg.Encryptor(string(cookieValue.Value()), cfg.Key) if err != nil { panic(err) } cookieValue.SetValue(encryptedValue) - c.Response().Header.SetCookie(&cookieValue) + c.Context().Response.Header.SetCookie(&cookieValue) } } }) diff --git a/middleware/etag/etag.go b/middleware/etag/etag.go index 312e557887f..5b2d54acfa4 100644 --- a/middleware/etag/etag.go +++ b/middleware/etag/etag.go @@ -34,16 +34,16 @@ func New(config ...Config) fiber.Handler { } // Don't generate ETags for invalid responses - if c.Response().StatusCode() != fiber.StatusOK { + if c.Context().Response.StatusCode() != fiber.StatusOK { return nil } - body := c.Response().Body() + body := c.Context().Response.Body() // Skips ETag if no response body is present if len(body) == 0 { return nil } // Skip ETag if header is already present - if c.Response().Header.PeekBytes(normalizedHeaderETag) != nil { + if c.Context().Response.Header.PeekBytes(normalizedHeaderETag) != nil { return nil } @@ -77,7 +77,7 @@ func New(config ...Config) fiber.Handler { return c.SendStatus(fiber.StatusNotModified) } // W/1 != W/2 || W/1 != 2 - c.Response().Header.SetCanonical(normalizedHeaderETag, etag) + c.Context().Response.Header.SetCanonical(normalizedHeaderETag, etag) return nil } @@ -89,7 +89,7 @@ func New(config ...Config) fiber.Handler { return c.SendStatus(fiber.StatusNotModified) } // 1 != 2 - c.Response().Header.SetCanonical(normalizedHeaderETag, etag) + c.Context().Response.Header.SetCanonical(normalizedHeaderETag, etag) return nil } diff --git a/middleware/filesystem/filesystem.go b/middleware/filesystem/filesystem.go index 0a0393bec45..883cdb0ad17 100644 --- a/middleware/filesystem/filesystem.go +++ b/middleware/filesystem/filesystem.go @@ -230,14 +230,14 @@ func New(config ...Config) fiber.Handler { if cfg.MaxAge > 0 { c.Set(fiber.HeaderCacheControl, cacheControlStr) } - c.Response().SetBodyStream(file, contentLength) + c.Context().Response.SetBodyStream(file, contentLength) return nil } if method == fiber.MethodHead { c.Context().Request.ResetBody() // Fasthttp should skipbody by default if HEAD? - c.Response().SkipBody = true - c.Response().Header.SetContentLength(contentLength) + c.Context().Response.SkipBody = true + c.Context().Response.Header.SetContentLength(contentLength) if err := file.Close(); err != nil { return fmt.Errorf("failed to close: %w", err) } @@ -305,14 +305,14 @@ func SendFile(c fiber.Ctx, filesystem fs.FS, path string) error { method := c.Method() if method == fiber.MethodGet { - c.Response().SetBodyStream(file, contentLength) + c.Context().Response.SetBodyStream(file, contentLength) return nil } if method == fiber.MethodHead { c.Context().Request.ResetBody() // Fasthttp should skipbody by default if HEAD? - c.Response().SkipBody = true - c.Response().Header.SetContentLength(contentLength) + c.Context().Response.SkipBody = true + c.Context().Response.Header.SetContentLength(contentLength) if err := file.Close(); err != nil { return fmt.Errorf("failed to close: %w", err) } diff --git a/middleware/idempotency/idempotency.go b/middleware/idempotency/idempotency.go index 923ce5ce9a0..a24bde363ed 100644 --- a/middleware/idempotency/idempotency.go +++ b/middleware/idempotency/idempotency.go @@ -117,9 +117,9 @@ func New(config ...Config) fiber.Handler { // Construct response res := &response{ - StatusCode: c.Response().StatusCode(), + StatusCode: c.Context().Response.StatusCode(), - Body: utils.CopyBytes(c.Response().Body()), + Body: utils.CopyBytes(c.Context().Response.Body()), } { headers := make(map[string][]string) diff --git a/middleware/limiter/limiter_fixed.go b/middleware/limiter/limiter_fixed.go index 1e2a1aa0e55..1ec0dad8c59 100644 --- a/middleware/limiter/limiter_fixed.go +++ b/middleware/limiter/limiter_fixed.go @@ -83,8 +83,8 @@ func (FixedWindow) New(cfg Config) fiber.Handler { err := c.Next() // Check for SkipFailedRequests and SkipSuccessfulRequests - if (cfg.SkipSuccessfulRequests && c.Response().StatusCode() < fiber.StatusBadRequest) || - (cfg.SkipFailedRequests && c.Response().StatusCode() >= fiber.StatusBadRequest) { + if (cfg.SkipSuccessfulRequests && c.Context().Response.StatusCode() < fiber.StatusBadRequest) || + (cfg.SkipFailedRequests && c.Context().Response.StatusCode() >= fiber.StatusBadRequest) { // Lock entry mux.Lock() e = manager.get(key) diff --git a/middleware/limiter/limiter_sliding.go b/middleware/limiter/limiter_sliding.go index a98593476ec..091fef6d644 100644 --- a/middleware/limiter/limiter_sliding.go +++ b/middleware/limiter/limiter_sliding.go @@ -114,8 +114,8 @@ func (SlidingWindow) New(cfg Config) fiber.Handler { err := c.Next() // Check for SkipFailedRequests and SkipSuccessfulRequests - if (cfg.SkipSuccessfulRequests && c.Response().StatusCode() < fiber.StatusBadRequest) || - (cfg.SkipFailedRequests && c.Response().StatusCode() >= fiber.StatusBadRequest) { + if (cfg.SkipSuccessfulRequests && c.Context().Response.StatusCode() < fiber.StatusBadRequest) || + (cfg.SkipFailedRequests && c.Context().Response.StatusCode() >= fiber.StatusBadRequest) { // Lock entry mux.Lock() e = manager.get(key) diff --git a/middleware/logger/default_logger.go b/middleware/logger/default_logger.go index 5744fd1a5f7..40cabde1185 100644 --- a/middleware/logger/default_logger.go +++ b/middleware/logger/default_logger.go @@ -33,7 +33,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { buf.WriteString( fmt.Sprintf("%s |%s %3d %s| %13v | %15s |%s %-7s %s| %-"+data.ErrPaddingStr+"s %s\n", data.Timestamp.Load().(string), //nolint:forcetypeassert // Timestamp is always a string - statusColor(c.Response().StatusCode(), colors), c.Response().StatusCode(), colors.Reset, + statusColor(c.Context().Response.StatusCode(), colors), c.Context().Response.StatusCode(), colors.Reset, data.Stop.Sub(data.Start), c.IP(), methodColor(c.Method(), colors), c.Method(), colors.Reset, @@ -66,7 +66,7 @@ func defaultLoggerInstance(c fiber.Ctx, data *Data, cfg Config) error { buf.WriteString(" | ") // Status Code with 3 fixed width, right aligned - fixedWidth(strconv.Itoa(c.Response().StatusCode()), 3, true) + fixedWidth(strconv.Itoa(c.Context().Response.StatusCode()), 3, true) buf.WriteString(" | ") // Duration with 13 fixed width, right aligned diff --git a/middleware/logger/logger_test.go b/middleware/logger/logger_test.go index 0aa517bcf56..fea5359d553 100644 --- a/middleware/logger/logger_test.go +++ b/middleware/logger/logger_test.go @@ -115,7 +115,7 @@ func Test_Logger_Done(t *testing.T) { app.Use(New(Config{ Done: func(c fiber.Ctx, logString []byte) { - if c.Response().StatusCode() == fiber.StatusOK { + if c.Context().Response.StatusCode() == fiber.StatusOK { _, err := buf.Write(logString) require.NoError(t, err) } diff --git a/middleware/logger/tags.go b/middleware/logger/tags.go index cf1b49ff844..fc6b315f121 100644 --- a/middleware/logger/tags.go +++ b/middleware/logger/tags.go @@ -90,16 +90,16 @@ func createTagMap(cfg *Config) map[string]LogFunc { return appendInt(output, len(c.Req().Body())) }, TagBytesSent: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) { - if c.Response().Header.ContentLength() < 0 { + if c.Context().Response.Header.ContentLength() < 0 { return appendInt(output, 0) } - return appendInt(output, len(c.Response().Body())) + return appendInt(output, len(c.Context().Response.Body())) }, TagRoute: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) { return output.WriteString(c.Route().Path) }, TagResBody: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) { - return output.Write(c.Response().Body()) + return output.Write(c.Context().Response.Body()) }, TagReqHeaders: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) { out := make(map[string][]string, 0) @@ -184,9 +184,9 @@ func createTagMap(cfg *Config) map[string]LogFunc { TagStatus: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) { if cfg.enableColors { colors := c.App().Config().ColorScheme - return output.WriteString(fmt.Sprintf("%s%3d%s", statusColor(c.Response().StatusCode(), colors), c.Response().StatusCode(), colors.Reset)) + return output.WriteString(fmt.Sprintf("%s%3d%s", statusColor(c.Context().Response.StatusCode(), colors), c.Context().Response.StatusCode(), colors.Reset)) } - return appendInt(output, c.Response().StatusCode()) + return appendInt(output, c.Context().Response.StatusCode()) }, TagMethod: func(output Buffer, c fiber.Ctx, _ *Data, _ string) (int, error) { if cfg.enableColors { diff --git a/middleware/proxy/proxy.go b/middleware/proxy/proxy.go index 746ee7ab8d4..2145f908a6e 100644 --- a/middleware/proxy/proxy.go +++ b/middleware/proxy/proxy.go @@ -65,7 +65,7 @@ func Balancer(config Config) fiber.Handler { // Set request and response req := &c.Context().Request - res := c.Response() + res := &c.Context().Response // Don't proxy "Connection" header req.Header.Del(fiber.HeaderConnection) @@ -173,7 +173,7 @@ func doAction( } req := &c.Context().Request - res := c.Response() + res := &c.Context().Response originalURL := utils.CopyString(c.OriginalURL()) defer req.SetRequestURI(originalURL) diff --git a/middleware/proxy/proxy_test.go b/middleware/proxy/proxy_test.go index 7f10273d57f..78e43553d71 100644 --- a/middleware/proxy/proxy_test.go +++ b/middleware/proxy/proxy_test.go @@ -343,7 +343,7 @@ func Test_Proxy_Modify_Response(t *testing.T) { app.Use(Balancer(Config{ Servers: []string{addr}, ModifyResponse: func(c fiber.Ctx) error { - c.Response().SetStatusCode(fiber.StatusOK) + c.Res().Status(fiber.StatusOK) return c.SendString("modified response") }, })) @@ -645,7 +645,7 @@ func Test_Proxy_Do_HTTP_Prefix_URL(t *testing.T) { if err := Do(c, url); err != nil { return err } - c.Response().Header.Del(fiber.HeaderServer) + c.Context().Response.Header.Del(fiber.HeaderServer) return nil }) diff --git a/middleware/session/session.go b/middleware/session/session.go index 259d062c3e5..921d2816f19 100644 --- a/middleware/session/session.go +++ b/middleware/session/session.go @@ -216,7 +216,7 @@ func (s *Session) SetExpiry(exp time.Duration) { func (s *Session) setSession() { if s.config.source == SourceHeader { s.ctx.Context().Request.Header.SetBytesV(s.config.sessionName, []byte(s.id)) - s.ctx.Response().Header.SetBytesV(s.config.sessionName, []byte(s.id)) + s.ctx.Context().Response.Header.SetBytesV(s.config.sessionName, []byte(s.id)) } else { fcookie := fasthttp.AcquireCookie() fcookie.SetKey(s.config.sessionName) @@ -240,7 +240,7 @@ func (s *Session) setSession() { default: fcookie.SetSameSite(fasthttp.CookieSameSiteLaxMode) } - s.ctx.Response().Header.SetCookie(fcookie) + s.ctx.Context().Response.Header.SetCookie(fcookie) fasthttp.ReleaseCookie(fcookie) } } @@ -248,10 +248,10 @@ func (s *Session) setSession() { func (s *Session) delSession() { if s.config.source == SourceHeader { s.ctx.Context().Request.Header.Del(s.config.sessionName) - s.ctx.Response().Header.Del(s.config.sessionName) + s.ctx.Context().Response.Header.Del(s.config.sessionName) } else { s.ctx.Context().Request.Header.DelCookie(s.config.sessionName) - s.ctx.Response().Header.DelCookie(s.config.sessionName) + s.ctx.Context().Response.Header.DelCookie(s.config.sessionName) fcookie := fasthttp.AcquireCookie() fcookie.SetKey(s.config.sessionName) @@ -271,7 +271,7 @@ func (s *Session) delSession() { fcookie.SetSameSite(fasthttp.CookieSameSiteLaxMode) } - s.ctx.Response().Header.SetCookie(fcookie) + s.ctx.Context().Response.Header.SetCookie(fcookie) fasthttp.ReleaseCookie(fcookie) } } diff --git a/middleware/session/session_test.go b/middleware/session/session_test.go index b3172262896..30ac870015c 100644 --- a/middleware/session/session_test.go +++ b/middleware/session/session_test.go @@ -337,8 +337,8 @@ func Test_Session_Save(t *testing.T) { // save session err = sess.Save() require.NoError(t, err) - require.Equal(t, store.getSessionID(ctx), string(ctx.Response().Header.Peek(store.sessionName))) - require.Equal(t, store.getSessionID(ctx), ctx.Get(store.sessionName)) + require.Equal(t, store.getSessionID(ctx), ctx.Res().Get(store.sessionName)) + require.Equal(t, store.getSessionID(ctx), ctx.Req().Get(store.sessionName)) }) } @@ -434,8 +434,8 @@ func Test_Session_Destroy(t *testing.T) { err = sess.Destroy() require.NoError(t, err) - require.Equal(t, "", string(ctx.Response().Header.Peek(store.sessionName))) - require.Equal(t, "", ctx.Get(store.sessionName)) + require.Equal(t, "", ctx.Res().Get(store.sessionName)) + require.Equal(t, "", ctx.Req().Get(store.sessionName)) }) } @@ -468,7 +468,7 @@ func Test_Session_Cookie(t *testing.T) { require.NoError(t, sess.Save()) // cookie should be set on Save ( even if empty data ) - require.Len(t, ctx.Response().Header.PeekCookie(store.sessionName), 84) + require.Len(t, ctx.Context().Response.Header.PeekCookie(store.sessionName), 84) } // go test -run Test_Session_Cookie_In_Response @@ -585,8 +585,8 @@ func Test_Session_Reset(t *testing.T) { require.NoError(t, err) // Check that the session id is not in the header or cookie anymore - require.Equal(t, "", string(ctx.Response().Header.Peek(store.sessionName))) - require.Equal(t, "", ctx.Get(store.sessionName)) + require.Equal(t, "", ctx.Res().Get(store.sessionName)) + require.Equal(t, "", ctx.Req().Get(store.sessionName)) }) } diff --git a/middleware/session/store.go b/middleware/session/store.go index e480164efb7..74b0baade42 100644 --- a/middleware/session/store.go +++ b/middleware/session/store.go @@ -123,7 +123,7 @@ func (s *Store) getSessionID(c fiber.Ctx) string { func (s *Store) responseCookies(c fiber.Ctx) (string, error) { // Get key from response cookie - cookieValue := c.Response().Header.PeekCookie(s.sessionName) + cookieValue := c.Context().Response.Header.PeekCookie(s.sessionName) if len(cookieValue) == 0 { return "", nil } diff --git a/redirect.go b/redirect.go index f03981b8181..d66a2cb3e0b 100644 --- a/redirect.go +++ b/redirect.go @@ -37,8 +37,9 @@ type Redirect struct { c *DefaultCtx // Embed ctx status int // Status code of redirection. Default: StatusFound - messages []string // Flash messages - oldInput map[string]string // Old input data + messages []string // Flash messages + redirectionMessages []string // Messages of the previous redirect + oldInput map[string]string // Old input data } // RedirectConfig A config to use with Redirect().Route() @@ -71,6 +72,7 @@ func ReleaseRedirect(r *Redirect) { func (r *Redirect) release() { r.status = 302 r.messages = r.messages[:0] + r.redirectionMessages = r.redirectionMessages[:0] // reset map for k := range r.oldInput { delete(r.oldInput, k) @@ -119,7 +121,7 @@ func (r *Redirect) WithInput() *Redirect { // Messages Get flash messages. func (r *Redirect) Messages() map[string]string { - msgs := r.c.redirectionMessages + msgs := r.redirectionMessages flashMessages := make(map[string]string, len(msgs)) for _, msg := range msgs { @@ -135,7 +137,7 @@ func (r *Redirect) Messages() map[string]string { // Message Get flash message by key. func (r *Redirect) Message(key string) string { - msgs := r.c.redirectionMessages + msgs := r.redirectionMessages for _, msg := range msgs { k, v := parseMessage(msg) @@ -149,7 +151,7 @@ func (r *Redirect) Message(key string) string { // OldInputs Get old input data. func (r *Redirect) OldInputs() map[string]string { - msgs := r.c.redirectionMessages + msgs := r.redirectionMessages oldInputs := make(map[string]string, len(msgs)) for _, msg := range msgs { @@ -165,7 +167,7 @@ func (r *Redirect) OldInputs() map[string]string { // OldInput Get old input data by key. func (r *Redirect) OldInput(key string) string { - msgs := r.c.redirectionMessages + msgs := r.redirectionMessages for _, msg := range msgs { k, v := parseMessage(msg) @@ -179,7 +181,7 @@ func (r *Redirect) OldInput(key string) string { // 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.res.setCanonical(HeaderLocation, location) r.c.Status(r.status) return nil @@ -279,10 +281,10 @@ func (r *Redirect) setFlash() { for { commaPos = findNextNonEscapedCharsetPosition(cookieValue, []byte(CookieDataSeparator)) if commaPos == -1 { - r.c.redirectionMessages = append(r.c.redirectionMessages, strings.Trim(cookieValue, " ")) + r.redirectionMessages = append(r.redirectionMessages, strings.Trim(cookieValue, " ")) break } - r.c.redirectionMessages = append(r.c.redirectionMessages, strings.Trim(cookieValue[:commaPos], " ")) + r.redirectionMessages = append(r.redirectionMessages, strings.Trim(cookieValue[:commaPos], " ")) cookieValue = cookieValue[commaPos+1:] } diff --git a/redirect_test.go b/redirect_test.go index c69e0548d71..e95cd6ba64f 100644 --- a/redirect_test.go +++ b/redirect_test.go @@ -25,13 +25,13 @@ func Test_Redirect_To(t *testing.T) { err := c.Redirect().To("http://default.com") require.NoError(t, err) - require.Equal(t, 302, c.Response().StatusCode()) - require.Equal(t, "http://default.com", string(c.Response().Header.Peek(HeaderLocation))) + require.Equal(t, 302, c.Context().Response.StatusCode()) + require.Equal(t, "http://default.com", c.Res().Get(HeaderLocation)) err = c.Redirect().Status(301).To("http://example.com") require.NoError(t, err) - require.Equal(t, 301, c.Response().StatusCode()) - require.Equal(t, "http://example.com", string(c.Response().Header.Peek(HeaderLocation))) + require.Equal(t, 301, c.Context().Response.StatusCode()) + require.Equal(t, "http://example.com", c.Res().Get(HeaderLocation)) } // go test -run Test_Redirect_Route_WithParams @@ -49,8 +49,8 @@ func Test_Redirect_Route_WithParams(t *testing.T) { }, }) require.NoError(t, err) - require.Equal(t, 302, c.Response().StatusCode()) - require.Equal(t, "/user/fiber", string(c.Response().Header.Peek(HeaderLocation))) + require.Equal(t, 302, c.Context().Response.StatusCode()) + require.Equal(t, "/user/fiber", c.Res().Get(HeaderLocation)) } // go test -run Test_Redirect_Route_WithParams_WithQueries @@ -69,9 +69,9 @@ func Test_Redirect_Route_WithParams_WithQueries(t *testing.T) { Queries: map[string]string{"data[0][name]": "john", "data[0][age]": "10", "test": "doe"}, }) require.NoError(t, err) - require.Equal(t, 302, c.Response().StatusCode()) + require.Equal(t, 302, c.Context().Response.StatusCode()) // analysis of query parameters with url parsing, since a map pass is always randomly ordered - location, err := url.Parse(string(c.Response().Header.Peek(HeaderLocation))) + location, err := url.Parse(c.Res().Get(HeaderLocation)) require.NoError(t, err, "url.Parse(location)") require.Equal(t, "/user/fiber", location.Path) require.Equal(t, url.Values{"data[0][name]": []string{"john"}, "data[0][age]": []string{"10"}, "test": []string{"doe"}}, location.Query()) @@ -92,8 +92,8 @@ func Test_Redirect_Route_WithOptionalParams(t *testing.T) { }, }) require.NoError(t, err) - require.Equal(t, 302, c.Response().StatusCode()) - require.Equal(t, "/user/fiber", string(c.Response().Header.Peek(HeaderLocation))) + require.Equal(t, 302, c.Context().Response.StatusCode()) + require.Equal(t, "/user/fiber", c.Res().Get(HeaderLocation)) } // go test -run Test_Redirect_Route_WithOptionalParamsWithoutValue @@ -107,8 +107,8 @@ func Test_Redirect_Route_WithOptionalParamsWithoutValue(t *testing.T) { err := c.Redirect().Route("user") require.NoError(t, err) - require.Equal(t, 302, c.Response().StatusCode()) - require.Equal(t, "/user/", string(c.Response().Header.Peek(HeaderLocation))) + require.Equal(t, 302, c.Context().Response.StatusCode()) + require.Equal(t, "/user/", c.Res().Get(HeaderLocation)) } // go test -run Test_Redirect_Route_WithGreedyParameters @@ -126,8 +126,8 @@ func Test_Redirect_Route_WithGreedyParameters(t *testing.T) { }, }) require.NoError(t, err) - require.Equal(t, 302, c.Response().StatusCode()) - require.Equal(t, "/user/test/routes", string(c.Response().Header.Peek(HeaderLocation))) + require.Equal(t, 302, c.Context().Response.StatusCode()) + require.Equal(t, "/user/test/routes", c.Res().Get(HeaderLocation)) } // go test -run Test_Redirect_Back @@ -141,11 +141,11 @@ func Test_Redirect_Back(t *testing.T) { err := c.Redirect().Back("/") require.NoError(t, err) - require.Equal(t, 302, c.Response().StatusCode()) - require.Equal(t, "/", string(c.Response().Header.Peek(HeaderLocation))) + require.Equal(t, 302, c.Context().Response.StatusCode()) + require.Equal(t, "/", c.Res().Get(HeaderLocation)) err = c.Redirect().Back() - require.Equal(t, 500, c.Response().StatusCode()) + require.Equal(t, 500, c.Context().Response.StatusCode()) require.ErrorAs(t, err, &ErrRedirectBackNoFallback) } @@ -164,9 +164,9 @@ func Test_Redirect_Back_WithReferer(t *testing.T) { c.Context().Request.Header.Set(HeaderReferer, "/back") err := c.Redirect().Back("/") require.NoError(t, err) - require.Equal(t, 302, c.Response().StatusCode()) + require.Equal(t, 302, c.Context().Response.StatusCode()) require.Equal(t, "/back", c.Get(HeaderReferer)) - require.Equal(t, "/back", string(c.Response().Header.Peek(HeaderLocation))) + require.Equal(t, "/back", c.Res().Get(HeaderLocation)) } // go test -run Test_Redirect_Route_WithFlashMessages @@ -182,7 +182,7 @@ func Test_Redirect_Route_WithFlashMessages(t *testing.T) { err := c.Redirect().With("success", "1").With("message", "test").Route("user") require.NoError(t, err) - require.Equal(t, 302, c.Response().StatusCode()) + require.Equal(t, 302, c.Context().Response.StatusCode()) require.Equal(t, "/user", string(c.Response().Header.Peek(HeaderLocation))) equal := c.GetRespHeader(HeaderSetCookie) == "fiber_flash=success:1,message:test; path=/; SameSite=Lax" || c.GetRespHeader(HeaderSetCookie) == "fiber_flash=message:test,success:1; path=/; SameSite=Lax" @@ -207,7 +207,7 @@ func Test_Redirect_Route_WithOldInput(t *testing.T) { c.Context().URI().SetQueryString("id=1&name=tom") err := c.Redirect().With("success", "1").With("message", "test").WithInput().Route("user") require.NoError(t, err) - require.Equal(t, 302, c.Response().StatusCode()) + require.Equal(t, 302, c.Context().Response.StatusCode()) require.Equal(t, "/user", string(c.Response().Header.Peek(HeaderLocation))) require.Contains(t, c.GetRespHeader(HeaderSetCookie), "fiber_flash=") @@ -356,7 +356,7 @@ func Benchmark_Redirect_Route(b *testing.B) { } require.NoError(b, err) - require.Equal(b, 302, c.Response().StatusCode()) + require.Equal(b, 302, c.Context().Response.StatusCode()) require.Equal(b, "/user/fiber", string(c.Response().Header.Peek(HeaderLocation))) } @@ -384,7 +384,7 @@ func Benchmark_Redirect_Route_WithQueries(b *testing.B) { } require.NoError(b, err) - require.Equal(b, 302, c.Response().StatusCode()) + require.Equal(b, 302, c.Context().Response.StatusCode()) // analysis of query parameters with url parsing, since a map pass is always randomly ordered location, err := url.Parse(string(c.Response().Header.Peek(HeaderLocation))) require.NoError(b, err, "url.Parse(location)") @@ -411,7 +411,7 @@ func Benchmark_Redirect_Route_WithFlashMessages(b *testing.B) { } require.NoError(b, err) - require.Equal(b, 302, c.Response().StatusCode()) + require.Equal(b, 302, c.Context().Response.StatusCode()) require.Equal(b, "/user", string(c.Response().Header.Peek(HeaderLocation))) equal := c.GetRespHeader(HeaderSetCookie) == "fiber_flash=success:1,message:test; path=/; SameSite=Lax" || c.GetRespHeader(HeaderSetCookie) == "fiber_flash=message:test,success:1; path=/; SameSite=Lax" diff --git a/request.go b/request.go index ff3c3fad14f..fe9f61e056c 100644 --- a/request.go +++ b/request.go @@ -499,7 +499,7 @@ func (r *Request) Fresh() bool { // if-none-match if noneMatch != "" && noneMatch != "*" { - etag := r.app.getString(r.ctx.Response().Header.Peek(HeaderETag)) + etag := r.ctx.Res().Get(HeaderETag) if etag == "" { return false } @@ -508,7 +508,7 @@ func (r *Request) Fresh() bool { } if modifiedSince != "" { - lastModified := r.app.getString(r.ctx.Response().Header.Peek(HeaderLastModified)) + lastModified := r.ctx.Res().Get(HeaderLastModified) if lastModified != "" { lastModifiedTime, err := http.ParseTime(lastModified) if err != nil { @@ -556,7 +556,7 @@ func (r *Request) Subdomains(offset ...int) []string { // XHR returns a Boolean property, that is true, if the request's X-Requested-With header field is XMLHttpRequest, // indicating that the request was issued by a client library (such as jQuery). func (r *Request) XHR() bool { - return utils.EqualFold(r.Get(HeaderXRequestedWith), "xmlhttprequest") + return utils.EqualFold(r.fasthttp.Header.Peek(HeaderXRequestedWith), []byte("xmlhttprequest")) } // Params is used to get the route parameters. diff --git a/response.go b/response.go new file mode 100644 index 00000000000..ca059751ef6 --- /dev/null +++ b/response.go @@ -0,0 +1,528 @@ +package fiber + +import ( + "fmt" + "path/filepath" + "strings" + "sync" + "text/template" + "time" + + "github.com/gofiber/utils/v2" + "github.com/valyala/bytebufferpool" + "github.com/valyala/fasthttp" +) + +var ( + sendFileOnce sync.Once + sendFileFS *fasthttp.FS + sendFileHandler fasthttp.RequestHandler +) + +type Response struct { + app *App + ctx Ctx + fasthttp *fasthttp.Response + viewBindMap sync.Map // Default view map to bind template engine +} + +// ResFmt associates a Content Type to a fiber.Handler for c.Format +type ResFmt struct { + MediaType string + Handler func(Ctx) error +} + +func (r *Response) App() *App { + return r.app +} + +// Append the specified value to the HTTP response header field. +// If the header is not already set, it creates the header with the specified value. +func (r *Response) Append(field string, values ...string) { + if len(values) == 0 { + return + } + h := r.app.getString(r.fasthttp.Header.Peek(field)) + originalH := h + for _, value := range values { + if len(h) == 0 { + h = value + } else if h != value && !strings.HasPrefix(h, value+",") && !strings.HasSuffix(h, " "+value) && + !strings.Contains(h, " "+value+",") { + h += ", " + value + } + } + if originalH != h { + r.Set(field, h) + } +} + +func (r *Response) Attachment(filename ...string) { + if len(filename) > 0 { + fname := filepath.Base(filename[0]) + r.Type(filepath.Ext(fname)) + + r.setCanonical(HeaderContentDisposition, `attachment; filename="`+r.app.quoteString(fname)+`"`) + return + } + r.setCanonical(HeaderContentDisposition, "attachment") +} + +// ViewBind adds vars to default view var map binding to template engine. +// Variables are read by the Render method and may be overwritten. +func (r *Response) ViewBind(vars Map) error { + // init viewBindMap - lazy map + for k, v := range vars { + r.viewBindMap.Store(k, v) + } + return nil +} + +// Cookie sets a cookie by passing a cookie struct. +func (r *Response) 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) + } + fcookie.SetSecure(cookie.Secure) + fcookie.SetHTTPOnly(cookie.HTTPOnly) + + switch utils.ToLower(cookie.SameSite) { + case CookieSameSiteStrictMode: + fcookie.SetSameSite(fasthttp.CookieSameSiteStrictMode) + case CookieSameSiteNoneMode: + fcookie.SetSameSite(fasthttp.CookieSameSiteNoneMode) + case CookieSameSiteDisabled: + fcookie.SetSameSite(fasthttp.CookieSameSiteDisabled) + default: + fcookie.SetSameSite(fasthttp.CookieSameSiteLaxMode) + } + + r.fasthttp.Header.SetCookie(fcookie) + fasthttp.ReleaseCookie(fcookie) +} + +// ClearCookie expires a specific cookie by key on the client side. +// If no key is provided it expires all cookies that came with the request. +func (r *Response) ClearCookie(key ...string) { + if len(key) > 0 { + for i := range key { + r.fasthttp.Header.DelClientCookie(key[i]) + } + return + } + r.ctx.Context().Request.Header.VisitAllCookie(func(k, _ []byte) { + r.fasthttp.Header.DelClientCookieBytes(k) + }) +} + +// 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. +func (r *Response) Download(file string, filename ...string) error { + var fname string + if len(filename) > 0 { + fname = filename[0] + } else { + fname = filepath.Base(file) + } + r.setCanonical(HeaderContentDisposition, `attachment; filename="`+r.app.quoteString(fname)+`"`) + return r.SendFile(file) +} + +// Format performs content-negotiation on the Accept HTTP header. +// It uses Accepts to select a proper format and calls the matching +// user-provided handler function. +// If no accepted format is found, and a format with MediaType "default" is given, +// that default handler is called. If no format is found and no default is given, +// StatusNotAcceptable is sent. +func (r *Response) Format(handlers ...ResFmt) error { + if len(handlers) == 0 { + return ErrNoHandlers + } + + r.Vary(HeaderAccept) + + if r.ctx.Get(HeaderAccept) == "" { + r.fasthttp.Header.SetContentType(handlers[0].MediaType) + return handlers[0].Handler(r.ctx) + } + + // Using an int literal as the slice capacity allows for the slice to be + // allocated on the stack. The number was chosen arbitrarily as an + // approximation of the maximum number of content types a user might handle. + // If the user goes over, it just causes allocations, so it's not a problem. + types := make([]string, 0, 8) + var defaultHandler Handler + for _, h := range handlers { + if h.MediaType == "default" { + defaultHandler = h.Handler + continue + } + types = append(types, h.MediaType) + } + accept := r.ctx.Accepts(types...) + + if accept == "" { + if defaultHandler == nil { + return r.SendStatus(StatusNotAcceptable) + } + return defaultHandler(r.ctx) + } + + for _, h := range handlers { + if h.MediaType == accept { + r.fasthttp.Header.SetContentType(h.MediaType) + return h.Handler(r.ctx) + } + } + + return fmt.Errorf("%w: format: an Accept was found but no handler was called", errUnreachable) +} + +// Get returns the HTTP response header specified by field. +// Field names are case-insensitive. +// Returned value is only valid within the handler. Do not store any references. +// Make copies or use the Immutable setting instead. +func (r *Response) Get(key string, defaultValue ...string) string { + return defaultString(r.app.getString(r.fasthttp.Header.Peek(key)), defaultValue) +} + +// JSON converts any interface or string to JSON. +// Array and slice values encode as JSON arrays, +// except that []byte encodes as a base64-encoded string, +// and a nil slice encodes as the null JSON value. +// If the ctype parameter is given, this method will set the +// Content-Type header equal to ctype. If ctype is not given, +// The Content-Type header will be set to application/json. +func (r *Response) JSON(data any, ctype ...string) error { + raw, err := r.app.config.JSONEncoder(data) + if err != nil { + return err + } + r.fasthttp.SetBodyRaw(raw) + if len(ctype) > 0 { + r.fasthttp.Header.SetContentType(ctype[0]) + } else { + r.fasthttp.Header.SetContentType(MIMEApplicationJSON) + } + return nil +} + +// JSONP sends a JSON response with JSONP support. +// This method is identical to JSON, except that it opts-in to JSONP callback support. +// By default, the callback name is simply callback. +func (r *Response) JSONP(data any, callback ...string) error { + raw, err := r.app.config.JSONEncoder(data) + if err != nil { + return err + } + + var result, cb string + + if len(callback) > 0 { + cb = callback[0] + } else { + cb = "callback" + } + + result = cb + "(" + r.app.getString(raw) + ");" + + r.setCanonical(HeaderXContentTypeOptions, "nosniff") + r.fasthttp.Header.SetContentType(MIMETextJavaScriptCharsetUTF8) + return r.SendString(result) +} + +// Links joins the links followed by the property to populate the response's Link HTTP header field. +func (r *Response) Links(link ...string) { + if len(link) == 0 { + return + } + bb := bytebufferpool.Get() + for i := range link { + if i%2 == 0 { + bb.WriteByte('<') + bb.WriteString(link[i]) + bb.WriteByte('>') + } else { + bb.WriteString(`; rel="` + link[i] + `",`) + } + } + r.setCanonical(HeaderLink, strings.TrimRight(r.app.getString(bb.Bytes()), ",")) + bytebufferpool.Put(bb) +} + +// Location sets the response Location HTTP header to the specified path parameter. +func (r *Response) Location(path string) { + r.setCanonical(HeaderLocation, path) +} + +// Render a template with data and sends a text/html response. +// We support the following engines: https://github.com/gofiber/template +func (r *Response) Render(name string, bind Map, layouts ...string) error { + // Get new buffer from pool + buf := bytebufferpool.Get() + defer bytebufferpool.Put(buf) + + // Initialize empty bind map if bind is nil + if bind == nil { + bind = make(Map) + } + + // Pass-locals-to-views, bind, appListKeys + r.renderExtensions(bind) + + var rendered bool + for i := len(r.app.mountFields.appListKeys) - 1; i >= 0; i-- { + prefix := r.app.mountFields.appListKeys[i] + app := r.app.mountFields.appList[prefix] + if prefix == "" || strings.Contains(r.ctx.OriginalURL(), prefix) { + if len(layouts) == 0 && app.config.ViewsLayout != "" { + layouts = []string{ + app.config.ViewsLayout, + } + } + + // Render template from Views + if app.config.Views != nil { + if err := app.config.Views.Render(buf, name, bind, layouts...); err != nil { + return fmt.Errorf("failed to render: %w", err) + } + + rendered = true + break + } + } + } + + if !rendered { + // Render raw template using 'name' as filepath if no engine is set + var tmpl *template.Template + if _, err := readContent(buf, name); err != nil { + return err + } + // Parse template + tmpl, err := template.New("").Parse(r.app.getString(buf.Bytes())) + if err != nil { + return fmt.Errorf("failed to parse: %w", err) + } + buf.Reset() + // Render template + if err := tmpl.Execute(buf, bind); err != nil { + return fmt.Errorf("failed to execute: %w", err) + } + } + + // Set Content-Type to text/html + r.fasthttp.Header.SetContentType(MIMETextHTMLCharsetUTF8) + // Set rendered template to body + r.fasthttp.SetBody(buf.Bytes()) + + return nil +} + +func (r *Response) renderExtensions(bind any) { + if bindMap, ok := bind.(Map); ok { + // Bind view map + r.viewBindMap.Range(func(key, value any) bool { + keyValue, ok := key.(string) + if !ok { + return true + } + if _, ok := bindMap[keyValue]; !ok { + bindMap[keyValue] = value + } + return true + }) + + // Check if the PassLocalsToViews option is enabled (by default it is disabled) + if r.app.config.PassLocalsToViews { + // Loop through each local and set it in the map + r.ctx.Context().VisitUserValues(func(key []byte, val any) { + // check if bindMap doesn't contain the key + if _, ok := bindMap[r.app.getString(key)]; !ok { + // Set the key and value in the bindMap + bindMap[r.app.getString(key)] = val + } + }) + } + } + + if len(r.app.mountFields.appListKeys) == 0 { + r.app.generateAppListKeys() + } +} + +// Send sets the HTTP response body without copying it. +// From this point onward the body argument must not be changed. +func (r *Response) Send(body []byte) error { + // Write response body + r.fasthttp.SetBodyRaw(body) + return nil +} + +// SendFile transfers the file from the given path. +// The file is not compressed by default, enable this by passing a 'true' argument +// Sets the Content-Type response HTTP header field based on the filenames extension. +func (r *Response) SendFile(file string, compress ...bool) error { + // Save the filename, we will need it in the error message if the file isn't found + filename := file + + // https://github.com/valyala/fasthttp/blob/c7576cc10cabfc9c993317a2d3f8355497bea156/fs.go#L129-L134 + sendFileOnce.Do(func() { + const cacheDuration = 10 * time.Second + sendFileFS = &fasthttp.FS{ + Root: "", + AllowEmptyRoot: true, + GenerateIndexPages: false, + AcceptByteRange: true, + Compress: true, + CompressedFileSuffix: r.app.config.CompressedFileSuffix, + CacheDuration: cacheDuration, + IndexNames: []string{"index.html"}, + PathNotFound: func(ctx *fasthttp.RequestCtx) { + ctx.Response.SetStatusCode(StatusNotFound) + }, + } + sendFileHandler = sendFileFS.NewRequestHandler() + }) + + // Keep original path for mutable params + r.ctx.Req().pathOriginal = utils.CopyString(r.ctx.Req().pathOriginal) + // Disable compression + if len(compress) == 0 || !compress[0] { + // https://github.com/valyala/fasthttp/blob/7cc6f4c513f9e0d3686142e0a1a5aa2f76b3194a/fs.go#L55 + r.ctx.Context().Request.Header.Del(HeaderAcceptEncoding) + } + // copy of https://github.com/valyala/fasthttp/blob/7cc6f4c513f9e0d3686142e0a1a5aa2f76b3194a/fs.go#L103-L121 with small adjustments + if len(file) == 0 || !filepath.IsAbs(file) { + // extend relative path to absolute path + hasTrailingSlash := len(file) > 0 && (file[len(file)-1] == '/' || file[len(file)-1] == '\\') + + var err error + file = filepath.FromSlash(file) + if file, err = filepath.Abs(file); err != nil { + return fmt.Errorf("failed to determine abs file path: %w", err) + } + if hasTrailingSlash { + file += "/" + } + } + // convert the path to forward slashes regardless the OS in order to set the URI properly + // the handler will convert back to OS path separator before opening the file + file = filepath.ToSlash(file) + + // Restore the original requested URL + originalURL := utils.CopyString(r.ctx.OriginalURL()) + defer r.ctx.Context().Request.SetRequestURI(originalURL) + // Set new URI for fileHandler + r.ctx.Context().Request.SetRequestURI(file) + // Save status code + status := r.fasthttp.StatusCode() + // Serve file + sendFileHandler(r.ctx.Context()) + // Get the status code which is set by fasthttp + fsStatus := r.fasthttp.StatusCode() + // Set the status code set by the user if it is different from the fasthttp status code and 200 + if status != fsStatus && status != StatusOK { + r.Status(status) + } + // Check for error + if status != StatusNotFound && fsStatus == StatusNotFound { + return NewError(StatusNotFound, fmt.Sprintf("sendfile: file %s not found", filename)) + } + return nil +} + +// SendStatus sets the HTTP status code and if the response body is empty, +// it sets the correct status message in the body. +func (r *Response) SendStatus(status int) error { + r.Status(status) + + // Only set status body when there is no response body + if len(r.fasthttp.Body()) == 0 { + return r.SendString(utils.StatusMessage(status)) + } + + return nil +} + +// SendString sets the HTTP response body for string types. +// This means no type assertion, recommended for faster performance +func (r *Response) SendString(body string) error { + r.fasthttp.SetBodyString(body) + return nil +} + +// Set sets the response's HTTP header field to the specified key, value. +func (r *Response) Set(key, val string) { + r.fasthttp.Header.Set(key, val) +} + +// setCanonical is the same as set, but it assumes key is already in canonical form, +// making it more efficient. +func (r *Response) setCanonical(key, val string) { + r.fasthttp.Header.SetCanonical(utils.UnsafeBytes(key), utils.UnsafeBytes(val)) +} + +// Status sets the HTTP status for the response. +// This method is chainable. +func (r *Response) Status(status int) *Response { + r.fasthttp.SetStatusCode(status) + return r +} + +// Type sets the Content-Type HTTP header to the MIME type specified by the file extension. +func (r *Response) Type(extension string, charset ...string) *Response { + if len(charset) > 0 { + r.fasthttp.Header.SetContentType(utils.GetMIME(extension) + "; charset=" + charset[0]) + } else { + r.fasthttp.Header.SetContentType(utils.GetMIME(extension)) + } + return r +} + +// 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. +func (r *Response) Vary(fields ...string) { + r.Append(HeaderVary, fields...) +} + +// Write appends p into response body. +func (r *Response) Write(p []byte) (int, error) { + r.fasthttp.AppendBody(p) + return len(p), nil +} + +// Writef appends f & a into response body writer. +func (r *Response) Writef(f string, a ...any) (int, error) { + //nolint:wrapcheck // This must not be wrapped + return fmt.Fprintf(r.fasthttp.BodyWriter(), f, a...) +} + +// WriteString appends s to response body. +func (r *Response) WriteString(s string) (int, error) { + r.fasthttp.AppendBodyString(s) + return len(s), nil +} + +// XML converts any interface or string to XML. +// This method also sets the content header to application/xml. +func (r *Response) XML(data any) error { + raw, err := r.app.config.XMLEncoder(data) + if err != nil { + return err + } + r.fasthttp.SetBodyRaw(raw) + r.fasthttp.Header.SetContentType(MIMEApplicationXML) + return nil +}