fiber/client/hooks.go
Jinquan Wang b38be4bcb3
v3 (feature): client refactor (#1986)
*  v3: Move the client module to the client folder and fix the error

*  v3: add xml encoder and decoder

* 🚧 v3: design plugin and hook mechanism, complete simple get request

* 🚧 v3: reset add some field

* 🚧 v3: add doc and fix some error

* 🚧 v3: add header merge

* 🚧 v3: add query param

* 🚧 v3: change to fasthttp's header and args

*  v3: add body and ua setting

* 🚧 v3: add cookie support

* 🚧 v3: add path param support

*  v3: fix error test case

* 🚧 v3: add formdata and file support

* 🚧 v3: referer support

* 🚧 v3: reponse unmarshal

*  v3: finish API design

* 🔥 v3: remove plugin mechanism

* 🚧 v3: add timeout

* 🚧 v3: change path params pattern and add unit test for core

* ✏️ v3: error spell

*  v3: improve test coverage

*  perf: change test func name to fit project format

* 🚧 v3: handle error

* 🚧 v3: add unit test and fix error

* ️ chore: change func to improve performance

*  v3: add some unit test

*  v3: fix error test

* 🐛 fix: add cookie to response

*  v3: add unit test

*  v3: export raw field

* 🐛 fix: fix data race

* 🔒️ chore: change package

* 🐛 fix: data race

* 🐛 fix: test fail

*  feat: move core to req

* 🐛 fix: connection reuse

* 🐛 fix: data race

* 🐛 fix: data race

* 🔀 fix: change to testify

*  fix: fail test in windows

*  feat: response body save to file

*  feat: support tls config

* 🐛 fix: add err check

* 🎨 perf: fix some static check

*  feat: add proxy support

*  feat: add retry feature

* 🐛 fix: static check error

* 🎨 refactor: move som code

* docs: change readme

*  feat: extend axios API

* perf: change field to export field

*  chore: disable startup message

* 🐛 fix: fix test error

* chore: fix error test

* chore: fix test case

* feat: add some test to client

* chore: add test case

* chore: add test case

*  feat: add peek for client

*  chore: add test case

* ️ feat: lazy generate rand string

* 🚧 perf: add config test case

* 🐛 fix: fix merge error

* 🐛 fix utils error

*  add redirection

* 🔥 chore: delete deps

* perf: fix spell error

* 🎨 perf: spell error

*  feat: add logger

*  feat: add cookie jar

*  feat: logger with level

* 🎨 perf: change the field name

* perf: add jar test

* fix proxy test

* improve test coverage

* fix proxy tests

* add cookiejar support from pending fasthttp PR

* fix some lint errors.

* add benchmark for SetValWithStruct

* optimize

* update

* fix proxy middleware

* use panicf instead of errorf and fix panic on default logger

* update

* update

* cleanup comments

* cleanup comments

* fix golang-lint errors

* Update helper_test.go

* add more test cases

* add hostclient pool

* make it more thread safe
-> there is still something which is shared between the requests

* fixed some golangci-lint errors

* fix Test_Request_FormData test

* create new test suite

* just create client for once

* use random port instead of 3000

* remove client pooling and fix test suite

* fix data races on logger tests

* fix proxy tests

* fix global tests

* remove unused code

* fix logger test

* fix proxy tests

* fix linter

* use lock instead of rlock

* fix cookiejar data-race

* fix(client): race conditions

* fix(client): race conditions

* apply some reviews

* change client property name

* apply review

* add parallel benchmark for simple request

* apply review

* apply review

* fix log tests

* fix linter

* fix(client): return error in SetProxyURL instead of panic

---------

Co-authored-by: Muhammed Efe Çetin <efectn@protonmail.com>
Co-authored-by: René Werner <rene.werner@verivox.com>
Co-authored-by: Joey <fenny@gofiber.io>
Co-authored-by: René <rene@gofiber.io>
2024-03-04 08:49:14 +01:00

329 lines
7.7 KiB
Go

package client
import (
"errors"
"fmt"
"io"
"math/rand"
"mime/multipart"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"github.com/gofiber/utils/v2"
"github.com/valyala/fasthttp"
)
var (
protocolCheck = regexp.MustCompile(`^https?://.*$`)
headerAccept = "Accept"
applicationJSON = "application/json"
applicationXML = "application/xml"
applicationForm = "application/x-www-form-urlencoded"
multipartFormData = "multipart/form-data"
letterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
letterIdxBits = 6 // 6 bits to represent a letter index
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
)
// randString returns a random string with n length
func randString(n int) string {
b := make([]byte, n)
length := len(letterBytes)
src := rand.NewSource(time.Now().UnixNano())
for i, cache, remain := n-1, src.Int63(), letterIdxMax; i >= 0; {
if remain == 0 {
cache, remain = src.Int63(), letterIdxMax
}
if idx := int(cache & int64(letterIdxMask)); idx < length {
b[i] = letterBytes[idx]
i--
}
cache >>= int64(letterIdxBits)
remain--
}
return utils.UnsafeString(b)
}
// parserRequestURL will set the options for the hostclient
// and normalize the url.
// The baseUrl will be merge with request uri.
// Query params and path params deal in this function.
func parserRequestURL(c *Client, req *Request) error {
splitURL := strings.Split(req.url, "?")
// I don't want to judge splitURL length.
splitURL = append(splitURL, "")
// Determine whether to superimpose baseurl based on
// whether the URL starts with the protocol
uri := splitURL[0]
if !protocolCheck.MatchString(uri) {
uri = c.baseURL + uri
if !protocolCheck.MatchString(uri) {
return ErrURLFormat
}
}
// set path params
req.path.VisitAll(func(key, val string) {
uri = strings.ReplaceAll(uri, ":"+key, val)
})
c.path.VisitAll(func(key, val string) {
uri = strings.ReplaceAll(uri, ":"+key, val)
})
// set uri to request and other related setting
req.RawRequest.SetRequestURI(uri)
// merge query params
hashSplit := strings.Split(splitURL[1], "#")
hashSplit = append(hashSplit, "")
args := fasthttp.AcquireArgs()
defer func() {
fasthttp.ReleaseArgs(args)
}()
args.Parse(hashSplit[0])
c.params.VisitAll(func(key, value []byte) {
args.AddBytesKV(key, value)
})
req.params.VisitAll(func(key, value []byte) {
args.AddBytesKV(key, value)
})
req.RawRequest.URI().SetQueryStringBytes(utils.CopyBytes(args.QueryString()))
req.RawRequest.URI().SetHash(hashSplit[1])
return nil
}
// parserRequestHeader will make request header up.
// It will merge headers from client and request.
// Header should be set automatically based on data.
// User-Agent should be set.
func parserRequestHeader(c *Client, req *Request) error {
// set method
req.RawRequest.Header.SetMethod(req.Method())
// merge header
c.header.VisitAll(func(key, value []byte) {
req.RawRequest.Header.AddBytesKV(key, value)
})
req.header.VisitAll(func(key, value []byte) {
req.RawRequest.Header.AddBytesKV(key, value)
})
// according to data set content-type
switch req.bodyType {
case jsonBody:
req.RawRequest.Header.SetContentType(applicationJSON)
req.RawRequest.Header.Set(headerAccept, applicationJSON)
case xmlBody:
req.RawRequest.Header.SetContentType(applicationXML)
case formBody:
req.RawRequest.Header.SetContentType(applicationForm)
case filesBody:
req.RawRequest.Header.SetContentType(multipartFormData)
// set boundary
if req.boundary == boundary {
req.boundary += randString(16)
}
req.RawRequest.Header.SetMultipartFormBoundary(req.boundary)
default:
}
// set useragent
req.RawRequest.Header.SetUserAgent(defaultUserAgent)
if c.userAgent != "" {
req.RawRequest.Header.SetUserAgent(c.userAgent)
}
if req.userAgent != "" {
req.RawRequest.Header.SetUserAgent(req.userAgent)
}
// set referer
req.RawRequest.Header.SetReferer(c.referer)
if req.referer != "" {
req.RawRequest.Header.SetReferer(req.referer)
}
// set cookie
// add cookie form jar to req
if c.cookieJar != nil {
c.cookieJar.dumpCookiesToReq(req.RawRequest)
}
c.cookies.VisitAll(func(key, val string) {
req.RawRequest.Header.SetCookie(key, val)
})
req.cookies.VisitAll(func(key, val string) {
req.RawRequest.Header.SetCookie(key, val)
})
return nil
}
// parserRequestBody automatically serializes the data according to
// the data type and stores it in the body of the rawRequest
func parserRequestBody(c *Client, req *Request) error {
switch req.bodyType {
case jsonBody:
body, err := c.jsonMarshal(req.body)
if err != nil {
return err
}
req.RawRequest.SetBody(body)
case xmlBody:
body, err := c.xmlMarshal(req.body)
if err != nil {
return err
}
req.RawRequest.SetBody(body)
case formBody:
req.RawRequest.SetBody(req.formData.QueryString())
case filesBody:
return parserRequestBodyFile(req)
case rawBody:
if body, ok := req.body.([]byte); ok {
req.RawRequest.SetBody(body)
} else {
return ErrBodyType
}
case noBody:
return nil
}
return nil
}
// parserRequestBodyFile parses request body if body type is file
// this is an addition of parserRequestBody.
func parserRequestBodyFile(req *Request) error {
mw := multipart.NewWriter(req.RawRequest.BodyWriter())
err := mw.SetBoundary(req.boundary)
if err != nil {
return fmt.Errorf("set boundary error: %w", err)
}
defer func() {
err := mw.Close()
if err != nil {
return
}
}()
// add formdata
req.formData.VisitAll(func(key, value []byte) {
if err != nil {
return
}
err = mw.WriteField(utils.UnsafeString(key), utils.UnsafeString(value))
})
if err != nil {
return fmt.Errorf("write formdata error: %w", err)
}
// add file
b := make([]byte, 512)
for i, v := range req.files {
if v.name == "" && v.path == "" {
return ErrFileNoName
}
// if name is not exist, set name
if v.name == "" && v.path != "" {
v.path = filepath.Clean(v.path)
v.name = filepath.Base(v.path)
}
// if field name is not exist, set it
if v.fieldName == "" {
v.fieldName = "file" + strconv.Itoa(i+1)
}
// check the reader
if v.reader == nil {
v.reader, err = os.Open(v.path)
if err != nil {
return fmt.Errorf("open file error: %w", err)
}
}
// write file
w, err := mw.CreateFormFile(v.fieldName, v.name)
if err != nil {
return fmt.Errorf("create file error: %w", err)
}
for {
n, err := v.reader.Read(b)
if err != nil && !errors.Is(err, io.EOF) {
return fmt.Errorf("read file error: %w", err)
}
if errors.Is(err, io.EOF) {
break
}
_, err = w.Write(b[:n])
if err != nil {
return fmt.Errorf("write file error: %w", err)
}
}
err = v.reader.Close()
if err != nil {
return fmt.Errorf("close file error: %w", err)
}
}
return nil
}
// parserResponseHeader will parse the response header and store it in the response
func parserResponseCookie(c *Client, resp *Response, req *Request) error {
var err error
resp.RawResponse.Header.VisitAllCookie(func(key, value []byte) {
cookie := fasthttp.AcquireCookie()
err = cookie.ParseBytes(value)
if err != nil {
return
}
cookie.SetKeyBytes(key)
resp.cookie = append(resp.cookie, cookie)
})
if err != nil {
return err
}
// store cookies to jar
if c.cookieJar != nil {
c.cookieJar.parseCookiesFromResp(req.RawRequest.URI().Host(), req.RawRequest.URI().Path(), resp.RawResponse)
}
return nil
}
// logger is a response hook that logs the request and response
func logger(c *Client, resp *Response, req *Request) error {
if !c.debug {
return nil
}
c.logger.Debugf("%s\n", req.RawRequest.String())
c.logger.Debugf("%s\n", resp.RawResponse.String())
return nil
}