mirror of
https://github.com/zap-proto/fiber.git
synced 2026-10-30 15:06:23 +00:00
Agent-Logs-Url: https://github.com/gofiber/fiber/sessions/5b86c6bd-c9af-4bf0-a36b-f5d170af9c47 Co-authored-by: gaby <[email protected]>
1174 lines
37 KiB
Go
1174 lines
37 KiB
Go
package fiber
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"mime/multipart"
|
|
"net"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/gofiber/utils/v2"
|
|
utilsbytes "github.com/gofiber/utils/v2/bytes"
|
|
utilsstrings "github.com/gofiber/utils/v2/strings"
|
|
"github.com/valyala/bytebufferpool"
|
|
"github.com/valyala/fasthttp"
|
|
"golang.org/x/net/idna"
|
|
)
|
|
|
|
// Pre-allocated byte slices for common header comparisons to avoid allocations
|
|
var (
|
|
xForwardedPrefix = []byte("X-Forwarded-")
|
|
xForwardedProtoBytes = []byte(HeaderXForwardedProto)
|
|
xForwardedProtocolBytes = []byte(HeaderXForwardedProtocol)
|
|
xForwardedSslBytes = []byte(HeaderXForwardedSsl)
|
|
xURLSchemeBytes = []byte(HeaderXUrlScheme)
|
|
onBytes = []byte("on")
|
|
)
|
|
|
|
// Range represents the parsed HTTP Range header extracted by DefaultReq.Range.
|
|
type Range struct {
|
|
Type string
|
|
Ranges []RangeSet
|
|
}
|
|
|
|
// RangeSet represents a single content range from a request.
|
|
type RangeSet struct {
|
|
Start int64
|
|
End int64
|
|
}
|
|
|
|
// DefaultReq is the default implementation of Req used by DefaultCtx.
|
|
//
|
|
//go:generate ifacemaker --file req.go --struct DefaultReq --iface Req --pkg fiber --output req_interface_gen.go --not-exported true --iface-comment "Req is an interface for request-related Ctx methods."
|
|
type DefaultReq struct {
|
|
c *DefaultCtx
|
|
}
|
|
|
|
// Accepts checks if the specified extensions or content types are acceptable.
|
|
func (r *DefaultReq) Accepts(offers ...string) string {
|
|
header := joinHeaderValues(r.c.fasthttp.Request.Header.PeekAll(HeaderAccept))
|
|
return getOffer(header, acceptsOfferType, offers...)
|
|
}
|
|
|
|
// AcceptsCharsets checks if the specified charset is acceptable.
|
|
func (r *DefaultReq) AcceptsCharsets(offers ...string) string {
|
|
header := joinHeaderValues(r.c.fasthttp.Request.Header.PeekAll(HeaderAcceptCharset))
|
|
return getOffer(header, acceptsOffer, offers...)
|
|
}
|
|
|
|
// AcceptsEncodings checks if the specified encoding is acceptable.
|
|
func (r *DefaultReq) AcceptsEncodings(offers ...string) string {
|
|
header := joinHeaderValues(r.c.fasthttp.Request.Header.PeekAll(HeaderAcceptEncoding))
|
|
return getOffer(header, acceptsOffer, offers...)
|
|
}
|
|
|
|
// AcceptsLanguages checks if the specified language is acceptable using
|
|
// RFC 4647 Basic Filtering.
|
|
func (r *DefaultReq) AcceptsLanguages(offers ...string) string {
|
|
header := joinHeaderValues(r.c.fasthttp.Request.Header.PeekAll(HeaderAcceptLanguage))
|
|
return getOffer(header, acceptsLanguageOfferBasic, offers...)
|
|
}
|
|
|
|
// AcceptsLanguagesExtended checks if the specified language is acceptable using
|
|
// RFC 4647 Extended Filtering.
|
|
func (r *DefaultReq) AcceptsLanguagesExtended(offers ...string) string {
|
|
header := joinHeaderValues(r.c.fasthttp.Request.Header.PeekAll(HeaderAcceptLanguage))
|
|
return getOffer(header, acceptsLanguageOfferExtended, offers...)
|
|
}
|
|
|
|
// App returns the *App reference to the instance of the Fiber application
|
|
func (r *DefaultReq) App() *App {
|
|
return r.c.app
|
|
}
|
|
|
|
// BaseURL returns (protocol + host + base path).
|
|
func (r *DefaultReq) BaseURL() string {
|
|
return r.c.BaseURL()
|
|
}
|
|
|
|
// BodyRaw contains the raw body submitted in a POST request.
|
|
// Returned value is only valid within the handler. Do not store any references.
|
|
// Make copies or use the Immutable setting instead.
|
|
func (r *DefaultReq) BodyRaw() []byte {
|
|
return r.getBody()
|
|
}
|
|
|
|
//nolint:nonamedreturns // gocritic unnamedResult prefers naming decoded body, decode count, and error
|
|
func (r *DefaultReq) tryDecodeBodyInOrder(
|
|
originalBody *[]byte,
|
|
encodings []string,
|
|
) (body []byte, decodesRealized uint8, err error) {
|
|
request := &r.c.fasthttp.Request
|
|
maxBodySize := r.c.app.config.BodyLimit
|
|
for idx := range encodings {
|
|
i := len(encodings) - 1 - idx
|
|
encoding := encodings[i]
|
|
decodesRealized++
|
|
var decodeErr error
|
|
switch encoding {
|
|
case StrGzip, "x-gzip":
|
|
body, decodeErr = request.BodyGunzipWithLimit(maxBodySize)
|
|
case StrBr, StrBrotli:
|
|
body, decodeErr = request.BodyUnbrotliWithLimit(maxBodySize)
|
|
case StrDeflate:
|
|
body, decodeErr = request.BodyInflateWithLimit(maxBodySize)
|
|
case StrZstd:
|
|
body, decodeErr = request.BodyUnzstdWithLimit(maxBodySize)
|
|
case StrIdentity:
|
|
body = request.Body()
|
|
case StrCompress, "x-compress":
|
|
return nil, decodesRealized - 1, ErrNotImplemented
|
|
default:
|
|
return nil, decodesRealized - 1, ErrUnsupportedMediaType
|
|
}
|
|
|
|
if decodeErr != nil {
|
|
return nil, decodesRealized, decodeErr
|
|
}
|
|
|
|
if i > 0 && decodesRealized > 0 {
|
|
if i == len(encodings)-1 {
|
|
tempBody := request.Body()
|
|
*originalBody = make([]byte, len(tempBody))
|
|
copy(*originalBody, tempBody)
|
|
}
|
|
request.SetBodyRaw(body)
|
|
}
|
|
}
|
|
|
|
return body, decodesRealized, nil
|
|
}
|
|
|
|
// Body contains the raw body submitted in a POST request.
|
|
// This method will decompress the body if the 'Content-Encoding' header is provided.
|
|
// It returns the original (or decompressed) body data which is valid only within the handler.
|
|
// Don't store direct references to the returned data.
|
|
// If you need to keep the body's data later, make a copy or use the Immutable option.
|
|
func (r *DefaultReq) Body() []byte {
|
|
var (
|
|
err error
|
|
body, originalBody []byte
|
|
headerEncoding string
|
|
encodingOrder = []string{"", "", ""}
|
|
)
|
|
|
|
request := &r.c.fasthttp.Request
|
|
|
|
// Get Content-Encoding header
|
|
headerEncoding = utils.UnsafeString(utilsbytes.UnsafeToLower(request.Header.ContentEncoding()))
|
|
|
|
// If no encoding is provided, return the original body
|
|
if headerEncoding == "" {
|
|
return r.getBody()
|
|
}
|
|
|
|
// Split and get the encodings list, in order to attend the
|
|
// rule defined at: https://www.rfc-editor.org/rfc/rfc9110#section-8.4-5
|
|
encodingOrder = getSplicedStrList(headerEncoding, encodingOrder)
|
|
for i := range encodingOrder {
|
|
encodingOrder[i] = utilsstrings.UnsafeToLower(encodingOrder[i])
|
|
}
|
|
if len(encodingOrder) == 0 {
|
|
return r.getBody()
|
|
}
|
|
|
|
var decodesRealized uint8
|
|
body, decodesRealized, err = r.tryDecodeBodyInOrder(&originalBody, encodingOrder)
|
|
|
|
// Ensure that the body will be the original
|
|
if originalBody != nil && decodesRealized > 0 {
|
|
request.SetBodyRaw(originalBody)
|
|
}
|
|
if err != nil {
|
|
switch {
|
|
case errors.Is(err, ErrUnsupportedMediaType):
|
|
_ = r.c.DefaultRes.SendStatus(StatusUnsupportedMediaType) //nolint:errcheck,staticcheck // It is fine to ignore the error and the static check
|
|
case errors.Is(err, ErrNotImplemented):
|
|
_ = r.c.DefaultRes.SendStatus(StatusNotImplemented) //nolint:errcheck,staticcheck // It is fine to ignore the error and the static check
|
|
case errors.Is(err, fasthttp.ErrBodyTooLarge):
|
|
_ = r.c.DefaultRes.SendStatus(StatusRequestEntityTooLarge) //nolint:errcheck,staticcheck // It is fine to ignore the error and the static check
|
|
default:
|
|
// do nothing
|
|
}
|
|
return []byte(err.Error())
|
|
}
|
|
|
|
return r.c.app.GetBytes(body)
|
|
}
|
|
|
|
// RequestCtx returns *fasthttp.RequestCtx that carries a deadline
|
|
// a cancellation signal, and other values across API boundaries.
|
|
func (r *DefaultReq) RequestCtx() *fasthttp.RequestCtx {
|
|
return r.c.fasthttp
|
|
}
|
|
|
|
// FullURL returns the full request URL (protocol + host + original URL).
|
|
func (c *DefaultCtx) FullURL() string {
|
|
buf := bytebufferpool.Get()
|
|
defer bytebufferpool.Put(buf)
|
|
|
|
buf.WriteString(c.Scheme())
|
|
buf.WriteString("://")
|
|
buf.WriteString(c.Host())
|
|
buf.WriteString(c.OriginalURL())
|
|
|
|
return c.app.toString(buf.Bytes())
|
|
}
|
|
|
|
// UserAgent returns the User-Agent request header.
|
|
func (c *DefaultCtx) UserAgent() string {
|
|
return c.app.toString(c.fasthttp.Request.Header.UserAgent())
|
|
}
|
|
|
|
// Referer returns the Referer request header.
|
|
func (c *DefaultCtx) Referer() string {
|
|
return c.app.toString(c.fasthttp.Request.Header.Referer())
|
|
}
|
|
|
|
// AcceptLanguage returns the Accept-Language request header.
|
|
func (c *DefaultCtx) AcceptLanguage() string {
|
|
return c.app.toString(c.fasthttp.Request.Header.Peek(HeaderAcceptLanguage))
|
|
}
|
|
|
|
// AcceptEncoding returns the Accept-Encoding request header.
|
|
func (c *DefaultCtx) AcceptEncoding() string {
|
|
return c.app.toString(c.fasthttp.Request.Header.Peek(HeaderAcceptEncoding))
|
|
}
|
|
|
|
// HasHeader reports whether the request includes a header with the given key.
|
|
func (c *DefaultCtx) HasHeader(key string) bool {
|
|
return len(c.fasthttp.Request.Header.Peek(key)) > 0
|
|
}
|
|
|
|
// MediaType returns the MIME type from the Content-Type header without parameters.
|
|
func (c *DefaultCtx) MediaType() string {
|
|
contentType := utils.TrimSpace(c.fasthttp.Request.Header.ContentType())
|
|
if len(contentType) == 0 {
|
|
return ""
|
|
}
|
|
if idx := bytes.IndexByte(contentType, ';'); idx != -1 {
|
|
contentType = contentType[:idx]
|
|
}
|
|
contentType = utils.TrimSpace(contentType)
|
|
return c.app.toString(contentType)
|
|
}
|
|
|
|
// Charset returns the charset parameter from the Content-Type header.
|
|
func (c *DefaultCtx) Charset() string {
|
|
contentType := c.fasthttp.Request.Header.ContentType()
|
|
if len(contentType) == 0 {
|
|
return ""
|
|
}
|
|
_, after, ok := bytes.Cut(contentType, []byte{';'})
|
|
if !ok {
|
|
return ""
|
|
}
|
|
params := after
|
|
for len(params) > 0 {
|
|
params = utils.TrimSpace(params)
|
|
if len(params) == 0 {
|
|
return ""
|
|
}
|
|
param := params
|
|
if idx := bytes.IndexByte(params, ';'); idx != -1 {
|
|
param = params[:idx]
|
|
params = params[idx+1:]
|
|
} else {
|
|
params = nil
|
|
}
|
|
|
|
param = utils.TrimSpace(param)
|
|
if len(param) == 0 {
|
|
continue
|
|
}
|
|
before, after, ok := bytes.Cut(param, []byte{'='})
|
|
if !ok {
|
|
continue
|
|
}
|
|
name := utils.TrimSpace(before)
|
|
if !bytes.EqualFold(name, []byte("charset")) {
|
|
continue
|
|
}
|
|
value := utils.TrimSpace(after)
|
|
if len(value) >= 2 && value[0] == '"' && value[len(value)-1] == '"' {
|
|
value = value[1 : len(value)-1]
|
|
}
|
|
return c.app.toString(value)
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// IsJSON reports whether the Content-Type header is JSON.
|
|
func (c *DefaultCtx) IsJSON() bool {
|
|
return utils.EqualFold(c.MediaType(), MIMEApplicationJSON)
|
|
}
|
|
|
|
// IsForm reports whether the Content-Type header is form-encoded.
|
|
func (c *DefaultCtx) IsForm() bool {
|
|
return utils.EqualFold(c.MediaType(), MIMEApplicationForm)
|
|
}
|
|
|
|
// IsMultipart reports whether the Content-Type header is multipart form data.
|
|
func (c *DefaultCtx) IsMultipart() bool {
|
|
return utils.EqualFold(c.MediaType(), MIMEMultipartForm)
|
|
}
|
|
|
|
// AcceptsJSON reports whether the Accept header allows JSON.
|
|
func (c *DefaultCtx) AcceptsJSON() bool {
|
|
return c.Accepts(MIMEApplicationJSON) != ""
|
|
}
|
|
|
|
// AcceptsHTML reports whether the Accept header allows HTML.
|
|
func (c *DefaultCtx) AcceptsHTML() bool {
|
|
return c.Accepts(MIMETextHTML) != ""
|
|
}
|
|
|
|
// AcceptsXML reports whether the Accept header allows XML.
|
|
func (c *DefaultCtx) AcceptsXML() bool {
|
|
return c.Accepts(MIMEApplicationXML, MIMETextXML) != ""
|
|
}
|
|
|
|
// AcceptsEventStream reports whether the Accept header allows text/event-stream.
|
|
func (c *DefaultCtx) AcceptsEventStream() bool {
|
|
return c.Accepts("text/event-stream") != ""
|
|
}
|
|
|
|
// Cookies are used for getting a cookie value by key.
|
|
// Defaults to the empty string "" if the cookie doesn't exist.
|
|
// If a default value is given, it will return that value if the cookie doesn't exist.
|
|
// The 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 *DefaultReq) Cookies(key string, defaultValue ...string) string {
|
|
return defaultString(r.c.app.toString(r.c.fasthttp.Request.Header.Cookie(key)), defaultValue)
|
|
}
|
|
|
|
// Request return the *fasthttp.Request object
|
|
// This allows you to use all fasthttp request methods
|
|
// https://godoc.org/github.com/valyala/fasthttp#Request
|
|
func (r *DefaultReq) Request() *fasthttp.Request {
|
|
return &r.c.fasthttp.Request
|
|
}
|
|
|
|
// FormFile returns the first file by key from a MultipartForm.
|
|
// The multipart form is parsed using the application's BodyLimit to prevent
|
|
// unbounded memory usage.
|
|
func (r *DefaultReq) FormFile(key string) (*multipart.FileHeader, error) {
|
|
if _, err := r.MultipartForm(); err != nil {
|
|
return nil, err
|
|
}
|
|
return r.c.fasthttp.FormFile(key)
|
|
}
|
|
|
|
// FormValue returns the first value by key from a MultipartForm.
|
|
// Search is performed in QueryArgs, PostArgs, MultipartForm and FormFile in this particular order.
|
|
// Defaults to the empty string "" if the form value doesn't exist.
|
|
// If a default value is given, it will return that value if the form value does not exist.
|
|
// Returned value is only valid within the handler. Do not store any references.
|
|
// Make copies or use the Immutable setting instead.
|
|
// When the request is a multipart form, it is parsed using the application's
|
|
// BodyLimit so the configured limit is consistently enforced.
|
|
func (r *DefaultReq) FormValue(key string, defaultValue ...string) string {
|
|
if r.c.IsMultipart() {
|
|
// For multipart requests, parse the form using the application's BodyLimit.
|
|
// fasthttp's FormValue would otherwise re-parse with its default 8 MiB limit,
|
|
// effectively bypassing the configured BodyLimit.
|
|
//
|
|
// Preserve the original search order: QueryArgs → PostArgs → MultipartForm.
|
|
if v := r.c.fasthttp.QueryArgs().Peek(key); len(v) > 0 {
|
|
return r.c.app.toString(v)
|
|
}
|
|
if v := r.c.fasthttp.PostArgs().Peek(key); len(v) > 0 {
|
|
return r.c.app.toString(v)
|
|
}
|
|
mf, err := r.MultipartForm()
|
|
if err != nil {
|
|
return defaultString("", defaultValue)
|
|
}
|
|
if vals := mf.Value[key]; len(vals) > 0 {
|
|
return vals[0]
|
|
}
|
|
return defaultString("", defaultValue)
|
|
}
|
|
return defaultString(r.c.app.toString(r.c.fasthttp.FormValue(key)), defaultValue)
|
|
}
|
|
|
|
// Fresh returns true when the response is still “fresh” in the client's cache,
|
|
// otherwise false is returned to indicate that the client cache is now stale
|
|
// and the full response should be sent.
|
|
// When a client sends the Cache-Control: no-cache request header to indicate an end-to-end
|
|
// reload request, this module will return false to make handling these requests transparent.
|
|
// https://github.com/jshttp/fresh/blob/master/index.js#L33
|
|
func (r *DefaultReq) Fresh() bool {
|
|
header := &r.c.fasthttp.Request.Header
|
|
|
|
// fields
|
|
modifiedSince := header.Peek(HeaderIfModifiedSince)
|
|
noneMatch := header.Peek(HeaderIfNoneMatch)
|
|
|
|
// unconditional request
|
|
if len(modifiedSince) == 0 && len(noneMatch) == 0 {
|
|
return false
|
|
}
|
|
|
|
// Always return stale when Cache-Control: no-cache
|
|
// to support end-to-end reload requests
|
|
// https://www.rfc-editor.org/rfc/rfc9111#section-5.2.1.4
|
|
cacheControl := header.Peek(HeaderCacheControl)
|
|
if len(cacheControl) > 0 && isNoCache(utils.UnsafeString(cacheControl)) {
|
|
return false
|
|
}
|
|
|
|
// if-none-match
|
|
if len(noneMatch) > 0 && (len(noneMatch) != 1 || noneMatch[0] != '*') {
|
|
app := r.c.app
|
|
response := &r.c.fasthttp.Response
|
|
etag := app.toString(response.Header.Peek(HeaderETag))
|
|
if etag == "" {
|
|
return false
|
|
}
|
|
if app.isEtagStale(etag, noneMatch) {
|
|
return false
|
|
}
|
|
|
|
if len(modifiedSince) > 0 {
|
|
lastModified := response.Header.Peek(HeaderLastModified)
|
|
if len(lastModified) > 0 {
|
|
lastModifiedTime, err := fasthttp.ParseHTTPDate(lastModified)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
modifiedSinceTime, err := fasthttp.ParseHTTPDate(modifiedSince)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return lastModifiedTime.Compare(modifiedSinceTime) != 1
|
|
}
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Get returns the HTTP request 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 *DefaultReq) Get(key string, defaultValue ...string) string {
|
|
return GetReqHeader(r.c, key, defaultValue...)
|
|
}
|
|
|
|
// GetReqHeader returns the HTTP request header specified by filed.
|
|
// This function is generic and can handle different headers type values.
|
|
// If the generic type cannot be matched to a supported type, the function
|
|
// returns the default value (if provided) or the zero value of type V.
|
|
func GetReqHeader[V GenericType](c Ctx, key string, defaultValue ...V) V {
|
|
v, err := genericParseType[V](c.App().toString(c.Request().Header.Peek(key)))
|
|
if err != nil && len(defaultValue) > 0 {
|
|
return defaultValue[0]
|
|
}
|
|
return v
|
|
}
|
|
|
|
// GetHeaders (a.k.a GetReqHeaders) returns the HTTP request headers.
|
|
// Returned value is only valid within the handler. Do not store any references.
|
|
// Make copies or use the Immutable setting instead.
|
|
func (r *DefaultReq) GetHeaders() map[string][]string {
|
|
app := r.c.app
|
|
reqHeader := &r.c.fasthttp.Request.Header
|
|
// Pre-allocate map with known header count to avoid reallocations
|
|
headers := make(map[string][]string, reqHeader.Len())
|
|
for k, v := range reqHeader.All() {
|
|
key := app.toString(k)
|
|
headers[key] = append(headers[key], app.toString(v))
|
|
}
|
|
return headers
|
|
}
|
|
|
|
// Host contains the host derived from the X-Forwarded-Host or Host HTTP header.
|
|
// Returned value is only valid within the handler. Do not store any references.
|
|
// In a network context, `Host` refers to the combination of a hostname and potentially a port number used for connecting,
|
|
// while `Hostname` refers specifically to the name assigned to a device on a network, excluding any port information.
|
|
// Example: URL: https://example.com:8080 -> Host: example.com:8080
|
|
// Make copies or use the Immutable setting instead.
|
|
// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.
|
|
func (r *DefaultReq) Host() string {
|
|
if r.IsProxyTrusted() {
|
|
if host := r.Get(HeaderXForwardedHost); host != "" {
|
|
if before, _, found := strings.Cut(host, ","); found {
|
|
return utils.TrimSpace(before)
|
|
}
|
|
return utils.TrimSpace(host)
|
|
}
|
|
}
|
|
return r.c.app.toString(r.c.fasthttp.Request.URI().Host())
|
|
}
|
|
|
|
// Hostname contains the hostname derived from the X-Forwarded-Host or Host HTTP header using the c.Host() method.
|
|
// Returned value is only valid within the handler. Do not store any references.
|
|
// Example: URL: https://example.com:8080 -> Hostname: example.com
|
|
// Make copies or use the Immutable setting instead.
|
|
// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.
|
|
func (r *DefaultReq) Hostname() string {
|
|
addr, _ := parseAddr(r.Host())
|
|
|
|
return addr
|
|
}
|
|
|
|
// Port returns the remote port of the request.
|
|
func (r *DefaultReq) Port() string {
|
|
addr := r.c.fasthttp.RemoteAddr()
|
|
if addr == nil {
|
|
return "0"
|
|
}
|
|
switch typedAddr := addr.(type) {
|
|
case *net.TCPAddr:
|
|
return strconv.Itoa(typedAddr.Port)
|
|
case *net.UnixAddr:
|
|
return ""
|
|
}
|
|
|
|
_, port, err := net.SplitHostPort(addr.String())
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
return port
|
|
}
|
|
|
|
// IP returns the remote IP address of the request.
|
|
// If ProxyHeader and IP Validation is configured, it will parse that header and return the first valid IP address.
|
|
// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.
|
|
func (r *DefaultReq) IP() string {
|
|
app := r.c.app
|
|
if r.IsProxyTrusted() && app.config.ProxyHeader != "" {
|
|
return r.extractIPFromHeader(app.config.ProxyHeader)
|
|
}
|
|
|
|
if ip := r.c.fasthttp.RemoteIP(); ip != nil {
|
|
return ip.String()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// extractIPsFromHeader will return a slice of IPs it found given a header name in the order they appear.
|
|
// When IP validation is enabled, any invalid IPs will be omitted.
|
|
func (r *DefaultReq) extractIPsFromHeader(header string) []string {
|
|
// TODO: Reuse the c.extractIPFromHeader func somehow in here
|
|
|
|
headerValue := r.Get(header)
|
|
|
|
// We can't know how many IPs we will return, but we will try to guess with this constant division.
|
|
// Counting ',' makes function slower for about 50ns in general case.
|
|
const maxEstimatedCount = 8
|
|
estimatedCount := min(len(headerValue)/maxEstimatedCount,
|
|
// Avoid big allocation on big header
|
|
maxEstimatedCount)
|
|
|
|
ipsFound := make([]string, 0, estimatedCount)
|
|
|
|
i := 0
|
|
j := -1
|
|
|
|
for {
|
|
var v4, v6 bool
|
|
|
|
// Manually splitting string without allocating slice, working with parts directly
|
|
i, j = j+1, j+2
|
|
|
|
if j > len(headerValue) {
|
|
break
|
|
}
|
|
|
|
for j < len(headerValue) && headerValue[j] != ',' {
|
|
switch headerValue[j] {
|
|
case ':':
|
|
v6 = true
|
|
case '.':
|
|
v4 = true
|
|
default:
|
|
// do nothing
|
|
}
|
|
j++
|
|
}
|
|
|
|
for i < j && (headerValue[i] == ' ' || headerValue[i] == ',') {
|
|
i++
|
|
}
|
|
|
|
s := utils.TrimRight(headerValue[i:j], ' ')
|
|
|
|
if r.c.app.config.EnableIPValidation {
|
|
// Skip validation if IP is clearly not IPv4/IPv6; otherwise, validate without allocations
|
|
if (!v6 && !v4) || (v6 && !utils.IsIPv6(s)) || (v4 && !utils.IsIPv4(s)) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
ipsFound = append(ipsFound, s)
|
|
}
|
|
|
|
return ipsFound
|
|
}
|
|
|
|
// extractIPFromHeader will attempt to pull the real client IP from the given header when IP validation is enabled.
|
|
// currently, it will return the first valid IP address in header.
|
|
// when IP validation is disabled, it will simply return the value of the header without any inspection.
|
|
// Implementation is almost the same as in extractIPsFromHeader, but without allocation of []string.
|
|
func (r *DefaultReq) extractIPFromHeader(header string) string {
|
|
app := r.c.app
|
|
if app.config.EnableIPValidation {
|
|
headerValue := r.Get(header)
|
|
|
|
i := 0
|
|
j := -1
|
|
|
|
for {
|
|
var v4, v6 bool
|
|
|
|
// Manually splitting string without allocating slice, working with parts directly
|
|
i, j = j+1, j+2
|
|
|
|
if j > len(headerValue) {
|
|
break
|
|
}
|
|
|
|
for j < len(headerValue) && headerValue[j] != ',' {
|
|
switch headerValue[j] {
|
|
case ':':
|
|
v6 = true
|
|
case '.':
|
|
v4 = true
|
|
default:
|
|
// do nothing
|
|
}
|
|
j++
|
|
}
|
|
|
|
for i < j && headerValue[i] == ' ' {
|
|
i++
|
|
}
|
|
|
|
s := utils.TrimRight(headerValue[i:j], ' ')
|
|
|
|
if app.config.EnableIPValidation {
|
|
if (!v6 && !v4) || (v6 && !utils.IsIPv6(s)) || (v4 && !utils.IsIPv4(s)) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
if ip := r.c.fasthttp.RemoteIP(); ip != nil {
|
|
return ip.String()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// default behavior if IP validation is not enabled is just to return whatever value is
|
|
// in the proxy header. Even if it is empty or invalid
|
|
return r.Get(app.config.ProxyHeader)
|
|
}
|
|
|
|
// IPs returns a string slice of IP addresses specified in the X-Forwarded-For request header.
|
|
// When IP validation is enabled, only valid IPs are returned.
|
|
func (r *DefaultReq) IPs() []string {
|
|
return r.extractIPsFromHeader(HeaderXForwardedFor)
|
|
}
|
|
|
|
// Is returns the matching content type,
|
|
// if the incoming request's Content-Type HTTP header field matches the MIME type specified by the type parameter
|
|
func (r *DefaultReq) Is(extension string) bool {
|
|
extensionHeader := utils.GetMIME(extension)
|
|
if extensionHeader == "" {
|
|
return false
|
|
}
|
|
|
|
ct := r.c.app.toString(r.c.fasthttp.Request.Header.ContentType())
|
|
if i := strings.IndexByte(ct, ';'); i != -1 {
|
|
ct = ct[:i]
|
|
}
|
|
ct = utils.TrimSpace(ct)
|
|
return utils.EqualFold(ct, extensionHeader)
|
|
}
|
|
|
|
// Locals makes it possible to pass any values under keys scoped to the request
|
|
// and therefore available to all following routes that match the request.
|
|
//
|
|
// All the values are removed from ctx after returning from the top
|
|
// RequestHandler. Additionally, Close method is called on each value
|
|
// implementing io.Closer before removing the value from ctx.
|
|
func (r *DefaultReq) Locals(key any, value ...any) any {
|
|
if len(value) == 0 {
|
|
return r.c.fasthttp.UserValue(key)
|
|
}
|
|
r.c.fasthttp.SetUserValue(key, value[0])
|
|
return value[0]
|
|
}
|
|
|
|
// Locals function utilizing Go's generics feature.
|
|
// This function allows for manipulating and retrieving local values within a
|
|
// request context with a more specific data type.
|
|
//
|
|
// All the values are removed from ctx after returning from the top
|
|
// RequestHandler. Additionally, Close method is called on each value
|
|
// implementing io.Closer before removing the value from ctx.
|
|
func Locals[V any](c Ctx, key any, value ...V) V {
|
|
var v V
|
|
var ok bool
|
|
if len(value) == 0 {
|
|
v, ok = c.Locals(key).(V)
|
|
} else {
|
|
v, ok = c.Locals(key, value[0]).(V)
|
|
}
|
|
if !ok {
|
|
return v // return zero of type V
|
|
}
|
|
return v
|
|
}
|
|
|
|
// Method returns the HTTP request method for the context, optionally overridden by the provided argument.
|
|
// If no override is given or if the provided override is not a valid HTTP method, it returns the current method from the context.
|
|
// Otherwise, it updates the context's method and returns the overridden method as a string.
|
|
func (r *DefaultReq) Method(override ...string) string {
|
|
app := r.c.app
|
|
if len(override) == 0 {
|
|
// Nothing to override, just return current method from context
|
|
return app.method(r.c.methodInt)
|
|
}
|
|
|
|
method := utilsstrings.ToUpper(override[0])
|
|
methodInt := app.methodInt(method)
|
|
if methodInt == -1 {
|
|
// Provided override does not valid HTTP method, no override, return current method
|
|
return app.method(r.c.methodInt)
|
|
}
|
|
r.c.methodInt = methodInt
|
|
return method
|
|
}
|
|
|
|
// MultipartForm parse form entries from binary.
|
|
// This returns a map[string][]string, so given a key, the value will be a string slice.
|
|
func (r *DefaultReq) MultipartForm() (*multipart.Form, error) {
|
|
return r.c.fasthttp.MultipartFormWithLimit(r.c.app.config.BodyLimit)
|
|
}
|
|
|
|
// 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 *DefaultReq) OriginalURL() string {
|
|
return r.c.app.toString(r.c.fasthttp.Request.Header.RequestURI())
|
|
}
|
|
|
|
// Params is used to get the route parameters.
|
|
// Defaults to empty string "" if the param doesn't exist.
|
|
// If a default value is given, it will return that value if the param doesn't exist.
|
|
// 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 *DefaultReq) Params(key string, defaultValue ...string) string {
|
|
if key == "*" || key == "+" {
|
|
key += "1"
|
|
}
|
|
|
|
app := r.c.app
|
|
route := r.c.Route()
|
|
values := &r.c.values
|
|
for i := range route.Params {
|
|
if len(key) != len(route.Params[i]) {
|
|
continue
|
|
}
|
|
if route.Params[i] == key || (!app.config.CaseSensitive && utils.EqualFold(route.Params[i], key)) {
|
|
// if there is no value for the key
|
|
if len(values) <= i || values[i] == "" {
|
|
break
|
|
}
|
|
val := values[i]
|
|
return r.c.app.GetString(val)
|
|
}
|
|
}
|
|
return defaultString("", defaultValue)
|
|
}
|
|
|
|
// Params is used to get the route parameters.
|
|
// This function is generic and can handle different route parameters type values.
|
|
// If the generic type cannot be matched to a supported type, the function
|
|
// returns the default value (if provided) or the zero value of type V.
|
|
//
|
|
// Example:
|
|
//
|
|
// http://example.com/user/:user -> http://example.com/user/john
|
|
// Params[string](c, "user") -> returns john
|
|
//
|
|
// http://example.com/id/:id -> http://example.com/user/114
|
|
// Params[int](c, "id") -> returns 114 as integer.
|
|
//
|
|
// http://example.com/id/:number -> http://example.com/id/john
|
|
// Params[int](c, "number", 0) -> returns 0 because can't parse 'john' as integer.
|
|
func Params[V GenericType](c Ctx, key string, defaultValue ...V) V {
|
|
v, err := genericParseType[V](c.Params(key))
|
|
if err != nil && len(defaultValue) > 0 {
|
|
return defaultValue[0]
|
|
}
|
|
return v
|
|
}
|
|
|
|
// Scheme contains the request protocol string: http or https for TLS requests.
|
|
// Please use Config.TrustProxy to prevent header spoofing if your app is behind a proxy.
|
|
func (r *DefaultReq) Scheme() string {
|
|
ctx := r.c.fasthttp
|
|
if ctx.IsTLS() {
|
|
return schemeHTTPS
|
|
}
|
|
if !r.IsProxyTrusted() {
|
|
return schemeHTTP
|
|
}
|
|
|
|
app := r.c.app
|
|
scheme := schemeHTTP
|
|
const lenXHeaderName = 12
|
|
for key, val := range ctx.Request.Header.All() {
|
|
if len(key) < lenXHeaderName {
|
|
continue // Neither "X-Forwarded-" nor "X-Url-Scheme"
|
|
}
|
|
switch {
|
|
case utils.EqualFold(key[:len(xForwardedPrefix)], xForwardedPrefix):
|
|
if utils.EqualFold(key, xForwardedProtoBytes) ||
|
|
utils.EqualFold(key, xForwardedProtocolBytes) {
|
|
v := app.toString(val)
|
|
if before, _, found := strings.Cut(v, ","); found {
|
|
scheme = utils.TrimSpace(before)
|
|
} else {
|
|
scheme = utils.TrimSpace(v)
|
|
}
|
|
} else if utils.EqualFold(key, xForwardedSslBytes) && utils.EqualFold(val, onBytes) {
|
|
scheme = schemeHTTPS
|
|
}
|
|
|
|
case utils.EqualFold(key, xURLSchemeBytes):
|
|
scheme = utils.TrimSpace(app.toString(val))
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
return utilsstrings.ToLower(utils.TrimSpace(scheme))
|
|
}
|
|
|
|
// Protocol returns the HTTP protocol of request: HTTP/1.1 and HTTP/2.
|
|
func (r *DefaultReq) Protocol() string {
|
|
return r.c.app.toString(r.c.fasthttp.Request.Header.Protocol())
|
|
}
|
|
|
|
// Query returns the query string parameter in the url.
|
|
// Defaults to empty string "" if the query doesn't exist.
|
|
// If a default value is given, it will return that value if the query doesn't exist.
|
|
// 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 *DefaultReq) Query(key string, defaultValue ...string) string {
|
|
return Query(r.c, key, defaultValue...)
|
|
}
|
|
|
|
// Queries returns a map of query parameters and their values.
|
|
//
|
|
// GET /?name=alex&wanna_cake=2&id=
|
|
// Queries()["name"] == "alex"
|
|
// Queries()["wanna_cake"] == "2"
|
|
// Queries()["id"] == ""
|
|
//
|
|
// GET /?field1=value1&field1=value2&field2=value3
|
|
// Queries()["field1"] == "value2"
|
|
// Queries()["field2"] == "value3"
|
|
//
|
|
// GET /?list_a=1&list_a=2&list_a=3&list_b[]=1&list_b[]=2&list_b[]=3&list_c=1,2,3
|
|
// Queries()["list_a"] == "3"
|
|
// Queries()["list_b[]"] == "3"
|
|
// Queries()["list_c"] == "1,2,3"
|
|
//
|
|
// GET /api/search?filters.author.name=John&filters.category.name=Technology&filters[customer][name]=Alice&filters[status]=pending
|
|
// Queries()["filters.author.name"] == "John"
|
|
// Queries()["filters.category.name"] == "Technology"
|
|
// Queries()["filters[customer][name]"] == "Alice"
|
|
// Queries()["filters[status]"] == "pending"
|
|
func (r *DefaultReq) Queries() map[string]string {
|
|
app := r.c.app
|
|
queryArgs := r.c.fasthttp.QueryArgs()
|
|
|
|
m := make(map[string]string, queryArgs.Len())
|
|
for key, value := range queryArgs.All() {
|
|
m[app.toString(key)] = app.toString(value)
|
|
}
|
|
return m
|
|
}
|
|
|
|
// Query Retrieves the value of a query parameter from the request's URI.
|
|
// The function is generic and can handle query parameter values of different types.
|
|
// It takes the following parameters:
|
|
// - c: The context object representing the current request.
|
|
// - key: The name of the query parameter.
|
|
// - defaultValue: (Optional) The default value to return if the query parameter is not found or cannot be parsed.
|
|
// The function performs the following steps:
|
|
// 1. Type-asserts the context object to *DefaultCtx.
|
|
// 2. Retrieves the raw query parameter value from the request's URI.
|
|
// 3. Parses the raw value into the appropriate type based on the generic type parameter V.
|
|
// If parsing fails, the function checks if a default value is provided. If so, it returns the default value.
|
|
// 4. Returns the parsed value.
|
|
//
|
|
// If the generic type cannot be matched to a supported type, the function returns the default value (if provided) or the zero value of type V.
|
|
//
|
|
// Example usage:
|
|
//
|
|
// GET /?search=john&age=8
|
|
// name := Query[string](c, "search") // Returns "john"
|
|
// age := Query[int](c, "age") // Returns 8
|
|
// unknown := Query[string](c, "unknown", "default") // Returns "default" since the query parameter "unknown" is not found
|
|
func Query[V GenericType](c Ctx, key string, defaultValue ...V) V {
|
|
q := c.App().toString(c.RequestCtx().QueryArgs().Peek(key))
|
|
v, err := genericParseType[V](q)
|
|
if err != nil && len(defaultValue) > 0 {
|
|
return defaultValue[0]
|
|
}
|
|
return v
|
|
}
|
|
|
|
// Range returns a struct containing the type and a slice of ranges.
|
|
func (r *DefaultReq) Range(size int64) (Range, error) {
|
|
var (
|
|
rangeData Range
|
|
ranges string
|
|
)
|
|
rangeStr := utils.TrimSpace(r.Get(HeaderRange))
|
|
maxRanges := r.c.app.config.MaxRanges
|
|
const maxRangePrealloc = 8
|
|
prealloc := min(maxRanges, maxRangePrealloc)
|
|
if prealloc > 0 {
|
|
rangeData.Ranges = make([]RangeSet, 0, prealloc)
|
|
}
|
|
|
|
parseBound := func(value string) (int64, error) {
|
|
parsed, err := utils.ParseUint(value)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("parse range bound %q: %w", value, err)
|
|
}
|
|
if parsed > (math.MaxUint64 >> 1) {
|
|
return 0, ErrRangeMalformed
|
|
}
|
|
return int64(parsed), nil
|
|
}
|
|
|
|
before, after, found := strings.Cut(rangeStr, "=")
|
|
if !found || strings.IndexByte(after, '=') >= 0 {
|
|
return rangeData, ErrRangeMalformed
|
|
}
|
|
rangeData.Type = utilsstrings.ToLower(utils.TrimSpace(before))
|
|
if rangeData.Type != "bytes" {
|
|
return rangeData, ErrRangeMalformed
|
|
}
|
|
ranges = utils.TrimSpace(after)
|
|
|
|
var (
|
|
singleRange string
|
|
moreRanges = ranges
|
|
rangeCount int
|
|
)
|
|
for moreRanges != "" {
|
|
rangeCount++
|
|
if rangeCount > maxRanges {
|
|
r.c.DefaultRes.Status(StatusRequestedRangeNotSatisfiable)
|
|
r.c.DefaultRes.Set(HeaderContentRange, "bytes */"+utils.FormatInt(size)) //nolint:staticcheck // It is fine to ignore the static check
|
|
return rangeData, ErrRangeTooLarge
|
|
}
|
|
singleRange = moreRanges
|
|
if i := strings.IndexByte(moreRanges, ','); i >= 0 {
|
|
singleRange = moreRanges[:i]
|
|
moreRanges = utils.TrimSpace(moreRanges[i+1:])
|
|
} else {
|
|
moreRanges = ""
|
|
}
|
|
|
|
singleRange = utils.TrimSpace(singleRange)
|
|
|
|
var (
|
|
startStr, endStr string
|
|
i int
|
|
)
|
|
if i = strings.IndexByte(singleRange, '-'); i == -1 {
|
|
return rangeData, ErrRangeMalformed
|
|
}
|
|
startStr = utils.TrimSpace(singleRange[:i])
|
|
endStr = utils.TrimSpace(singleRange[i+1:])
|
|
|
|
start, startErr := parseBound(startStr)
|
|
end, endErr := parseBound(endStr)
|
|
if errors.Is(startErr, ErrRangeMalformed) || errors.Is(endErr, ErrRangeMalformed) {
|
|
return rangeData, ErrRangeMalformed
|
|
}
|
|
if startErr != nil { // -nnn
|
|
start = max(size-end, 0)
|
|
end = size - 1
|
|
} else if endErr != nil { // nnn-
|
|
end = size - 1
|
|
}
|
|
if end > size-1 { // limit last-byte-pos to current length
|
|
end = size - 1
|
|
}
|
|
if start > end || start < 0 {
|
|
continue
|
|
}
|
|
rangeData.Ranges = append(rangeData.Ranges, RangeSet{
|
|
Start: start,
|
|
End: end,
|
|
})
|
|
}
|
|
if len(rangeData.Ranges) < 1 {
|
|
r.c.DefaultRes.Status(StatusRequestedRangeNotSatisfiable)
|
|
r.c.DefaultRes.Set(HeaderContentRange, "bytes */"+utils.FormatInt(size)) //nolint:staticcheck // It is fine to ignore the static check
|
|
return rangeData, ErrRequestedRangeNotSatisfiable
|
|
}
|
|
|
|
return rangeData, nil
|
|
}
|
|
|
|
// Route returns the matched Route struct.
|
|
func (r *DefaultReq) Route() *Route {
|
|
return r.c.Route()
|
|
}
|
|
|
|
// Subdomains returns a slice of subdomains from the host, excluding the last `offset` components.
|
|
// If the offset is negative or exceeds the number of subdomains, an empty slice is returned.
|
|
// If the offset is zero every label (no trimming) is returned.
|
|
func (r *DefaultReq) Subdomains(offset ...int) []string {
|
|
o := 2
|
|
if len(offset) > 0 {
|
|
o = offset[0]
|
|
}
|
|
|
|
// Negative offset, return nothing.
|
|
if o < 0 {
|
|
return []string{}
|
|
}
|
|
|
|
// Normalize host according to RFC 3986
|
|
host := r.Hostname()
|
|
// Trim the trailing dot of a fully-qualified domain
|
|
if strings.HasSuffix(host, ".") {
|
|
host = utils.TrimRight(host, '.')
|
|
}
|
|
host = utilsstrings.ToLower(host)
|
|
|
|
// Decode punycode labels only when necessary
|
|
if strings.Contains(host, "xn--") {
|
|
if u, err := idna.Lookup.ToUnicode(host); err == nil {
|
|
host = utilsstrings.ToLower(u)
|
|
}
|
|
}
|
|
|
|
// Return nothing for IP addresses
|
|
ip := host
|
|
if strings.HasPrefix(ip, "[") && strings.HasSuffix(ip, "]") {
|
|
ip = ip[1 : len(ip)-1]
|
|
}
|
|
if utils.IsIPv4(ip) || utils.IsIPv6(ip) {
|
|
return []string{}
|
|
}
|
|
|
|
// Use stack-allocated array for typical domain names (up to 8 labels)
|
|
// This avoids heap allocation for most common cases
|
|
var partsBuf [8]string
|
|
parts := partsBuf[:0]
|
|
|
|
for part := range strings.SplitSeq(host, ".") {
|
|
parts = append(parts, part)
|
|
}
|
|
|
|
// offset == 0, caller wants everything.
|
|
if o == 0 {
|
|
// Need to return a copy since partsBuf is on the stack
|
|
result := make([]string, len(parts))
|
|
copy(result, parts)
|
|
return result
|
|
}
|
|
|
|
// If we trim away the whole slice (or more), nothing remains.
|
|
if o >= len(parts) {
|
|
return []string{}
|
|
}
|
|
|
|
// Return a heap-allocated copy of the relevant portion
|
|
result := make([]string, len(parts)-o)
|
|
copy(result, parts[:len(parts)-o])
|
|
return result
|
|
}
|
|
|
|
// Stale returns the inverse of Fresh, indicating if the client's cached response is considered stale.
|
|
func (r *DefaultReq) Stale() bool {
|
|
return !r.Fresh()
|
|
}
|
|
|
|
// IsProxyTrusted checks trustworthiness of remote ip.
|
|
// If Config.TrustProxy false, it returns false.
|
|
// IsProxyTrusted can check remote ip by proxy ranges and ip map.
|
|
func (r *DefaultReq) IsProxyTrusted() bool {
|
|
config := r.c.app.config
|
|
if !config.TrustProxy {
|
|
return false
|
|
}
|
|
|
|
remoteAddr := r.c.fasthttp.RemoteAddr()
|
|
switch remoteAddr.(type) {
|
|
case *net.UnixAddr:
|
|
return config.TrustProxyConfig.UnixSocket
|
|
case *net.TCPAddr, *net.UDPAddr:
|
|
// Keep existing RemoteIP/IP-map/CIDR checks for TCP/UDP paths as-is.
|
|
default:
|
|
// Unknown address type: do not trust by default.
|
|
return false
|
|
}
|
|
|
|
ip := r.c.fasthttp.RemoteIP()
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
|
|
if (config.TrustProxyConfig.Loopback && ip.IsLoopback()) ||
|
|
(config.TrustProxyConfig.Private && ip.IsPrivate()) ||
|
|
(config.TrustProxyConfig.LinkLocal && ip.IsLinkLocalUnicast()) {
|
|
return true
|
|
}
|
|
|
|
if _, trusted := config.TrustProxyConfig.ips[ip.String()]; trusted {
|
|
return true
|
|
}
|
|
|
|
for _, ipNet := range config.TrustProxyConfig.ranges {
|
|
if ipNet.Contains(ip) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// IsFromLocal will return true if request came from local.
|
|
func (r *DefaultReq) IsFromLocal() bool {
|
|
// Unix sockets are inherently local - only processes on the same host can connect.
|
|
remoteAddr := r.c.fasthttp.RemoteAddr()
|
|
if _, ok := remoteAddr.(*net.UnixAddr); ok {
|
|
return true
|
|
}
|
|
|
|
if ip := r.c.fasthttp.RemoteIP(); ip != nil {
|
|
return ip.IsLoopback()
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Release is a method to reset Req fields when to use ReleaseCtx()
|
|
func (r *DefaultReq) release() {
|
|
r.c = nil
|
|
}
|
|
|
|
func (r *DefaultReq) getBody() []byte {
|
|
return r.c.app.GetBytes(r.c.fasthttp.Request.Body())
|
|
}
|