| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143 |
- package fiber
- import (
- "bufio"
- "bytes"
- "fmt"
- "html/template"
- "io"
- "io/fs"
- "net/http"
- "net/url"
- "os"
- pathpkg "path"
- "path/filepath"
- "strconv"
- "strings"
- "time"
- "unicode"
- "unicode/utf8"
- "github.com/gofiber/utils/v2"
- "github.com/valyala/bytebufferpool"
- "github.com/valyala/fasthttp"
- )
- // SendFile defines configuration options when to transfer file with SendFile.
- type SendFile struct {
- // FS is the file system to serve the static files from.
- // You can use interfaces compatible with fs.FS like embed.FS, os.DirFS etc.
- //
- // Optional. Default: nil
- FS fs.FS
- // When set to true, the server tries minimizing CPU usage by caching compressed files.
- // This works differently than the github.com/gofiber/compression middleware.
- // You have to set Content-Encoding header to compress the file.
- // Available compression methods are gzip, br, and zstd.
- //
- // Optional. Default: false
- Compress bool `json:"compress"`
- // When set to true, enables byte range requests.
- //
- // Optional. Default: false
- ByteRange bool `json:"byte_range"`
- // When set to true, enables direct download.
- //
- // Optional. Default: false
- Download bool `json:"download"`
- // Expiration duration for inactive file handlers.
- // Use a negative time.Duration to disable it.
- //
- // Optional. Default: 10 * time.Second
- CacheDuration time.Duration `json:"cache_duration"`
- // The value for the Cache-Control HTTP-header
- // that is set on the file response. MaxAge is defined in seconds.
- //
- // Optional. Default: 0
- MaxAge int `json:"max_age"`
- }
- // sendFileStore is used to keep the SendFile configuration and the handler.
- type sendFileStore struct {
- handler fasthttp.RequestHandler
- cacheControlValue string
- config SendFile
- }
- // configEqual compares the current SendFile config with the new one
- // and returns true if they are equal.
- //
- // Here we don't use reflect.DeepEqual because it is quite slow compared to manual comparison.
- func (sf *sendFileStore) configEqual(cfg SendFile) bool {
- if sf.config.FS != cfg.FS {
- return false
- }
- if sf.config.Compress != cfg.Compress {
- return false
- }
- if sf.config.ByteRange != cfg.ByteRange {
- return false
- }
- if sf.config.Download != cfg.Download {
- return false
- }
- if sf.config.CacheDuration != cfg.CacheDuration {
- return false
- }
- if sf.config.MaxAge != cfg.MaxAge {
- return false
- }
- return true
- }
- // Cookie defines the values used when configuring cookies emitted by
- // DefaultRes.Cookie.
- type Cookie struct {
- Expires time.Time `json:"expires"` // The expiration date of the cookie
- Name string `json:"name"` // The name of the cookie
- Value string `json:"value"` // The value of the cookie
- Path string `json:"path"` // Specifies a URL path which is allowed to receive the cookie
- Domain string `json:"domain"` // Specifies the domain which is allowed to receive the cookie
- SameSite string `json:"same_site"` // Controls whether or not a cookie is sent with cross-site requests
- MaxAge int `json:"max_age"` // The maximum age (in seconds) of the cookie
- Secure bool `json:"secure"` // Indicates that the cookie should only be transmitted over a secure HTTPS connection
- HTTPOnly bool `json:"http_only"` // Indicates that the cookie is accessible only through the HTTP protocol
- Partitioned bool `json:"partitioned"` // Indicates if the cookie is stored in a partitioned cookie jar
- SessionOnly bool `json:"session_only"` // Indicates if the cookie is a session-only cookie
- }
- // ResFmt associates a Content Type to a fiber.Handler for c.Format
- type ResFmt struct {
- Handler func(Ctx) error
- MediaType string
- }
- // DefaultRes is the default implementation of Res used by DefaultCtx.
- //
- //go:generate ifacemaker --file res.go --struct DefaultRes --iface Res --pkg fiber --output res_interface_gen.go --not-exported true --iface-comment "Res is an interface for response-related Ctx methods."
- type DefaultRes struct {
- c *DefaultCtx
- }
- // App returns the *App reference to the instance of the Fiber application
- func (r *DefaultRes) App() *App {
- return r.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.
- func (r *DefaultRes) Append(field string, values ...string) {
- if len(values) == 0 {
- return
- }
- h := r.c.app.toString(r.c.fasthttp.Response.Header.Peek(field))
- originalH := h
- for _, value := range values {
- if h == "" {
- h = value
- } else if !headerContainsValue(h, value) {
- h += ", " + value
- }
- }
- if originalH != h {
- r.Set(field, h)
- }
- }
- // headerContainsValue checks if a header value already contains the given value
- // as a comma-separated element. Per RFC 9110, list elements are separated by commas
- // with optional whitespace (OWS) around them.
- func headerContainsValue(header, value string) bool {
- // Empty value should never match
- if value == "" {
- return false
- }
- // Exact match (single value header)
- if header == value {
- return true
- }
- // Check each comma-separated element, handling optional whitespace (OWS)
- for part := range strings.SplitSeq(header, ",") {
- if utils.TrimSpace(part) == value {
- return true
- }
- }
- return false
- }
- func sanitizeFilename(filename string) string {
- for _, r := range filename {
- if unicode.IsControl(r) {
- b := make([]byte, 0, len(filename))
- for _, rr := range filename {
- if !unicode.IsControl(rr) {
- b = utf8.AppendRune(b, rr)
- }
- }
- return utils.TrimSpace(string(b))
- }
- }
- return utils.TrimSpace(filename)
- }
- func fallbackFilenameIfInvalid(filename string) string {
- if filename == "" || filename == "." {
- return "download"
- }
- return filename
- }
- // Attachment sets the HTTP response Content-Disposition header field to attachment.
- func (r *DefaultRes) Attachment(filename ...string) {
- if len(filename) > 0 {
- fname := filepath.Base(filename[0])
- fname = sanitizeFilename(fname)
- fname = fallbackFilenameIfInvalid(fname)
- r.Type(filepath.Ext(fname))
- app := r.c.app
- var quoted string
- if app.isASCII(fname) {
- quoted = app.quoteString(fname)
- } else {
- quoted = app.quoteRawString(fname)
- }
- disp := `attachment; filename="` + quoted + `"`
- if !app.isASCII(fname) {
- disp += `; filename*=UTF-8''` + url.PathEscape(fname)
- }
- r.setCanonical(HeaderContentDisposition, disp)
- return
- }
- r.setCanonical(HeaderContentDisposition, "attachment")
- }
- // 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 *DefaultRes) ClearCookie(key ...string) {
- request := &r.c.fasthttp.Request
- response := &r.c.fasthttp.Response
- if len(key) > 0 {
- for i := range key {
- response.Header.DelClientCookie(key[i])
- }
- return
- }
- for k := range request.Header.Cookies() {
- response.Header.DelClientCookieBytes(k)
- }
- }
- // RequestCtx returns *fasthttp.RequestCtx that carries a deadline
- // a cancellation signal, and other values across API boundaries.
- func (r *DefaultRes) RequestCtx() *fasthttp.RequestCtx {
- return r.c.fasthttp
- }
- // Cookie sets a cookie by passing a cookie struct.
- func (r *DefaultRes) Cookie(cookie *Cookie) {
- if cookie.Path == "" {
- cookie.Path = "/"
- }
- if cookie.SessionOnly {
- cookie.MaxAge = 0
- cookie.Expires = time.Time{}
- }
- var sameSite http.SameSite
- switch {
- case utils.EqualFold(cookie.SameSite, CookieSameSiteStrictMode):
- sameSite = http.SameSiteStrictMode
- case utils.EqualFold(cookie.SameSite, CookieSameSiteNoneMode):
- sameSite = http.SameSiteNoneMode
- // SameSite=None requires Secure=true per RFC and browser requirements
- cookie.Secure = true
- case utils.EqualFold(cookie.SameSite, CookieSameSiteDisabled):
- sameSite = 0
- case utils.EqualFold(cookie.SameSite, CookieSameSiteLaxMode):
- sameSite = http.SameSiteLaxMode
- default:
- sameSite = http.SameSiteLaxMode
- }
- // Partitioned requires Secure=true per CHIPS spec
- if cookie.Partitioned {
- cookie.Secure = true
- }
- // create/validate cookie using net/http
- hc := &http.Cookie{
- Name: cookie.Name,
- Value: cookie.Value,
- Path: cookie.Path,
- Domain: cookie.Domain,
- Expires: cookie.Expires,
- MaxAge: cookie.MaxAge,
- Secure: cookie.Secure,
- HttpOnly: cookie.HTTPOnly,
- SameSite: sameSite,
- Partitioned: cookie.Partitioned,
- }
- if err := hc.Valid(); err != nil {
- // invalid cookies are ignored, same approach as net/http
- return
- }
- // create fasthttp cookie
- fcookie := fasthttp.AcquireCookie()
- fcookie.SetKey(hc.Name)
- fcookie.SetValue(hc.Value)
- fcookie.SetPath(hc.Path)
- fcookie.SetDomain(hc.Domain)
- if !cookie.SessionOnly {
- fcookie.SetMaxAge(hc.MaxAge)
- fcookie.SetExpire(hc.Expires)
- }
- fcookie.SetSecure(hc.Secure)
- fcookie.SetHTTPOnly(hc.HttpOnly)
- switch sameSite {
- case http.SameSiteLaxMode:
- fcookie.SetSameSite(fasthttp.CookieSameSiteLaxMode)
- case http.SameSiteStrictMode:
- fcookie.SetSameSite(fasthttp.CookieSameSiteStrictMode)
- case http.SameSiteNoneMode:
- fcookie.SetSameSite(fasthttp.CookieSameSiteNoneMode)
- case http.SameSiteDefaultMode:
- fcookie.SetSameSite(fasthttp.CookieSameSiteDefaultMode)
- default:
- fcookie.SetSameSite(fasthttp.CookieSameSiteDisabled)
- }
- fcookie.SetPartitioned(hc.Partitioned)
- // Set resp header
- r.c.fasthttp.Response.Header.SetCookie(fcookie)
- fasthttp.ReleaseCookie(fcookie)
- }
- // 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 *DefaultRes) Download(file string, filename ...string) error {
- var fname string
- if len(filename) > 0 {
- fname = filepath.Base(filename[0])
- } else {
- fname = filepath.Base(file)
- }
- fname = sanitizeFilename(fname)
- fname = fallbackFilenameIfInvalid(fname)
- app := r.c.app
- var quoted string
- if app.isASCII(fname) {
- quoted = app.quoteString(fname)
- } else {
- quoted = app.quoteRawString(fname)
- }
- disp := `attachment; filename="` + quoted + `"`
- if !app.isASCII(fname) {
- disp += `; filename*=UTF-8''` + url.PathEscape(fname)
- }
- r.setCanonical(HeaderContentDisposition, disp)
- return r.SendFile(file)
- }
- // Response return the *fasthttp.Response object
- // This allows you to use all fasthttp response methods
- // https://godoc.org/github.com/valyala/fasthttp#Response
- func (r *DefaultRes) Response() *fasthttp.Response {
- return &r.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.
- func (r *DefaultRes) Format(handlers ...ResFmt) error {
- if len(handlers) == 0 {
- return ErrNoHandlers
- }
- for i, h := range handlers {
- if h.Handler == nil {
- return fmt.Errorf("format handler is nil for media type %q at index %d", h.MediaType, i)
- }
- }
- r.Vary(HeaderAccept)
- if r.c.DefaultReq.Get(HeaderAccept) == "" {
- r.c.fasthttp.Response.Header.SetContentType(handlers[0].MediaType)
- return handlers[0].Handler(r.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 := r.c.DefaultReq.Accepts(types...) //nolint:staticcheck // It is fine to ignore the static check
- if accept == "" {
- if defaultHandler == nil {
- return r.SendStatus(StatusNotAcceptable)
- }
- return defaultHandler(r.c)
- }
- for _, h := range handlers {
- if h.MediaType == accept {
- r.c.fasthttp.Response.Header.SetContentType(h.MediaType)
- return h.Handler(r.c)
- }
- }
- return fmt.Errorf("%w: format: an Accept was found but no handler was called", errUnreachable)
- }
- // AutoFormat performs content-negotiation on the Accept HTTP header.
- // It uses Accepts to select a proper format.
- // The supported content types are text/html, text/plain, application/json, application/xml, application/vnd.msgpack, and application/cbor.
- // For more flexible content negotiation, use Format.
- // If the header is not specified or there is no proper format, text/plain is used.
- func (r *DefaultRes) AutoFormat(body any) error {
- // Get accepted content type
- accept := r.c.DefaultReq.Accepts("html", "json", "txt", "xml", "msgpack", "cbor") //nolint:staticcheck // It is fine to ignore the static check
- // Set accepted content type
- r.Type(accept)
- // Type convert provided body
- var b string
- switch val := body.(type) {
- case string:
- b = val
- case []byte:
- b = r.c.app.toString(val)
- default:
- b = fmt.Sprintf("%v", val)
- }
- // Format based on the accept content type
- switch accept {
- case "txt":
- return r.SendString(b)
- case "json":
- return r.JSON(body)
- case "xml":
- return r.XML(body)
- case "html":
- return r.SendString("<p>" + b + "</p>")
- case "msgpack":
- return r.MsgPack(body)
- case "cbor":
- return r.CBOR(body)
- }
- // Default case
- return r.SendString(b)
- }
- // Get (a.k.a. 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.
- func (r *DefaultRes) Get(key string, defaultValue ...string) string {
- return defaultString(r.c.app.toString(r.c.fasthttp.Response.Header.Peek(key)), defaultValue)
- }
- // GetHeaders (a.k.a GetRespHeaders) returns the HTTP response headers.
- // Returned value is only valid within the handler. Do not store any references.
- // Make copies or use the Immutable setting instead.
- func (r *DefaultRes) GetHeaders() map[string][]string {
- app := r.c.app
- respHeader := &r.c.fasthttp.Response.Header
- // Pre-allocate map with known header count to avoid reallocations
- headers := make(map[string][]string, respHeader.Len())
- for k, v := range respHeader.All() {
- key := app.toString(k)
- headers[key] = append(headers[key], app.toString(v))
- }
- return headers
- }
- // 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; charset=utf-8.
- func (r *DefaultRes) JSON(data any, ctype ...string) error {
- raw, err := r.c.app.config.JSONEncoder(data)
- if err != nil {
- return err
- }
- response := &r.c.fasthttp.Response
- response.SetBodyRaw(raw)
- if len(ctype) > 0 {
- response.Header.SetContentType(ctype[0])
- } else {
- response.Header.SetContentType(MIMEApplicationJSONCharsetUTF8)
- }
- return nil
- }
- // MsgPack converts any interface or string to MessagePack encoded bytes.
- // 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/vnd.msgpack.
- func (r *DefaultRes) MsgPack(data any, ctype ...string) error {
- raw, err := r.c.app.config.MsgPackEncoder(data)
- if err != nil {
- return err
- }
- response := &r.c.fasthttp.Response
- response.SetBodyRaw(raw)
- if len(ctype) > 0 {
- response.Header.SetContentType(ctype[0])
- } else {
- response.Header.SetContentType(MIMEApplicationMsgPack)
- }
- return nil
- }
- // CBOR converts any interface or string to CBOR encoded bytes.
- // 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/cbor.
- func (r *DefaultRes) CBOR(data any, ctype ...string) error {
- raw, err := r.c.app.config.CBOREncoder(data)
- if err != nil {
- return err
- }
- response := &r.c.fasthttp.Response
- response.SetBodyRaw(raw)
- if len(ctype) > 0 {
- response.Header.SetContentType(ctype[0])
- } else {
- response.Header.SetContentType(MIMEApplicationCBOR)
- }
- 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 *DefaultRes) JSONP(data any, callback ...string) error {
- raw, err := r.c.app.config.JSONEncoder(data)
- if err != nil {
- return err
- }
- cb := "callback"
- if len(callback) > 0 {
- cb = callback[0]
- }
- // Build JSONP response: callback(data);
- // Use bytebufferpool to avoid string concatenation allocations
- buf := bytebufferpool.Get()
- buf.WriteString(cb)
- buf.WriteByte('(')
- buf.Write(raw)
- buf.WriteString(");")
- r.setCanonical(HeaderXContentTypeOptions, "nosniff")
- r.c.fasthttp.Response.Header.SetContentType(MIMETextJavaScriptCharsetUTF8)
- // Use SetBody (not SetBodyRaw) to copy the bytes before returning buffer to pool
- r.c.fasthttp.Response.SetBody(buf.Bytes())
- bytebufferpool.Put(buf)
- return nil
- }
- // XML converts any interface or string to XML.
- // This method also sets the content header to application/xml; charset=utf-8.
- func (r *DefaultRes) XML(data any) error {
- raw, err := r.c.app.config.XMLEncoder(data)
- if err != nil {
- return err
- }
- response := &r.c.fasthttp.Response
- response.SetBodyRaw(raw)
- response.Header.SetContentType(MIMEApplicationXMLCharsetUTF8)
- return nil
- }
- // Links joins the links followed by the property to populate the response's Link HTTP header field.
- func (r *DefaultRes) 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="`)
- bb.WriteString(link[i])
- bb.WriteString(`",`)
- }
- }
- r.setCanonical(HeaderLink, utils.TrimRight(r.c.app.toString(bb.Bytes()), ','))
- bytebufferpool.Put(bb)
- }
- // Location sets the response Location HTTP header to the specified path parameter.
- func (r *DefaultRes) Location(path string) {
- r.setCanonical(HeaderLocation, path)
- }
- // OriginalURL contains the original request URL.
- // Returned value is only valid within the handler. Do not store any references.
- // Make copies or use the Immutable setting to use the value outside the Handler.
- func (r *DefaultRes) OriginalURL() string {
- return r.c.OriginalURL()
- }
- // Redirect returns the Redirect reference.
- // Use Redirect().Status() to set custom redirection status code.
- // If status is not specified, status defaults to 303 See Other.
- // You can use Redirect().To(), Redirect().Route() and Redirect().Back() for redirection.
- func (r *DefaultRes) Redirect() *Redirect {
- return r.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.
- func (r *DefaultRes) ViewBind(vars Map) error {
- return r.c.ViewBind(vars)
- }
- // getLocationFromRoute get URL location from route using parameters
- func (r *DefaultRes) getLocationFromRoute(route *Route, params Map) (string, error) {
- if route == nil || route.Path == "" {
- return "", ErrNotFound
- }
- app := r.c.app
- buf := bytebufferpool.Get()
- for _, segment := range route.routeParser.segs {
- if !segment.IsParam {
- _, err := buf.WriteString(segment.Const)
- if err != nil {
- return "", fmt.Errorf("failed to write string: %w", err)
- }
- continue
- }
- for key, val := range params {
- isSame := key == segment.ParamName || (!app.config.CaseSensitive && utils.EqualFold(key, segment.ParamName))
- isGreedy := segment.IsGreedy && len(key) == 1 && bytes.IndexByte(greedyParameters, key[0]) >= 0
- if isSame || isGreedy {
- _, err := buf.WriteString(utils.ToString(val))
- if err != nil {
- return "", fmt.Errorf("failed to write string: %w", err)
- }
- }
- }
- }
- location := buf.String()
- // release buffer
- bytebufferpool.Put(buf)
- return location, nil
- }
- // GetRouteURL generates URLs to named routes, with parameters. URLs are relative, for example: "/user/1831"
- func (r *DefaultRes) GetRouteURL(routeName string, params Map) (string, error) {
- route := r.c.app.GetRoute(routeName)
- return r.getLocationFromRoute(&route, params)
- }
- // Render a template with data and sends a text/html response.
- // We support the following engines: https://github.com/gofiber/template
- func (r *DefaultRes) Render(name string, bind any, 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.c.renderExtensions(bind)
- rootApp := r.c.app
- var rendered bool
- for i := len(rootApp.mountFields.appListKeys) - 1; i >= 0; i-- {
- prefix := rootApp.mountFields.appListKeys[i]
- app := rootApp.mountFields.appList[prefix]
- if prefix == "" || strings.Contains(r.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(rootApp.toString(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)
- }
- }
- response := &r.c.fasthttp.Response
- // Set Content-Type to text/html
- response.Header.SetContentType(MIMETextHTMLCharsetUTF8)
- // Set rendered template to body
- response.SetBody(buf.Bytes())
- return nil
- }
- func (r *DefaultRes) renderExtensions(bind any) {
- r.c.renderExtensions(bind)
- }
- // Send sets the HTTP response body without copying it.
- // From this point onward the body argument must not be changed.
- func (r *DefaultRes) Send(body []byte) error {
- // Write response body
- r.c.fasthttp.Response.SetBodyRaw(body)
- return nil
- }
- // SendEarlyHints allows the server to hint to the browser what resources a page would need
- // so the browser can preload them while waiting for the server's full response. Only Link
- // headers already written to the response will be transmitted as Early Hints.
- //
- // This is a HTTP/2+ feature but all browsers will either understand it or safely ignore it.
- //
- // NOTE: Older HTTP/1.1 non-browser clients may face compatibility issues.
- //
- // See: https://developer.chrome.com/docs/web-platform/early-hints and
- // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Link#syntax
- func (r *DefaultRes) SendEarlyHints(hints []string) error {
- if len(hints) == 0 {
- return nil
- }
- for _, h := range hints {
- r.c.fasthttp.Response.Header.Add("Link", h)
- }
- return r.c.fasthttp.EarlyHints()
- }
- // SendFile transfers the file from the specified path.
- // By default, the file is not compressed. To enable compression, set SendFile.Compress to true.
- // The Content-Type response HTTP header field is set based on the file's extension.
- // If the file extension is missing or invalid, the Content-Type is detected from the file's format.
- func (r *DefaultRes) SendFile(file string, config ...SendFile) error {
- // Save the filename, we will need it in the error message if the file isn't found
- filename := file
- var cfg SendFile
- if len(config) > 0 {
- cfg = config[0]
- }
- if cfg.CacheDuration == 0 {
- cfg.CacheDuration = 10 * time.Second
- }
- var fsHandler fasthttp.RequestHandler
- var cacheControlValue string
- app := r.c.app
- app.sendfilesMutex.RLock()
- for _, sf := range app.sendfiles {
- if sf.configEqual(cfg) {
- fsHandler = sf.handler
- cacheControlValue = sf.cacheControlValue
- break
- }
- }
- app.sendfilesMutex.RUnlock()
- if fsHandler == nil {
- fasthttpFS := &fasthttp.FS{
- Root: "",
- FS: cfg.FS,
- AllowEmptyRoot: true,
- GenerateIndexPages: false,
- AcceptByteRange: cfg.ByteRange,
- Compress: cfg.Compress,
- CompressBrotli: cfg.Compress,
- CompressZstd: cfg.Compress,
- CompressedFileSuffixes: app.config.CompressedFileSuffixes,
- CacheDuration: cfg.CacheDuration,
- SkipCache: cfg.CacheDuration < 0,
- IndexNames: []string{"index.html"},
- PathNotFound: func(ctx *fasthttp.RequestCtx) {
- ctx.Response.SetStatusCode(StatusNotFound)
- },
- }
- if cfg.FS != nil {
- fasthttpFS.Root = "."
- }
- sf := &sendFileStore{
- config: cfg,
- handler: fasthttpFS.NewRequestHandler(),
- }
- maxAge := cfg.MaxAge
- if maxAge > 0 {
- sf.cacheControlValue = "public, max-age=" + strconv.Itoa(maxAge)
- }
- // set vars
- fsHandler = sf.handler
- cacheControlValue = sf.cacheControlValue
- app.sendfilesMutex.Lock()
- app.sendfiles = append(app.sendfiles, sf)
- app.sendfilesMutex.Unlock()
- }
- // Keep original path for mutable params
- r.c.pathOriginal = utils.CopyString(r.c.pathOriginal)
- request := &r.c.fasthttp.Request
- // Delete the Accept-Encoding header if compression is disabled
- if !cfg.Compress {
- // https://github.com/valyala/fasthttp/blob/7cc6f4c513f9e0d3686142e0a1a5aa2f76b3194a/fs.go#L55
- request.Header.Del(HeaderAcceptEncoding)
- }
- // copy of https://github.com/valyala/fasthttp/blob/7cc6f4c513f9e0d3686142e0a1a5aa2f76b3194a/fs.go#L103-L121 with small adjustments
- if file == "" || (!filepath.IsAbs(file) && cfg.FS == nil) {
- // extend relative path to absolute path
- hasTrailingSlash := file != "" && (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.c.OriginalURL())
- defer request.SetRequestURI(originalURL)
- // Set new URI for fileHandler
- request.SetRequestURI(file)
- var (
- sendFileSize int64
- hasSendFileSize bool
- )
- if cfg.ByteRange && len(request.Header.Peek(HeaderRange)) > 0 {
- sizePath := file
- if cfg.FS != nil {
- sizePath = filepath.ToSlash(filename)
- }
- if size, err := sendFileContentLength(sizePath, cfg); err == nil {
- sendFileSize = size
- hasSendFileSize = true
- }
- }
- // Save status code
- response := &r.c.fasthttp.Response
- status := response.StatusCode()
- // Serve file
- fsHandler(r.c.fasthttp)
- // Sets the response Content-Disposition header to attachment if the Download option is true
- if cfg.Download {
- r.Attachment()
- }
- // Get the status code which is set by fasthttp
- fsStatus := response.StatusCode()
- // Check for error
- if status != StatusNotFound && fsStatus == StatusNotFound {
- return NewError(StatusNotFound, fmt.Sprintf("sendfile: file %s not found", filename))
- }
- // 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)
- }
- // Apply cache control header
- if status != StatusNotFound && status != StatusForbidden {
- if cfg.ByteRange && hasSendFileSize && response.StatusCode() == StatusRequestedRangeNotSatisfiable && len(response.Header.Peek(HeaderContentRange)) == 0 {
- response.Header.Set(HeaderContentRange, "bytes */"+strconv.FormatInt(sendFileSize, 10))
- }
- if cacheControlValue != "" {
- response.Header.Set(HeaderCacheControl, cacheControlValue)
- }
- return nil
- }
- return nil
- }
- func sendFileContentLength(path string, cfg SendFile) (int64, error) {
- if cfg.FS != nil {
- cleanPath := pathpkg.Clean(utils.TrimLeft(path, '/'))
- if cleanPath == "." {
- cleanPath = ""
- }
- info, err := fs.Stat(cfg.FS, cleanPath)
- if err != nil {
- return 0, fmt.Errorf("stat %q: %w", cleanPath, err)
- }
- return info.Size(), nil
- }
- info, err := os.Stat(filepath.FromSlash(path))
- if err != nil {
- return 0, fmt.Errorf("stat %q: %w", path, err)
- }
- return info.Size(), 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 *DefaultRes) SendStatus(status int) error {
- r.Status(status)
- if statusDisallowsBody(status) {
- r.c.fasthttp.Response.ResetBody()
- return nil
- }
- // Only set status body when there is no response body
- if len(r.c.fasthttp.Response.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 *DefaultRes) SendString(body string) error {
- r.c.fasthttp.Response.SetBodyString(body)
- return nil
- }
- // SendStream sets response body stream and optional body size.
- func (r *DefaultRes) SendStream(stream io.Reader, size ...int) error {
- if len(size) > 0 && size[0] >= 0 {
- r.c.fasthttp.Response.SetBodyStream(stream, size[0])
- } else {
- r.c.fasthttp.Response.SetBodyStream(stream, -1)
- }
- return nil
- }
- // SendStreamWriter sets response body stream writer
- func (r *DefaultRes) SendStreamWriter(streamWriter func(*bufio.Writer)) error {
- r.c.fasthttp.Response.SetBodyStreamWriter(fasthttp.StreamWriter(streamWriter))
- return nil
- }
- // Set sets the response's HTTP header field to the specified key, value.
- func (r *DefaultRes) Set(key, val string) {
- r.c.fasthttp.Response.Header.Set(key, val)
- }
- func (r *DefaultRes) setCanonical(key, val string) {
- r.c.fasthttp.Response.Header.SetCanonical(utils.UnsafeBytes(key), utils.UnsafeBytes(val))
- }
- // Status sets the HTTP status for the response.
- // This method is chainable.
- func (r *DefaultRes) Status(status int) Ctx {
- r.c.fasthttp.Response.SetStatusCode(status)
- return r.c
- }
- func statusDisallowsBody(status int) bool {
- // As per RFC 9110, 1xx (Informational) responses cannot have a body.
- if status >= 100 && status < 200 {
- return true
- }
- switch status {
- case StatusNoContent, StatusResetContent, StatusNotModified:
- return true
- default:
- return false
- }
- }
- // Type sets the Content-Type HTTP header to the MIME type specified by the file extension.
- func (r *DefaultRes) Type(extension string, charset ...string) Ctx {
- mimeType := utils.GetMIME(extension)
- if len(charset) > 0 {
- r.c.fasthttp.Response.Header.SetContentType(mimeType + "; charset=" + charset[0])
- } else {
- // Automatically add UTF-8 charset for text-based MIME types
- if shouldIncludeCharset(mimeType) {
- r.c.fasthttp.Response.Header.SetContentType(mimeType + "; charset=utf-8")
- } else {
- r.c.fasthttp.Response.Header.SetContentType(mimeType)
- }
- }
- return r.c
- }
- // shouldIncludeCharset determines if a MIME type should include UTF-8 charset by default
- func shouldIncludeCharset(mimeType string) bool {
- // Everything under text/ gets UTF-8 by default.
- if strings.HasPrefix(mimeType, "text/") {
- return true
- }
- // Explicit application types that should default to UTF-8.
- switch mimeType {
- case MIMEApplicationJSON,
- MIMEApplicationJavaScript,
- MIMEApplicationXML:
- return true
- }
- // Any application/*+json or application/*+xml.
- if strings.HasSuffix(mimeType, "+json") || strings.HasSuffix(mimeType, "+xml") {
- return true
- }
- return false
- }
- // 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 *DefaultRes) Vary(fields ...string) {
- r.Append(HeaderVary, fields...)
- }
- // Write appends p into response body.
- func (r *DefaultRes) Write(p []byte) (int, error) {
- r.c.fasthttp.Response.AppendBody(p)
- return len(p), nil
- }
- // Writef appends f & a into response body writer.
- func (r *DefaultRes) Writef(f string, a ...any) (int, error) {
- //nolint:wrapcheck // This must not be wrapped
- return fmt.Fprintf(r.c.fasthttp.Response.BodyWriter(), f, a...)
- }
- // WriteString appends s to response body.
- func (r *DefaultRes) WriteString(s string) (int, error) {
- r.c.fasthttp.Response.AppendBodyString(s)
- return len(s), nil
- }
- // Release is a method to reset Res fields when to use ReleaseCtx()
- func (r *DefaultRes) release() {
- r.c = nil
- }
- // Drop closes the underlying connection without sending any response headers or body.
- // This can be useful for silently terminating client connections, such as in DDoS mitigation
- // or when blocking access to sensitive endpoints.
- func (r *DefaultRes) Drop() error {
- //nolint:wrapcheck // error wrapping is avoided to keep the operation lightweight and focused on connection closure.
- return r.c.fasthttp.Conn().Close()
- }
- // End immediately flushes the current response and closes the underlying connection.
- func (r *DefaultRes) End() error {
- ctx := r.c.fasthttp
- conn := ctx.Conn()
- bw := bufio.NewWriter(conn)
- if err := ctx.Response.Write(bw); err != nil {
- return err
- }
- if err := bw.Flush(); err != nil {
- return err //nolint:wrapcheck // unnecessary to wrap it
- }
- return conn.Close() //nolint:wrapcheck // unnecessary to wrap it
- }
|