mirror of
https://github.com/gofiber/fiber.git
synced 2025-09-04 19:35:47 +00:00
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Juan Calderon-Perez <835733+gaby@users.noreply.github.com> Co-authored-by: René <rene@gofiber.io> Co-authored-by: Giovanni Rivera <rivera.giovanni271@gmail.com> Co-authored-by: Juan Calderon-Perez <jgcalderonperez@protonmail.com>
999 lines
30 KiB
Go
999 lines
30 KiB
Go
package fiber
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"net/url"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"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
|
|
}
|
|
|
|
// compareConfig compares the current SendFile config with the new one
|
|
// and returns true if they are different.
|
|
//
|
|
// Here we don't use reflect.DeepEqual because it is quite slow compared to manual comparison.
|
|
func (sf *sendFileStore) compareConfig(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 data for c.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
|
|
}
|
|
|
|
//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.getString(r.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 {
|
|
r.Set(field, h)
|
|
}
|
|
}
|
|
|
|
// 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])
|
|
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
|
|
}
|
|
|
|
// 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 = filename[0]
|
|
} else {
|
|
fname = filepath.Base(file)
|
|
}
|
|
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
|
|
}
|
|
|
|
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...)
|
|
|
|
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")
|
|
|
|
// 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.getString(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.getString(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
|
|
headers := make(map[string][]string)
|
|
for k, v := range r.c.fasthttp.Response.Header.All() {
|
|
key := app.getString(k)
|
|
headers[key] = append(headers[key], app.getString(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
|
|
}
|
|
|
|
var result, cb string
|
|
|
|
if len(callback) > 0 {
|
|
cb = callback[0]
|
|
} else {
|
|
cb = "callback"
|
|
}
|
|
|
|
result = cb + "(" + r.c.app.getString(raw) + ");"
|
|
|
|
r.setCanonical(HeaderXContentTypeOptions, "nosniff")
|
|
r.c.fasthttp.Response.Header.SetContentType(MIMETextJavaScriptCharsetUTF8)
|
|
return r.SendString(result)
|
|
}
|
|
|
|
// 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="` + link[i] + `",`)
|
|
}
|
|
}
|
|
r.setCanonical(HeaderLink, utils.TrimRight(r.c.app.getString(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) {
|
|
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]) != -1
|
|
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) {
|
|
return r.getLocationFromRoute(r.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
|
|
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.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)
|
|
}
|
|
}
|
|
|
|
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.compareConfig(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 len(file) == 0 || (!filepath.IsAbs(file) && cfg.FS == nil) {
|
|
// 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.c.OriginalURL())
|
|
defer request.SetRequestURI(originalURL)
|
|
|
|
// Set new URI for fileHandler
|
|
request.SetRequestURI(file)
|
|
|
|
// 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 len(cacheControlValue) > 0 {
|
|
response.Header.Set(HeaderCacheControl, cacheControlValue)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
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 *DefaultRes) SendStatus(status int) error {
|
|
r.Status(status)
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|