package client import ( "bytes" "context" "errors" "io" "iter" "path/filepath" "reflect" "slices" "strconv" "sync" "time" "github.com/gofiber/fiber/v3" "github.com/gofiber/utils/v2" "github.com/valyala/fasthttp" ) // WithStruct is implemented by types that allow data to be stored from a struct via reflection. type WithStruct interface { Add(name, obj string) Del(name string) } // bodyType defines the type of request body. type bodyType int // Enumeration of request body types. const ( noBody bodyType = iota jsonBody xmlBody formBody filesBody rawBody cborBody ) var ErrClientNil = errors.New("client cannot be nil") // Request contains all data related to an HTTP request. type Request struct { ctx context.Context //nolint:containedctx // Context is needed to be stored in the request. body any header *Header params *QueryParam cookies *Cookie path *PathParam client *Client formData *FormData RawRequest *fasthttp.Request url string method string userAgent string boundary string referer string files []*File timeout time.Duration maxRedirects int bodyType bodyType } // Method returns the HTTP method set in the Request. func (r *Request) Method() string { return r.method } // SetMethod sets the HTTP method for the Request. // It is recommended to use the specialized methods (e.g., Get, Post) instead. func (r *Request) SetMethod(method string) *Request { r.method = method return r } // URL returns the URL set in the Request. func (r *Request) URL() string { return r.url } // SetURL sets the URL for the Request. func (r *Request) SetURL(url string) *Request { r.url = url return r } // Client returns the Client instance associated with this Request. func (r *Request) Client() *Client { return r.client } // SetClient sets the Client instance for the Request. func (r *Request) SetClient(c *Client) *Request { if c == nil { panic(ErrClientNil) } r.client = c return r } // Context returns the context associated with the Request. // If not set, a background context is returned. func (r *Request) Context() context.Context { if r.ctx == nil { return context.Background() } return r.ctx } // SetContext sets the context for the Request, allowing request cancellation if ctx is done. // See https://blog.golang.org/context article and the "context" package documentation. func (r *Request) SetContext(ctx context.Context) *Request { r.ctx = ctx return r } // Header returns all values associated with the given header key. func (r *Request) Header(key string) []string { return r.header.PeekMultiple(key) } // Headers returns an iterator over all headers in the Request. // Use maps.Collect() to gather them into a map if needed. // // The returned values are only valid until the request object is released. // Do not store references to returned values; make copies instead. func (r *Request) Headers() iter.Seq2[string, []string] { return func(yield func(string, []string) bool) { peekKeys := r.header.PeekKeys() keys := make([][]byte, len(peekKeys)) copy(keys, peekKeys) // It is necessary to have immutable byte slice. for _, key := range keys { vals := r.header.PeekAll(utils.UnsafeString(key)) valsStr := make([]string, len(vals)) for i, v := range vals { valsStr[i] = utils.UnsafeString(v) } if !yield(utils.UnsafeString(key), valsStr) { return } } } } // AddHeader adds a single header field and value to the Request. func (r *Request) AddHeader(key, val string) *Request { r.header.Add(key, val) return r } // SetHeader sets a single header field and value in the Request, overriding any previously set value. func (r *Request) SetHeader(key, val string) *Request { r.header.Del(key) r.header.Set(key, val) return r } // AddHeaders adds multiple header fields and values at once. func (r *Request) AddHeaders(h map[string][]string) *Request { r.header.AddHeaders(h) return r } // SetHeaders sets multiple header fields and values at once, overriding previously set values. func (r *Request) SetHeaders(h map[string]string) *Request { r.header.SetHeaders(h) return r } // Param returns all values associated with the given query parameter. func (r *Request) Param(key string) []string { var res []string tmp := r.params.PeekMulti(key) for _, v := range tmp { res = append(res, utils.UnsafeString(v)) } return res } // Params returns an iterator over all query parameters in the Request. // Use maps.Collect() to gather them into a map if needed. // // The returned values are only valid until the request object is released. // Do not store references to returned values; make copies instead. func (r *Request) Params() iter.Seq2[string, []string] { return func(yield func(string, []string) bool) { keys := r.params.Keys() for _, key := range keys { if key == "" { continue } vals := r.params.PeekMulti(key) valsStr := make([]string, len(vals)) for i, v := range vals { valsStr[i] = utils.UnsafeString(v) } if !yield(key, valsStr) { return } } } } // AddParam adds a single query parameter and value to the Request. func (r *Request) AddParam(key, val string) *Request { r.params.Add(key, val) return r } // SetParam sets a single query parameter and value in the Request, overriding any previously set value. func (r *Request) SetParam(key, val string) *Request { r.params.Set(key, val) return r } // AddParams adds multiple query parameters and their values at once. func (r *Request) AddParams(m map[string][]string) *Request { r.params.AddParams(m) return r } // SetParams sets multiple query parameters and their values at once, overriding previously set values. func (r *Request) SetParams(m map[string]string) *Request { r.params.SetParams(m) return r } // SetParamsWithStruct sets multiple query parameters from a struct, overriding previously set values. func (r *Request) SetParamsWithStruct(v any) *Request { r.params.SetParamsWithStruct(v) return r } // DelParams deletes one or more query parameters. func (r *Request) DelParams(key ...string) *Request { for _, v := range key { r.params.Del(v) } return r } // UserAgent returns the User-Agent header set in the Request. func (r *Request) UserAgent() string { return r.userAgent } // SetUserAgent sets the User-Agent header, overriding any previously set value. func (r *Request) SetUserAgent(ua string) *Request { r.userAgent = ua return r } // Boundary returns the multipart boundary used by the Request. func (r *Request) Boundary() string { return r.boundary } // SetBoundary sets the multipart boundary. func (r *Request) SetBoundary(b string) *Request { r.boundary = b return r } // Referer returns the Referer header set in the Request. func (r *Request) Referer() string { return r.referer } // SetReferer sets the Referer header, overriding any previously set value. func (r *Request) SetReferer(referer string) *Request { r.referer = referer return r } // Cookie returns the value of a named cookie. // If the cookie does not exist, an empty string is returned. func (r *Request) Cookie(key string) string { if val, ok := (*r.cookies)[key]; ok { return val } return "" } // Cookies returns an iterator over all cookies. // Use maps.Collect() to gather them into a map if needed. func (r *Request) Cookies() iter.Seq2[string, string] { return func(yield func(string, string) bool) { for k, v := range *r.cookies { res := yield(k, v) if !res { return } } } } // SetCookie sets a single cookie, overriding any previously set value. func (r *Request) SetCookie(key, val string) *Request { r.cookies.SetCookie(key, val) return r } // SetCookies sets multiple cookies at once, overriding previously set values. func (r *Request) SetCookies(m map[string]string) *Request { r.cookies.SetCookies(m) return r } // SetCookiesWithStruct sets multiple cookies from a struct, overriding previously set values. func (r *Request) SetCookiesWithStruct(v any) *Request { r.cookies.SetCookiesWithStruct(v) return r } // DelCookies deletes one or more cookies. func (r *Request) DelCookies(key ...string) *Request { r.cookies.DelCookies(key...) return r } // PathParam returns the value of a named path parameter. // If the parameter does not exist, an empty string is returned. func (r *Request) PathParam(key string) string { if val, ok := (*r.path)[key]; ok { return val } return "" } // PathParams returns an iterator over all path parameters. // Use maps.Collect() to gather them into a map if needed. func (r *Request) PathParams() iter.Seq2[string, string] { return func(yield func(string, string) bool) { for k, v := range *r.path { if !yield(k, v) { return } } } } // SetPathParam sets a single path parameter and value, overriding any previously set value. func (r *Request) SetPathParam(key, val string) *Request { r.path.SetParam(key, val) return r } // SetPathParams sets multiple path parameters and values at once, overriding previously set values. func (r *Request) SetPathParams(m map[string]string) *Request { r.path.SetParams(m) return r } // SetPathParamsWithStruct sets multiple path parameters from a struct, overriding previously set values. func (r *Request) SetPathParamsWithStruct(v any) *Request { r.path.SetParamsWithStruct(v) return r } // DelPathParams deletes one or more path parameters. func (r *Request) DelPathParams(key ...string) *Request { r.path.DelParams(key...) return r } // ResetPathParams deletes all path parameters. func (r *Request) ResetPathParams() *Request { r.path.Reset() return r } // SetJSON sets the request body to a JSON-encoded value. func (r *Request) SetJSON(v any) *Request { r.body = v r.bodyType = jsonBody return r } // SetXML sets the request body to an XML-encoded value. func (r *Request) SetXML(v any) *Request { r.body = v r.bodyType = xmlBody return r } // SetCBOR sets the request body to a CBOR-encoded value. func (r *Request) SetCBOR(v any) *Request { r.body = v r.bodyType = cborBody return r } // SetRawBody sets the request body to raw bytes. func (r *Request) SetRawBody(v []byte) *Request { r.body = v r.bodyType = rawBody return r } // resetBody clears the existing body. If the current body type is filesBody and // the new type is formBody, the formBody setting is ignored to preserve files. func (r *Request) resetBody(t bodyType) { r.body = nil // If bodyType is filesBody and we attempt to set formBody, ignore the change. if r.bodyType == filesBody && t == formBody { return } r.bodyType = t } // FormData returns all values associated with a form field. func (r *Request) FormData(key string) []string { var res []string tmp := r.formData.PeekMulti(key) for _, v := range tmp { res = append(res, utils.UnsafeString(v)) } return res } // AllFormData returns an iterator over all form fields. // Use maps.Collect() to gather them into a map if needed. // // The returned values are only valid until the request object is released. // Do not store references to returned values; make copies instead. func (r *Request) AllFormData() iter.Seq2[string, []string] { return func(yield func(string, []string) bool) { keys := r.formData.Keys() for _, key := range keys { if key == "" { continue } vals := r.formData.PeekMulti(key) valsStr := make([]string, len(vals)) for i, v := range vals { valsStr[i] = utils.UnsafeString(v) } if !yield(key, valsStr) { return } } } } // AddFormData adds a single form field and value to the Request. func (r *Request) AddFormData(key, val string) *Request { r.formData.Add(key, val) r.resetBody(formBody) return r } // SetFormData sets a single form field and value, overriding any previously set value. func (r *Request) SetFormData(key, val string) *Request { r.formData.Set(key, val) r.resetBody(formBody) return r } // AddFormDataWithMap adds multiple form fields and values to the Request. func (r *Request) AddFormDataWithMap(m map[string][]string) *Request { r.formData.AddWithMap(m) r.resetBody(formBody) return r } // SetFormDataWithMap sets multiple form fields and values at once, overriding previously set values. func (r *Request) SetFormDataWithMap(m map[string]string) *Request { r.formData.SetWithMap(m) r.resetBody(formBody) return r } // SetFormDataWithStruct sets multiple form fields from a struct, overriding previously set values. func (r *Request) SetFormDataWithStruct(v any) *Request { r.formData.SetWithStruct(v) r.resetBody(formBody) return r } // DelFormData deletes one or more form fields. func (r *Request) DelFormData(key ...string) *Request { r.formData.DelData(key...) r.resetBody(formBody) return r } // File returns the file associated with the given name. // If no name was provided during addition, it attempts to match by the file's base name. func (r *Request) File(name string) *File { for _, v := range r.files { if v.name == "" { if filepath.Base(v.path) == name { return v } } else if v.name == name { return v } } return nil } // Files returns all files added to the Request. // // The returned values are only valid until the request object is released. // Do not store references to returned values; make copies instead. func (r *Request) Files() []*File { return r.files } // FileByPath returns the file associated with the given file path. func (r *Request) FileByPath(path string) *File { for _, v := range r.files { if v.path == path { return v } } return nil } // AddFile adds a single file by its path. func (r *Request) AddFile(path string) *Request { r.files = append(r.files, AcquireFile(SetFilePath(path))) r.resetBody(filesBody) return r } // AddFileWithReader adds a file using an io.ReadCloser. func (r *Request) AddFileWithReader(name string, reader io.ReadCloser) *Request { r.files = append(r.files, AcquireFile(SetFileName(name), SetFileReader(reader))) r.resetBody(filesBody) return r } // AddFiles adds multiple files at once. func (r *Request) AddFiles(files ...*File) *Request { r.files = append(r.files, files...) r.resetBody(filesBody) return r } // Timeout returns the timeout duration set in the Request. func (r *Request) Timeout() time.Duration { return r.timeout } // SetTimeout sets the timeout for the Request, overriding any previously set value. func (r *Request) SetTimeout(t time.Duration) *Request { r.timeout = t return r } // MaxRedirects returns the maximum number of redirects configured for the Request. func (r *Request) MaxRedirects() int { return r.maxRedirects } // SetMaxRedirects sets the maximum number of redirects, overriding any previously set value. func (r *Request) SetMaxRedirects(count int) *Request { r.maxRedirects = count return r } // checkClient ensures that a Client is set. If none is set, it defaults to the global defaultClient. func (r *Request) checkClient() { if r.client == nil { r.SetClient(defaultClient) } } // Get sends a GET request to the given URL. func (r *Request) Get(url string) (*Response, error) { return r.SetURL(url).SetMethod(fiber.MethodGet).Send() } // Post sends a POST request to the given URL. func (r *Request) Post(url string) (*Response, error) { return r.SetURL(url).SetMethod(fiber.MethodPost).Send() } // Head sends a HEAD request to the given URL. func (r *Request) Head(url string) (*Response, error) { return r.SetURL(url).SetMethod(fiber.MethodHead).Send() } // Put sends a PUT request to the given URL. func (r *Request) Put(url string) (*Response, error) { return r.SetURL(url).SetMethod(fiber.MethodPut).Send() } // Delete sends a DELETE request to the given URL. func (r *Request) Delete(url string) (*Response, error) { return r.SetURL(url).SetMethod(fiber.MethodDelete).Send() } // Options sends an OPTIONS request to the given URL. func (r *Request) Options(url string) (*Response, error) { return r.SetURL(url).SetMethod(fiber.MethodOptions).Send() } // Patch sends a PATCH request to the given URL. func (r *Request) Patch(url string) (*Response, error) { return r.SetURL(url).SetMethod(fiber.MethodPatch).Send() } // Custom sends a request with a custom HTTP method to the given URL. func (r *Request) Custom(url, method string) (*Response, error) { return r.SetURL(url).SetMethod(method).Send() } // Send executes the Request. func (r *Request) Send() (*Response, error) { r.checkClient() return newCore().execute(r.Context(), r.Client(), r) } // Reset clears the Request object, returning it to its default state. // Used by ReleaseRequest to recycle the object. func (r *Request) Reset() { r.url = "" r.method = fiber.MethodGet r.userAgent = "" r.referer = "" r.ctx = nil r.body = nil r.timeout = 0 r.maxRedirects = 0 r.bodyType = noBody r.boundary = boundary for len(r.files) != 0 { t := r.files[0] r.files = r.files[1:] ReleaseFile(t) } r.formData.Reset() r.path.Reset() r.cookies.Reset() r.header.Reset() r.params.Reset() r.RawRequest.Reset() } // Header wraps fasthttp.RequestHeader, storing headers for both client and request. type Header struct { *fasthttp.RequestHeader } // PeekMultiple returns multiple values of a header field with the same key. func (h *Header) PeekMultiple(key string) []string { var res []string byteKey := []byte(key) h.RequestHeader.VisitAll(func(k, value []byte) { if bytes.EqualFold(k, byteKey) { res = append(res, utils.UnsafeString(value)) } }) return res } // AddHeaders adds multiple headers from a map. func (h *Header) AddHeaders(r map[string][]string) { for k, v := range r { for _, vv := range v { h.Add(k, vv) } } } // SetHeaders sets multiple headers from a map, overriding previously set values. func (h *Header) SetHeaders(r map[string]string) { for k, v := range r { h.Del(k) h.Set(k, v) } } // QueryParam wraps fasthttp.Args for query parameters. type QueryParam struct { *fasthttp.Args } // Keys returns all keys from the query parameters. func (p *QueryParam) Keys() []string { keys := make([]string, 0, p.Len()) p.VisitAll(func(key, _ []byte) { keys = append(keys, utils.UnsafeString(key)) }) return slices.Compact(keys) } // AddParams adds multiple parameters from a map. func (p *QueryParam) AddParams(r map[string][]string) { for k, v := range r { for _, vv := range v { p.Add(k, vv) } } } // SetParams sets multiple parameters from a map, overriding previously set values. func (p *QueryParam) SetParams(r map[string]string) { for k, v := range r { p.Set(k, v) } } // SetParamsWithStruct sets multiple parameters from a struct. // Nested structs are not currently supported. func (p *QueryParam) SetParamsWithStruct(v any) { SetValWithStruct(p, "param", v) } // Cookie is a map used to store cookies. type Cookie map[string]string // Add adds a cookie key-value pair. func (c Cookie) Add(key, val string) { c[key] = val } // Del deletes a cookie by key. func (c Cookie) Del(key string) { delete(c, key) } // SetCookie sets a single cookie value. func (c Cookie) SetCookie(key, val string) { c[key] = val } // SetCookies sets multiple cookies from a map. func (c Cookie) SetCookies(m map[string]string) { for k, v := range m { c[k] = v } } // SetCookiesWithStruct sets cookies from a struct. // Nested structs are not currently supported. func (c Cookie) SetCookiesWithStruct(v any) { SetValWithStruct(c, "cookie", v) } // DelCookies deletes multiple cookies by keys. func (c Cookie) DelCookies(key ...string) { for _, v := range key { c.Del(v) } } // VisitAll iterates through all cookies, calling f for each. func (c Cookie) VisitAll(f func(key, val string)) { for k, v := range c { f(k, v) } } // Reset clears the Cookie map. func (c Cookie) Reset() { for k := range c { delete(c, k) } } // PathParam is a map used to store path parameters. type PathParam map[string]string // Add adds a path parameter key-value pair. func (p PathParam) Add(key, val string) { p[key] = val } // Del deletes a path parameter by key. func (p PathParam) Del(key string) { delete(p, key) } // SetParam sets a single path parameter. func (p PathParam) SetParam(key, val string) { p[key] = val } // SetParams sets multiple path parameters from a map. func (p PathParam) SetParams(m map[string]string) { for k, v := range m { p[k] = v } } // SetParamsWithStruct sets multiple path parameters from a struct. // Nested structs are not currently supported. func (p PathParam) SetParamsWithStruct(v any) { SetValWithStruct(p, "path", v) } // DelParams deletes multiple path parameters. func (p PathParam) DelParams(key ...string) { for _, v := range key { p.Del(v) } } // VisitAll iterates through all path parameters, calling f for each. func (p PathParam) VisitAll(f func(key, val string)) { for k, v := range p { f(k, v) } } // Reset clears the PathParam map. func (p PathParam) Reset() { for k := range p { delete(p, k) } } // FormData wraps fasthttp.Args for URL-encoded bodies and form data. type FormData struct { *fasthttp.Args } // Keys returns all keys from the form data. func (f *FormData) Keys() []string { keys := make([]string, 0, f.Len()) f.VisitAll(func(key, _ []byte) { keys = append(keys, utils.UnsafeString(key)) }) return slices.Compact(keys) } // Add adds a single form field. func (f *FormData) Add(key, val string) { f.Args.Add(key, val) } // Set sets a single form field, overriding previously set values. func (f *FormData) Set(key, val string) { f.Args.Set(key, val) } // AddWithMap adds multiple form fields from a map. func (f *FormData) AddWithMap(m map[string][]string) { for k, v := range m { for _, vv := range v { f.Add(k, vv) } } } // SetWithMap sets multiple form fields from a map, overriding previously set values. func (f *FormData) SetWithMap(m map[string]string) { for k, v := range m { f.Set(k, v) } } // SetWithStruct sets multiple form fields from a struct. // Nested structs are not currently supported. func (f *FormData) SetWithStruct(v any) { SetValWithStruct(f, "form", v) } // DelData deletes multiple form fields. func (f *FormData) DelData(key ...string) { for _, v := range key { f.Args.Del(v) } } // Reset clears the FormData object. func (f *FormData) Reset() { f.Args.Reset() } // File represents a file to be sent with the request. type File struct { reader io.ReadCloser name string fieldName string path string } // SetName sets the file's name. func (f *File) SetName(n string) { f.name = n } // SetFieldName sets the key associated with the file in the body. func (f *File) SetFieldName(n string) { f.fieldName = n } // SetPath sets the file's path. func (f *File) SetPath(p string) { f.path = p } // SetReader sets the file's reader, which will be closed in the parserBody hook. func (f *File) SetReader(r io.ReadCloser) { f.reader = r } // Reset clears the File object. func (f *File) Reset() { f.name = "" f.fieldName = "" f.path = "" f.reader = nil } var requestPool = &sync.Pool{ New: func() any { return &Request{ header: &Header{RequestHeader: &fasthttp.RequestHeader{}}, params: &QueryParam{Args: fasthttp.AcquireArgs()}, cookies: &Cookie{}, path: &PathParam{}, boundary: "--FiberFormBoundary", formData: &FormData{Args: fasthttp.AcquireArgs()}, files: make([]*File, 0), RawRequest: fasthttp.AcquireRequest(), } }, } // AcquireRequest returns a new (pooled) Request object. func AcquireRequest() *Request { req, ok := requestPool.Get().(*Request) if !ok { panic(errors.New("failed to type-assert to *Request")) } return req } // ReleaseRequest returns the Request object to the pool. // Do not use the released Request afterward to avoid data races. func ReleaseRequest(req *Request) { req.Reset() requestPool.Put(req) } var filePool sync.Pool // SetFileFunc defines a function that modifies a File object. type SetFileFunc func(f *File) // SetFileName sets the file name. func SetFileName(n string) SetFileFunc { return func(f *File) { f.SetName(n) } } // SetFileFieldName sets the file's field name. func SetFileFieldName(p string) SetFileFunc { return func(f *File) { f.SetFieldName(p) } } // SetFilePath sets the file path. func SetFilePath(p string) SetFileFunc { return func(f *File) { f.SetPath(p) } } // SetFileReader sets the file's reader. func SetFileReader(r io.ReadCloser) SetFileFunc { return func(f *File) { f.SetReader(r) } } // AcquireFile returns a (pooled) File object and applies the provided SetFileFunc functions to it. func AcquireFile(setter ...SetFileFunc) *File { fv := filePool.Get() if fv != nil { f, ok := fv.(*File) if !ok { panic(errors.New("failed to type-assert to *File")) } for _, v := range setter { v(f) } return f } f := &File{} for _, v := range setter { v(f) } return f } // ReleaseFile returns the File object to the pool. // Do not use the released File afterward to avoid data races. func ReleaseFile(f *File) { f.Reset() filePool.Put(f) } // SetValWithStruct sets values using a struct. The struct's fields are examined via reflection. // `p` is a type that implements WithStruct. `tagName` defines the struct tag to look for. // `v` is the struct containing data. // // Fields in `v` should be string, int, int8, int16, int32, int64, uint, // uint8, uint16, uint32, uint64, float32, float64, complex64, // complex128 or bool. Arrays or slices are inserted sequentially with the // same key. Other types are ignored. func SetValWithStruct(p WithStruct, tagName string, v any) { valueOfV := reflect.ValueOf(v) typeOfV := reflect.TypeOf(v) // The value should be a struct or a pointer to a struct. if typeOfV.Kind() == reflect.Pointer && typeOfV.Elem().Kind() == reflect.Struct { valueOfV = valueOfV.Elem() typeOfV = typeOfV.Elem() } else if typeOfV.Kind() != reflect.Struct { return } // A helper function to set values. var setVal func(name string, val reflect.Value) setVal = func(name string, val reflect.Value) { switch val.Kind() { case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: p.Add(name, strconv.Itoa(int(val.Int()))) case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: p.Add(name, strconv.FormatUint(val.Uint(), 10)) case reflect.Float32, reflect.Float64: p.Add(name, strconv.FormatFloat(val.Float(), 'f', -1, 64)) case reflect.Complex64, reflect.Complex128: p.Add(name, strconv.FormatComplex(val.Complex(), 'f', -1, 128)) case reflect.Bool: if val.Bool() { p.Add(name, "true") } else { p.Add(name, "false") } case reflect.String: p.Add(name, val.String()) case reflect.Slice, reflect.Array: for i := 0; i < val.Len(); i++ { setVal(name, val.Index(i)) } default: return } } for i := 0; i < typeOfV.NumField(); i++ { field := typeOfV.Field(i) if !field.IsExported() { continue } name := field.Tag.Get(tagName) if name == "" { name = field.Name } val := valueOfV.Field(i) // To cover slice and array, we delete the val then add it. p.Del(name) setVal(name, val) } }