starnet/curl.go

1818 lines
56 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package starnet
import (
"bytes"
"context"
"crypto/tls"
"encoding/json"
"fmt"
"io"
"mime/multipart"
"net"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"sync"
"time"
)
type Request struct {
ctx context.Context
doCtx context.Context // 用于在请求中传递上下文信息
uri string
method string
errInfo error
RequestOpts
}
func (r *Request) Clone() *Request {
clonedRequest := &Request{
ctx: r.ctx,
uri: r.uri,
method: r.method,
errInfo: r.errInfo,
RequestOpts: RequestOpts{
headers: CloneHeader(r.headers),
cookies: CloneCookies(r.cookies),
bodyFormData: CloneStringMapSlice(r.bodyFormData),
bodyFileData: CloneFiles(r.bodyFileData),
queries: CloneStringMapSlice(r.queries),
bodyDataBytes: CloneByteSlice(r.bodyDataBytes),
proxy: r.proxy,
timeout: r.timeout,
dialTimeout: r.dialTimeout,
dialFn: r.dialFn,
alreadyApply: r.alreadyApply,
doRawRequest: r.doRawRequest,
skipTLSVerify: r.skipTLSVerify,
autoFetchRespBody: r.autoFetchRespBody,
customIP: CloneStringSlice(r.customIP),
alreadySetLookUpIPfn: r.alreadySetLookUpIPfn,
lookUpIPfn: r.lookUpIPfn,
customDNS: CloneStringSlice(r.customDNS),
basicAuth: r.basicAuth,
autoCalcContentLength: r.autoCalcContentLength,
},
}
clonedRequest.rawClient = r.rawClient
// 手动深拷贝嵌套引用类型
if r.bodyDataReader != nil {
clonedRequest.bodyDataReader = r.bodyDataReader
}
if r.fileUploadRecallFn != nil {
clonedRequest.fileUploadRecallFn = r.fileUploadRecallFn
}
// 对于 tlsConfig 类型,需要手动复制
if r.tlsConfig != nil {
clonedRequest.tlsConfig = r.tlsConfig.Clone()
}
if r.transport != nil {
clonedRequest.transport = r.transport
}
if r.doRawRequest {
clonedRequest.rawRequest = r.rawRequest
}
if clonedRequest.rawRequest == nil {
clonedRequest.rawRequest, _ = http.NewRequestWithContext(clonedRequest.ctx, clonedRequest.method, clonedRequest.uri, nil)
}
return clonedRequest
}
// CloneHeader 复制 http.Header
func CloneHeader(original http.Header) http.Header {
newHeader := make(http.Header)
for key, values := range original {
copiedValues := make([]string, len(values))
copy(copiedValues, values)
newHeader[key] = copiedValues
}
return newHeader
}
// CloneCookies 复制 []*http.Cookie
func CloneCookies(original []*http.Cookie) []*http.Cookie {
cloned := make([]*http.Cookie, len(original))
for i, cookie := range original {
cloned[i] = &http.Cookie{
Name: cookie.Name,
Value: cookie.Value,
Path: cookie.Path,
Domain: cookie.Domain,
Expires: cookie.Expires,
RawExpires: cookie.RawExpires,
MaxAge: cookie.MaxAge,
Secure: cookie.Secure,
HttpOnly: cookie.HttpOnly,
SameSite: cookie.SameSite,
Raw: cookie.Raw,
Unparsed: append([]string(nil), cookie.Unparsed...),
}
}
return cloned
}
// CloneStringMapSlice 复制 map[string][]string
func CloneStringMapSlice(original map[string][]string) map[string][]string {
newMap := make(map[string][]string)
for key, values := range original {
copiedValues := make([]string, len(values))
copy(copiedValues, values)
newMap[key] = copiedValues
}
return newMap
}
// CloneFiles 复制 []RequestFile
func CloneFiles(original []RequestFile) []RequestFile {
newFiles := make([]RequestFile, len(original))
copy(newFiles, original)
return newFiles
}
// CloneByteSlice 复制 []byte
func CloneByteSlice(original []byte) []byte {
if original == nil {
return nil
}
newSlice := make([]byte, len(original))
copy(newSlice, original)
return newSlice
}
// CloneStringSlice 复制 []string
func CloneStringSlice(original []string) []string {
newSlice := make([]string, len(original))
copy(newSlice, original)
return newSlice
}
func (r *Request) Method() string {
return r.method
}
func (r *Request) SetMethod(method string) error {
method = strings.ToUpper(method)
if !validMethod(method) {
return fmt.Errorf("invalid method: %s", method)
}
r.method = method
r.rawRequest.Method = method
return nil
}
func (r *Request) SetMethodNoError(method string) *Request {
r.SetMethod(method)
return r
}
func (r *Request) Uri() string {
return r.uri
}
func (r *Request) SetUri(uri string) error {
if r.doRawRequest {
return fmt.Errorf("doRawRequest is true, cannot set uri")
}
u, err := url.Parse(uri)
if err != nil {
return fmt.Errorf("parse uri error: %s", err)
}
r.uri = uri
u.Host = removeEmptyPort(u.Host)
r.rawRequest.Host = u.Host
r.rawRequest.URL = u
if r.tlsConfig != nil {
r.tlsConfig.ServerName = u.Hostname()
}
return nil
}
func (r *Request) SetUriNoError(uri string) *Request {
r.SetUri(uri)
return r
}
func (r *Request) RawRequest() *http.Request {
return r.rawRequest
}
func (r *Request) SetRawRequest(rawRequest *http.Request) *Request {
r.rawRequest = rawRequest
return r
}
func (r *Request) RawClient() *http.Client {
return r.rawClient
}
func (r *Request) SetRawClient(rawClient *http.Client) *Request {
r.rawClient = rawClient
return r
}
// Do sends the HTTP request and returns the response.
func (r *Request) Do() (*Response, error) {
return Curl(r)
}
// Get sends a GET request to the specified URI and returns the response.
func (r *Request) Get() (*Response, error) {
err := r.SetMethod("GET")
if err != nil {
return nil, err
}
return Curl(r)
}
// Post sends a POST request with the provided data to the specified URI and returns the response.
func (r *Request) Post(data []byte) (*Response, error) {
err := r.SetMethod("POST")
if err != nil {
return nil, err
}
r.bodyDataBytes = data
r.bodyDataReader = nil
return Curl(r)
}
type RequestOpts struct {
rawRequest *http.Request
rawClient *http.Client
transport *http.Transport
customTransport bool
alreadyApply bool
bodyDataBytes []byte
bodyDataReader io.Reader
bodyFormData map[string][]string
bodyFileData []RequestFile
//以上优先度为 bodyDataReader> bodyDataBytes > bodyFormData > bodyFileData
fileUploadRecallFn func(filename string, upPos int64, total int64)
proxy string
timeout time.Duration
dialTimeout time.Duration
dialFn func(ctx context.Context, network, addr string) (net.Conn, error)
headers http.Header
cookies []*http.Cookie
queries map[string][]string
//doRawRequest=true 不对request修改直接发送
doRawRequest bool
skipTLSVerify bool
tlsConfig *tls.Config
autoFetchRespBody bool
customIP []string
alreadySetLookUpIPfn bool
lookUpIPfn func(ctx context.Context, host string) ([]net.IPAddr, error)
customDNS []string
basicAuth [2]string
autoCalcContentLength bool
}
func (r *RequestOpts) CustomTransport() bool {
return r.customTransport
}
func (r *RequestOpts) SetCustomTransport(customTransport bool) {
r.customTransport = customTransport
}
func (r *RequestOpts) FileUploadRecallFn() func(filename string, upPos int64, total int64) {
return r.fileUploadRecallFn
}
func (r *RequestOpts) SetFileUploadRecallFn(FileUploadRecallFn func(filename string, upPos int64, total int64)) {
r.fileUploadRecallFn = FileUploadRecallFn
}
func (r *Request) DialFn() func(ctx context.Context, network, addr string) (net.Conn, error) {
return r.dialFn
}
// SetDialFn sets the dial function for the request.
func (r *Request) SetDialFn(dialFn func(ctx context.Context, network, addr string) (net.Conn, error)) {
r.dialFn = dialFn
}
func (r *Request) AutoCalcContentLength() bool {
return r.autoCalcContentLength
}
// SetAutoCalcContentLength sets whether to automatically calculate the Content-Length header based on the request body.
// WARN: If set to true, the Content-Length header will be set to the length of the request body, which may cause issues with chunked transfer encoding.
// also the memory usage will be higher
// Note that this function will not work if doRawRequest is true
func (r *Request) SetAutoCalcContentLength(autoCalcContentLength bool) error {
if r.doRawRequest {
return fmt.Errorf("doRawRequest is true, cannot set autoCalcContentLength")
}
r.autoCalcContentLength = autoCalcContentLength
return nil
}
func (r *Request) SetAutoCalcContentLengthNoError(autoCalcContentLength bool) *Request {
r.SetAutoCalcContentLength(autoCalcContentLength)
return r
}
// BasicAuth returns the username and password provided in the request's Authorization header.
func (r *RequestOpts) BasicAuth() (string, string) {
return r.basicAuth[0], r.basicAuth[1]
}
// SetBasicAuth sets the request's Authorization header to use HTTP Basic Authentication with the provided username and password.
// Note: If doRawRequest is true, this function will nolonger work
func (r *RequestOpts) SetBasicAuth(username, password string) *RequestOpts {
r.basicAuth = [2]string{username, password}
return r
}
func (r *Request) CustomDNS() []string {
return r.customDNS
}
// SetCustomDNS sets the custom DNS servers for the request.
// Note: if LookUpIPfn is set, this function will not be used.
// if use custom Transport Dialer, this function will not work by default,but if the *http.Client is create by this package, it will work
func (r *Request) SetCustomDNS(customDNS []string) error {
for _, v := range customDNS {
if net.ParseIP(v) == nil {
return fmt.Errorf("invalid custom dns: %s", v)
}
}
r.customDNS = customDNS
return nil
}
// SetCustomDNSNoError sets the custom DNS servers for the request.
// Note: if LookUpIPfn is set, this function will not be used.
// if use custom Transport Dialer, this function will not work by default,but if the *http.Client is create by this package, it will work
func (r *Request) SetCustomDNSNoError(customDNS []string) *Request {
r.SetCustomDNS(customDNS)
return r
}
// AddCustomDNS adds custom DNS servers to the request.
// Note: if LookUpIPfn is set, this function will not be used.
// if use custom Transport Dialer, this function will not work by default,but if the *http.Client is create by this package, it will work
func (r *Request) AddCustomDNS(customDNS []string) error {
for _, v := range customDNS {
if net.ParseIP(v) == nil {
return fmt.Errorf("invalid custom dns: %s", v)
}
}
r.customDNS = customDNS
return nil
}
// AddCustomDNSNoError adds custom DNS servers to the request.
// Note: if LookUpIPfn is set, this function will not be used.
// if use custom Transport Dialer, this function will not work by default,but if the *http.Client is create by this package, it will work
func (r *Request) AddCustomDNSNoError(customDNS []string) *Request {
r.AddCustomDNS(customDNS)
return r
}
func (r *Request) LookUpIPfn() func(ctx context.Context, host string) ([]net.IPAddr, error) {
return r.lookUpIPfn
}
// SetLookUpIPfn sets the function used to look up IP addresses for a given host.
// If lookUpIPfn is nil, it will use the default resolver's LookupIPAddr function.
// Note: if use custom Transport Dialer, this function will not work by default,but if the *http.Client is create by this package, it will work
// Note: if CustomHostIP is set, this function will not be used.
func (r *Request) SetLookUpIPfn(lookUpIPfn func(ctx context.Context, host string) ([]net.IPAddr, error)) *Request {
if lookUpIPfn == nil {
r.alreadySetLookUpIPfn = false
r.lookUpIPfn = net.DefaultResolver.LookupIPAddr
return r
}
r.lookUpIPfn = lookUpIPfn
r.alreadySetLookUpIPfn = true
return r
}
// CustomHostIP returns the custom IP addresses used for the request.
func (r *Request) CustomHostIP() []string {
return r.customIP
}
// SetCustomHostIP sets the custom IP addresses used for the request.
// if you want to use a specific IP address for a host without DNS resolution, you can set this.
// Set nil to clear the custom IP addresses.
// Note: lookUpIPfn will not be used if customIP is set.
func (r *Request) SetCustomHostIP(customIP []string) *Request {
r.customIP = customIP
return r
}
// AddCustomHostIP adds a custom IP address to the request.
func (r *Request) AddCustomHostIP(customIP string) *Request {
r.customIP = append(r.customIP, customIP)
return r
}
// BodyDataBytes returns the raw body data as a byte slice.
func (r *Request) BodyDataBytes() []byte {
return r.bodyDataBytes
}
// SetBodyDataBytes sets the raw body data for the request.
// The priority order of the data is: bodyDataReader > **bodyDataBytes** > bodyFormData > bodyFileData.
// Note: If doRawRequest is true, this function will not work.
func (r *Request) SetBodyDataBytes(bodyDataBytes []byte) *Request {
r.bodyDataBytes = bodyDataBytes
return r
}
// BodyDataReader returns the raw body data as an io.Reader.
func (r *Request) BodyDataReader() io.Reader {
return r.bodyDataReader
}
// SetBodyDataReader sets the raw body data for the request as an io.Reader.
// The priority order of the data is: **bodyDataReader** > bodyDataBytes > bodyFormData > bodyFileData.
// Note: If doRawRequest is true, this function will not work.
func (r *Request) SetBodyDataReader(bodyDataReader io.Reader) *Request {
r.bodyDataReader = bodyDataReader
return r
}
// BodyFormData returns the form data as a map of string slices.
// The priority order of the data is: bodyDataReader > bodyDataBytes > **bodyFormData** > bodyFileData.
// Note: If doRawRequest is true, this function will not work.
func (r *Request) BodyFormData() map[string][]string {
return r.bodyFormData
}
// SetBodyFormData sets the form data for the request.
// The priority order of the data is: bodyDataReader > bodyDataBytes > **bodyFormData** > bodyFileData.
// Note: If doRawRequest is true, this function will not work.
func (r *Request) SetBodyFormData(bodyFormData map[string][]string) *Request {
r.bodyFormData = bodyFormData
return r
}
// SetFormData is an alias for SetBodyFormData.
// It allows you to set form data in the request body.
// This is useful when you want to use a more descriptive name for the function.
// The priority order of the data is: bodyDataReader > bodyDataBytes > **bodyFormData** > bodyFileData.
// Note: If doRawRequest is true, this function will not work.
func (r *Request) SetFormData(bodyFormData map[string][]string) *Request {
return r.SetBodyFormData(bodyFormData)
}
// AddFormMapData adds form data from a map to the request body.
// The priority order of the data is: bodyDataReader > bodyDataBytes > **bodyFormData** > bodyFileData.
// Note: If doRawRequest is true, this function will not work.
func (r *Request) AddFormMapData(bodyFormData map[string]string) *Request {
for k, v := range bodyFormData {
r.bodyFormData[k] = append(r.bodyFormData[k], v)
}
return r
}
// AddFormData adds a single key-value pair to the form data in the request body.
// The priority order of the data is: bodyDataReader > bodyDataBytes > **bodyFormData** > bodyFileData.
// Note: If doRawRequest is true, this function will not work.
func (r *Request) AddFormData(k, v string) *Request {
r.bodyFormData[k] = append(r.bodyFormData[k], v)
return r
}
// BodyFileData returns the file data as a slice of RequestFile.
// The priority order of the data is: bodyDataReader > bodyDataBytes > bodyFormData > **bodyFileData**.
// Note: If doRawRequest is true, this function will not work.
func (r *Request) BodyFileData() []RequestFile {
return r.bodyFileData
}
// SetBodyFileData sets the file data for the request.
// The priority order of the data is: bodyDataReader > bodyDataBytes > bodyFormData > **bodyFileData**.
// Note: If doRawRequest is true, this function will not work.
func (r *Request) SetBodyFileData(bodyFileData []RequestFile) *Request {
r.bodyFileData = bodyFileData
return r
}
// Proxy returns the proxy URL for the request.
func (r *Request) Proxy() string {
return r.proxy
}
// SetProxy sets the proxy URL for the request.
func (r *Request) SetProxy(proxy string) *Request {
r.proxy = proxy
return r
}
// Timeout returns the timeout duration for the request.
func (r *Request) Timeout() time.Duration {
return r.timeout
}
// SetTimeout sets the timeout duration for the request.
func (r *Request) SetTimeout(timeout time.Duration) *Request {
r.timeout = timeout
return r
}
// DialTimeout returns the dial timeout duration for the request.
func (r *Request) DialTimeout() time.Duration {
return r.dialTimeout
}
// SetDialTimeout sets the dial timeout duration for the request.
func (r *Request) SetDialTimeout(dialTimeout time.Duration) *Request {
r.dialTimeout = dialTimeout
return r
}
// Headers returns the request headers as an http.Header.
func (r *Request) Headers() http.Header {
return r.headers
}
// SetHeaders sets the request headers using an http.Header.
// Note: If doRawRequest is true, this function will not work.
func (r *Request) SetHeaders(headers http.Header) *Request {
r.headers = headers
return r
}
// AddHeader adds a single header to the request.
// This function will append the header if it already exists.
// Note: If doRawRequest is true, this function will not work.
func (r *Request) AddHeader(key, val string) *Request {
r.headers.Add(key, val)
return r
}
// SetHeader sets a single header in the request.
// Note: If doRawRequest is true, this function will not work.
func (r *Request) SetHeader(key, val string) *Request {
r.headers.Set(key, val)
return r
}
// DeleteHeader removes a header from the request.
// if the header has multiple values, it will remove all values for that header.
// Note: If doRawRequest is true, this function will not work.
func (r *Request) DeleteHeader(key string) *Request {
r.headers.Del(key)
return r
}
// SetContentType sets the Content-Type header for the request.
// This function will overwrite any existing Content-Type header.
// Note: If doRawRequest is true, this function will not work.
func (r *Request) SetContentType(ct string) *Request {
r.headers.Set("Content-Type", ct)
return r
}
// SetUserAgent sets the User-Agent header for the request.
// This function will overwrite any existing User-Agent header.
// Note: If doRawRequest is true, this function will not work.
func (r *Request) SetUserAgent(ua string) *Request {
r.headers.Set("User-Agent", ua)
return r
}
// Cookies returns the request cookies as a slice of http.Cookie.
func (r *Request) Cookies() []*http.Cookie {
return r.cookies
}
// SetCookies sets the request cookies using a slice of http.Cookie.
// you can also use SetHeader("Cookie", "cookie1=value1; cookie2=value2") to set cookies.
// Note: If doRawRequest is true, this function will not work.
func (r *Request) SetCookies(cookies []*http.Cookie) *Request {
r.cookies = cookies
return r
}
// Transport returns the http.Transport used for the request.
func (r *Request) Transport() *http.Transport {
return r.transport
}
// SetTransport set the http.Transport used for the request.
// Note: If doRawClient is true, this function will not work.
func (r *Request) SetTransport(transport *http.Transport) *Request {
r.transport = transport
r.customTransport = true
return r
}
// Queries returns the request queries as a map of string slices.
func (r *Request) Queries() map[string][]string {
return r.queries
}
// SetQueries sets the request queries using a map of string slices.
func (r *Request) SetQueries(queries map[string][]string) *Request {
r.queries = queries
return r
}
// AddQueries adds multiple query parameters to the request.
func (r *Request) AddQueries(queries map[string]string) *Request {
for k, v := range queries {
r.queries[k] = append(r.queries[k], v)
}
return r
}
// AddQuery adds a single query parameter to the request.
func (r *Request) AddQuery(key, value string) *Request {
r.queries[key] = append(r.queries[key], value)
return r
}
// DelQueryKv removes a specific value from a query parameter.
func (r *Request) DelQueryKv(key, value string) *Request {
if _, ok := r.queries[key]; !ok {
return r
}
for i, v := range r.queries[key] {
if v == value {
r.queries[key] = append(r.queries[key][:i], r.queries[key][i+1:]...)
}
}
return r
}
// DelQuery removes a query parameter from the request.
func (r *Request) DelQuery(key string) *Request {
if _, ok := r.queries[key]; !ok {
return r
}
delete(r.queries, key)
return r
}
// DoRawRequest returns whether the request will be sent as a raw request.
func (r *Request) DoRawRequest() bool {
return r.doRawRequest
}
// SetDoRawRequest sets whether the request will be sent as a raw request without any modifications.
// you can use this with function SetRawRequest to set a custom http.Request.
func (r *Request) SetDoRawRequest(doRawRequest bool) *Request {
r.doRawRequest = doRawRequest
return r
}
// SkipTLSVerify returns whether the request will skip TLS verification.
func (r *RequestOpts) SkipTLSVerify() bool {
return r.skipTLSVerify
}
// SetSkipTLSVerify Sets whether the request will skip TLS verification.
func (r *Request) SetSkipTLSVerify(skipTLSVerify bool) *Request {
r.skipTLSVerify = skipTLSVerify
return r
}
// TlsConfig returns the TLS configuration used for the request.
func (r *Request) TlsConfig() *tls.Config {
return r.tlsConfig
}
// SetTlsConfig sets the TLS configuration for the request.
// Note: If you use SetSkipTLSVerify function, it will automatically set the InsecureSkipVerify field to true in the tls.Config.
func (r *Request) SetTlsConfig(tlsConfig *tls.Config) *Request {
r.tlsConfig = tlsConfig
return r
}
// AutoFetchRespBody returns whether the response body will be automatically fetched.
func (r *Request) AutoFetchRespBody() bool {
return r.autoFetchRespBody
}
// SetAutoFetchRespBody sets whether the response body will be automatically fetched after the request is sent.
// If set to true, the response body will be read and stored in the Response object.
// if the body is too large, it may cause high memory usage.
// If set to false, you will need to manually read the response body using Response.Body() method.
func (r *Request) SetAutoFetchRespBody(autoFetchRespBody bool) *Request {
r.autoFetchRespBody = autoFetchRespBody
return r
}
// ResetReqHeader resets the request headers to an empty http.Header.
func (r *Request) ResetReqHeader() *Request {
r.headers = make(http.Header)
return r
}
// ResetReqCookies resets the request cookies to an empty slice.
func (r *Request) ResetReqCookies() *Request {
r.cookies = []*http.Cookie{}
return r
}
// AddSimpleCookie add a key-value cookie to the request.
// the path will be set to "/"
func (r *Request) AddSimpleCookie(key, value string) *Request {
r.cookies = append(r.cookies, &http.Cookie{Name: key, Value: value, Path: "/"})
return r
}
// AddCookie adds a cookie to the request with the specified key, value, and path.
func (r *Request) AddCookie(key, value, path string) *Request {
r.cookies = append(r.cookies, &http.Cookie{Name: key, Value: value, Path: path})
return r
}
// AddFile adds a file to the request with the specified form name and file path.
// The file will be read and uploaded as a multipart/form-data request.
// the file type will be set to "application/octet-stream" by default.
func (r *Request) AddFile(formName, filepath string) error {
stat, err := os.Stat(filepath)
if err != nil {
return err
}
r.bodyFileData = append(r.bodyFileData, RequestFile{
FormName: formName,
FileName: stat.Name(),
FileData: nil,
FileSize: stat.Size(),
FileType: "application/octet-stream",
FilePath: filepath,
})
return nil
}
// AddFileStream adds a file to the request with the specified form name, filename, size, and io.Reader stream.
// The file will be read and uploaded as a multipart/form-data request.
// the file type will be set to "application/octet-stream" by default.
func (r *Request) AddFileStream(formName, filename string, size int64, stream io.Reader) error {
r.bodyFileData = append(r.bodyFileData, RequestFile{
FormName: formName,
FileName: filename,
FileData: stream,
FileSize: size,
FileType: "application/octet-stream",
})
return nil
}
// AddFileWithName adds a file to the request with the specified form name, file path, and filename.
// you can specify a custom filename for the file being uploaded.
// The file will be read and uploaded as a multipart/form-data request.
// the file type will be set to "application/octet-stream" by default.
func (r *Request) AddFileWithName(formName, filepath, filename string) error {
stat, err := os.Stat(filepath)
if err != nil {
return err
}
r.bodyFileData = append(r.bodyFileData, RequestFile{
FormName: formName,
FileName: filename,
FileData: nil,
FileSize: stat.Size(),
FileType: "application/octet-stream",
FilePath: filepath,
})
return nil
}
// AddFileWithType adds a file to the request with the specified form name, file path, and file type.
// you can specify a custom file type for the file being uploaded.
// The file will be read and uploaded as a multipart/form-data request.
func (r *Request) AddFileWithType(formName, filepath, filetype string) error {
stat, err := os.Stat(filepath)
if err != nil {
return err
}
r.bodyFileData = append(r.bodyFileData, RequestFile{
FormName: formName,
FileName: stat.Name(),
FileData: nil,
FileSize: stat.Size(),
FileType: filetype,
FilePath: filepath,
})
return nil
}
// AddFileWithNameAndType adds a file to the request with the specified form name, file path, filename, and file type.
// you can specify a custom filename and file type for the file being uploaded.
// The file will be read and uploaded as a multipart/form-data request.
func (r *Request) AddFileWithNameAndType(formName, filepath, filename, filetype string) error {
stat, err := os.Stat(filepath)
if err != nil {
return err
}
r.bodyFileData = append(r.bodyFileData, RequestFile{
FormName: formName,
FileName: filename,
FileData: nil,
FileSize: stat.Size(),
FileType: filetype,
FilePath: filepath,
})
return nil
}
// AddFileStreamWithType adds a file to the request with the specified form name, filename, file type, size, and io.Reader stream.
// The file will be read and uploaded as a multipart/form-data request.
// you can specify a custom file type for the file being uploaded.
func (r *Request) AddFileStreamWithType(formName, filename, filetype string, size int64, stream io.Reader) error {
r.bodyFileData = append(r.bodyFileData, RequestFile{
FormName: formName,
FileName: filename,
FileData: stream,
FileSize: size,
FileType: filetype,
})
return nil
}
// AddFileNoError adds a file to the request with the specified form name and file path.
// It will not return an error if the file cannot be added.
// this function is useful for chaining methods without error handling.
func (r *Request) AddFileNoError(formName, filepath string) *Request {
r.AddFile(formName, filepath)
return r
}
// AddFileWithNameNoError adds a file to the request with the specified form name, file path, and filename.
// It will not return an error if the file cannot be added.
// this function is useful for chaining methods without error handling.
func (r *Request) AddFileWithNameNoError(formName, filepath, filename string) *Request {
r.AddFileWithName(formName, filepath, filename)
return r
}
// AddFileWithTypeNoError adds a file to the request with the specified form name, file path, and file type.
// It will not return an error if the file cannot be added.
// this function is useful for chaining methods without error handling.
func (r *Request) AddFileWithTypeNoError(formName, filepath, filetype string) *Request {
r.AddFileWithType(formName, filepath, filetype)
return r
}
// AddFileWithNameAndTypeNoError adds a file to the request with the specified form name, file path, filename, and file type.
// It will not return an error if the file cannot be added.
// this function is useful for chaining methods without error handling.
func (r *Request) AddFileWithNameAndTypeNoError(formName, filepath, filename, filetype string) *Request {
r.AddFileWithNameAndType(formName, filepath, filename, filetype)
return r
}
// AddFileStreamNoError adds a file to the request with the specified form name, filename, size, and io.Reader stream.
// It will not return an error if the file cannot be added.
// this function is useful for chaining methods without error handling.
func (r *Request) AddFileStreamNoError(formName, filename string, size int64, stream io.Reader) *Request {
r.AddFileStream(formName, filename, size, stream)
return r
}
// AddFileStreamWithTypeNoError adds a file to the request with the specified form name, filename, file type, size, and io.Reader stream.
// It will not return an error if the file cannot be added.
// this function is useful for chaining methods without error handling.
func (r *Request) AddFileStreamWithTypeNoError(formName, filename, filetype string, size int64, stream io.Reader) *Request {
r.AddFileStreamWithType(formName, filename, filetype, size, stream)
return r
}
// HttpClient returns the http.Client used for the request.
func (r *Request) HttpClient() (*http.Client, error) {
err := applyOptions(r)
if err != nil {
return nil, err
}
return r.rawClient, nil
}
type RequestFile struct {
FormName string
FileName string
FileData io.Reader
FileSize int64
FileType string
FilePath string
}
type RequestOpt func(opt *RequestOpts) error
// WithDialTimeout sets the dial timeout for the request.
// If use custom Transport Dialer, this function will nolonger work.
func WithDialTimeout(timeout time.Duration) RequestOpt {
return func(opt *RequestOpts) error {
opt.dialTimeout = timeout
return nil
}
}
// WithDial sets a custom dial function for the request.
// functions like WithDialTimeout will nolonger work if this function is used.
// If use custom Transport Dialer, this function will nolonger work.
func WithDial(fn func(ctx context.Context, network string, addr string) (net.Conn, error)) RequestOpt {
return func(opt *RequestOpts) error {
opt.dialFn = fn
return nil
}
}
// WithTimeout sets the timeout for the request.
// If use custom Transport Dialer, this function will nolonger work.
func WithTimeout(timeout time.Duration) RequestOpt {
return func(opt *RequestOpts) error {
opt.timeout = timeout
return nil
}
}
// WithTlsConfig sets the TLS configuration for the request.
// If use custom Transport Dialer, this function will nolonger work.
func WithTlsConfig(tlscfg *tls.Config) RequestOpt {
return func(opt *RequestOpts) error {
opt.tlsConfig = tlscfg
return nil
}
}
// WithHeaders sets the request headers using an http.Header.
// If doRawRequest is true, this function will not work.
func WithHeader(key, val string) RequestOpt {
return func(opt *RequestOpts) error {
opt.headers.Set(key, val)
return nil
}
}
// WithHeaderMap sets the request headers using a map of string to string.
// If doRawRequest is true, this function will not work.
func WithHeaderMap(header map[string]string) RequestOpt {
return func(opt *RequestOpts) error {
for key, val := range header {
opt.headers.Set(key, val)
}
return nil
}
}
// WithReader sets the request body data using an io.Reader.
// The priority order of the data is: **bodyDataReader** > bodyDataBytes > bodyFormData > bodyFileData.
// If doRawRequest is true, this function will nolonger work.
func WithReader(r io.Reader) RequestOpt {
return func(opt *RequestOpts) error {
opt.bodyDataReader = r
return nil
}
}
// WithBytes sets the request body data using a byte slice.
// The priority order of the data is: bodyDataReader > **bodyDataBytes** > bodyFormData > bodyFileData.
// If doRawRequest is true, this function will nolonger work.
func WithBytes(r []byte) RequestOpt {
return func(opt *RequestOpts) error {
opt.bodyDataBytes = r
return nil
}
}
// WithFormData sets the request body data using a map of string slices.
// The priority order of the data is: bodyDataReader > bodyDataBytes > **bodyFormData** > bodyFileData.
// If doRawRequest is true, this function will nolonger work.
func WithFormData(data map[string][]string) RequestOpt {
return func(opt *RequestOpts) error {
opt.bodyFormData = data
return nil
}
}
// WithFileDatas sets the request body file data using a slice of RequestFile.
// The priority order of the data is: bodyDataReader > bodyDataBytes > bodyFormData > **bodyFileData**.
// If doRawRequest is true, this function will nolonger work.
func WithFileDatas(data []RequestFile) RequestOpt {
return func(opt *RequestOpts) error {
opt.bodyFileData = data
return nil
}
}
// WithFileData sets the request body file data using a single RequestFile.
// The priority order of the data is: bodyDataReader > bodyDataBytes > bodyFormData > **bodyFileData**.
// If doRawRequest is true, this function will nolonger work.
func WithFileData(data RequestFile) RequestOpt {
return func(opt *RequestOpts) error {
opt.bodyFileData = append(opt.bodyFileData, data)
return nil
}
}
// WithAddFile adds a file to the request with the specified form name and file path.
// The priority order of the data is: bodyDataReader > bodyDataBytes > bodyFormData > **bodyFileData**.
// The file will be read and uploaded as a multipart/form-data request.
// the file type will be set to "application/octet-stream" by default.
// If doRawRequest is true, this function will nolonger work.
func WithAddFile(formName, filepath string) RequestOpt {
return func(opt *RequestOpts) error {
stat, err := os.Stat(filepath)
if err != nil {
return err
}
opt.bodyFileData = append(opt.bodyFileData, RequestFile{
FormName: formName,
FileName: stat.Name(),
FileData: nil,
FileSize: stat.Size(),
FileType: "application/octet-stream",
FilePath: filepath,
})
return nil
}
}
// WithAddFileWithName adds a file to the request with the specified form name, file path, and filename.
// you can specify a custom filename for the file being uploaded.
// The priority order of the data is: bodyDataReader > bodyDataBytes > bodyFormData > **bodyFileData**.
// The file will be read and uploaded as a multipart/form-data request.
// If doRawRequest is true, this function will nolonger work.
func WithAddFileWithName(formName, filepath, filename string) RequestOpt {
return func(opt *RequestOpts) error {
stat, err := os.Stat(filepath)
if err != nil {
return err
}
opt.bodyFileData = append(opt.bodyFileData, RequestFile{
FormName: formName,
FileName: filename,
FileData: nil,
FileSize: stat.Size(),
FileType: "application/octet-stream",
})
return nil
}
}
// WithAddFileWithType adds a file to the request with the specified form name, file path, and file type.
// you can specify a custom file type for the file being uploaded.
// The priority order of the data is: bodyDataReader > bodyDataBytes > bodyFormData > **bodyFileData**.
// The file will be read and uploaded as a multipart/form-data request.
func WithAddFileWithType(formName, filepath, filetype string) RequestOpt {
return func(opt *RequestOpts) error {
stat, err := os.Stat(filepath)
if err != nil {
return err
}
opt.bodyFileData = append(opt.bodyFileData, RequestFile{
FormName: formName,
FileName: stat.Name(),
FileData: nil,
FileSize: stat.Size(),
FileType: filetype,
})
return nil
}
}
// WithAddFileWithNameAndType adds a file to the request with the specified form name, file path, filename, and file type.
// you can specify a custom filename and file type for the file being uploaded.
// The priority order of the data is: bodyDataReader > bodyDataBytes > bodyFormData > **bodyFileData**.
// The file will be read and uploaded as a multipart/form-data request.
func WithAddFileWithNameAndType(formName, filepath, filename, filetype string) RequestOpt {
return func(opt *RequestOpts) error {
stat, err := os.Stat(filepath)
if err != nil {
return err
}
opt.bodyFileData = append(opt.bodyFileData, RequestFile{
FormName: formName,
FileName: filename,
FileData: nil,
FileSize: stat.Size(),
FileType: filetype,
})
return nil
}
}
// WithFetchRespBody sets whether the response body will be automatically fetched after the request is sent.
// If set to true, the response body will be read and stored in the Response object.
// If the body is too large, it may cause high memory usage.
// If set to false, you will need to manually read the response body using Response.Body() method.
func WithFetchRespBody(fetch bool) RequestOpt {
return func(opt *RequestOpts) error {
opt.autoFetchRespBody = fetch
return nil
}
}
// WithCookies sets the request cookies using a slice of http.Cookie.
func WithCookies(ck []*http.Cookie) RequestOpt {
return func(opt *RequestOpts) error {
opt.cookies = ck
return nil
}
}
// WithCookie adds a cookie to the request with the specified key, value, and path.
func WithCookie(key, val, path string) RequestOpt {
return func(opt *RequestOpts) error {
opt.cookies = append(opt.cookies, &http.Cookie{Name: key, Value: val, Path: path})
return nil
}
}
// WithSimpleCookie adds a simple cookie to the request with the specified key and value.
func WithSimpleCookie(key, val string) RequestOpt {
return func(opt *RequestOpts) error {
opt.cookies = append(opt.cookies, &http.Cookie{Name: key, Value: val, Path: "/"})
return nil
}
}
// WithCookieMap sets the request cookies using a map of string to string.
func WithCookieMap(header map[string]string, path string) RequestOpt {
return func(opt *RequestOpts) error {
for key, val := range header {
opt.cookies = append(opt.cookies, &http.Cookie{Name: key, Value: val, Path: path})
}
return nil
}
}
// WithQueries sets the request queries using a map of string slices.
func WithQueries(queries map[string][]string) RequestOpt {
return func(opt *RequestOpts) error {
opt.queries = queries
return nil
}
}
// WithAddQueries adds multiple query parameters to the request using a map of string to string slices.
// if the key already exists, it will append the value to the existing slice.
func WithAddQueries(queries map[string]string) RequestOpt {
return func(opt *RequestOpts) error {
for k, v := range queries {
opt.queries[k] = append(opt.queries[k], v)
}
return nil
}
}
// WithAddQuery adds a single query parameter to the request.
func WithAddQuery(key, val string) RequestOpt {
return func(opt *RequestOpts) error {
opt.queries[key] = append(opt.queries[key], val)
return nil
}
}
// WithProxy sets the proxy URL for the request.
func WithProxy(proxy string) RequestOpt {
return func(opt *RequestOpts) error {
opt.proxy = proxy
return nil
}
}
// WithProcess sets a callback function to process the file upload progress.
// The callback function will be called with the file name, uploaded bytes and total bytes.
// example:
//
// WithProcess(func(name string, uploaded int64, total int64) {
// fmt.Printf("Uploading %s: %d/%d bytes\n", name, uploaded, total)
// })
func WithProcess(fn func(string, int64, int64)) RequestOpt {
return func(opt *RequestOpts) error {
opt.fileUploadRecallFn = fn
return nil
}
}
// WithContentType sets the Content-Type header for the request.
// This function will overwrite any existing Content-Type header.
func WithContentType(ct string) RequestOpt {
return func(opt *RequestOpts) error {
opt.headers.Set("Content-Type", ct)
return nil
}
}
// WithUserAgent sets the User-Agent header for the request.
// This function will overwrite any existing User-Agent header.
func WithUserAgent(ua string) RequestOpt {
return func(opt *RequestOpts) error {
opt.headers.Set("User-Agent", ua)
return nil
}
}
// WithSkipTLSVerify sets whether the request will skip TLS verification.
// If set to true, the request will not verify the server's TLS certificate.
func WithSkipTLSVerify(skip bool) RequestOpt {
return func(opt *RequestOpts) error {
opt.skipTLSVerify = skip
return nil
}
}
/*
// WithDisableRedirect sets whether the request will disable HTTP redirects.
// If set to true, the request will not follow redirects automatically.
// For example, if the server responds with a 301 or 302 status code, the request will not automatically follow the redirect.
// You will get the original response with the redirect status code and Location header.
func WithDisableRedirect(disable bool) RequestOpt {
return func(opt *RequestOpts) error {
opt.disableRedirect = disable
return nil
}
}
*/
// WithDoRawRequest sets whether the request will be sent as a raw request without any modifications.
// You can use this with function SetRawRequest to set a custom http.Request.
// If set to true, the request will not apply any modifications to the request headers, body, or other settings.
// If set to false, the request will apply the modifications as usual.
func WithDoRawRequest(doRawRequest bool) RequestOpt {
return func(opt *RequestOpts) error {
opt.doRawRequest = doRawRequest
return nil
}
}
// WithTransport sets the http.Transport used for the request.
func WithTransport(hs *http.Transport) RequestOpt {
return func(opt *RequestOpts) error {
opt.transport = hs
opt.customTransport = true
return nil
}
}
// WithRawRequest sets a custom http.Request for the request.
func WithRawRequest(req *http.Request) RequestOpt {
return func(opt *RequestOpts) error {
opt.rawRequest = req
return nil
}
}
// WithRawClient sets a custom http.Client for the request.
func WithRawClient(hc *http.Client) RequestOpt {
return func(opt *RequestOpts) error {
opt.rawClient = hc
return nil
}
}
// WithCustomHostIP sets custom IPs for the host.
// it means that the request will use the specified IPs to resolve the host instead of using DNS.
// Note: LookUpIPfn will be ignored if this function is used.
func WithCustomHostIP(ip []string) RequestOpt {
return func(opt *RequestOpts) error {
if len(ip) == 0 {
return nil
}
for _, v := range ip {
if net.ParseIP(v) == nil {
return fmt.Errorf("invalid custom ip: %s", v)
}
}
opt.customIP = ip
return nil
}
}
// WithAddCustomHostIP adds a custom IP to the request.
func WithAddCustomHostIP(ip string) RequestOpt {
return func(opt *RequestOpts) error {
if net.ParseIP(ip) == nil {
return fmt.Errorf("invalid custom ip: %s", ip)
}
opt.customIP = append(opt.customIP, ip)
return nil
}
}
// WithLookUpFn sets a custom function to look up IP addresses for the host.
// If set to nil, it will use the default net.Resolver.LookupIPAddr function.
// Note: If customDNS is set, this function will not be used.
func WithLookUpFn(lookUpIPfn func(ctx context.Context, host string) ([]net.IPAddr, error)) RequestOpt {
return func(opt *RequestOpts) error {
if lookUpIPfn == nil {
opt.alreadySetLookUpIPfn = false
opt.lookUpIPfn = net.DefaultResolver.LookupIPAddr
return nil
}
opt.lookUpIPfn = lookUpIPfn
opt.alreadySetLookUpIPfn = true
return nil
}
}
// WithCustomDNS will use custom dns to resolve the host
// Note: if LookUpIPfn is set, this function will not be used
func WithCustomDNS(customDNS []string) RequestOpt {
return func(opt *RequestOpts) error {
for _, v := range customDNS {
if net.ParseIP(v) == nil {
return fmt.Errorf("invalid custom dns: %s", v)
}
}
opt.customDNS = customDNS
return nil
}
}
// WithAddCustomDNS will use a custom dns to resolve the host
// Note: if LookUpIPfn is set, this function will not be used
func WithAddCustomDNS(customDNS string) RequestOpt {
return func(opt *RequestOpts) error {
if net.ParseIP(customDNS) == nil {
return fmt.Errorf("invalid custom dns: %s", customDNS)
}
opt.customDNS = append(opt.customDNS, customDNS)
return nil
}
}
// WithAutoCalcContentLength sets whether to automatically calculate the Content-Length header based on the request body.
// WARN: If set to true, the Content-Length header will be set to the length of the request body, which may cause issues with chunked transfer encoding.
// also the memory usage will be higher
// Note that this function will not work if doRawRequest is true
func (r *RequestOpts) WithAutoCalcContentLength(autoCalcContentLength bool) RequestOpt {
return func(opt *RequestOpts) error {
r.autoCalcContentLength = autoCalcContentLength
return nil
}
}
type Response struct {
*http.Response
req Request
data *Body
rawClient *http.Client
}
type Body struct {
full []byte
raw io.ReadCloser
isFull bool
sync.Mutex
}
func (b *Body) readAll() {
b.Lock()
defer b.Unlock()
if !b.isFull {
if b.raw == nil {
b.isFull = true
return
}
b.full, _ = io.ReadAll(b.raw)
b.isFull = true
b.raw.Close()
}
}
// String will read the body and return it as a string.
// if the body is too large, it may cause high memory usage.
func (b *Body) String() string {
b.readAll()
return string(b.full)
}
// Bytes will read the body and return it as a byte slice.
// if the body is too large, it may cause high memory usage.
func (b *Body) Bytes() []byte {
b.readAll()
return b.full
}
// Unmarshal will read the body and unmarshal it into the given interface using json.Unmarshal
// if the body is too large, it may cause high memory usage.
func (b *Body) Unmarshal(u interface{}) error {
b.readAll()
return json.Unmarshal(b.full, u)
}
// Reader returns a reader for the body
// if this function is called, other functions like String, Bytes, Unmarshal not work
func (b *Body) Reader() io.ReadCloser {
b.Lock()
defer b.Unlock()
if b.isFull {
return io.NopCloser(bytes.NewReader(b.full))
}
b.isFull = true
return b.raw
}
// Close closes the body reader.
func (b *Body) Close() error {
return b.raw.Close()
}
// GetRequest returns the original Request object associated with the Response.
func (r *Response) GetRequest() Request {
return r.req
}
// Body returns the Body object associated with the Response.
func (r *Response) Body() *Body {
return r.data
}
// Close closes the response body and releases any resources associated with it.
func (r *Response) Close() error {
if r != nil && r.data != nil && r.data.raw != nil {
return r.Response.Body.Close()
}
return nil
}
// CloseAll closes the response body and releases any resources associated with it.
// It also closes all idle connections in the http.Client if it is not nil.
func (r *Response) CloseAll() error {
if r.rawClient != nil {
r.rawClient.CloseIdleConnections()
}
return r.Close()
}
// HttpClient returns the http.Client used for the request.
func (r *Response) HttpClient() *http.Client {
return r.rawClient
}
// Curl sends the HTTP request and returns the response.
func Curl(r *Request) (*Response, error) {
r.errInfo = nil
err := applyOptions(r)
if err != nil {
return nil, fmt.Errorf("apply options error: %s", err)
}
r.rawRequest = r.rawRequest.WithContext(r.doCtx)
resp, err := r.rawClient.Do(r.rawRequest)
var res = Response{
Response: resp,
req: *r,
data: new(Body),
rawClient: r.rawClient,
}
if err != nil {
res.Response = &http.Response{}
return &res, fmt.Errorf("do request error: %s", err)
}
res.data.raw = resp.Body
if r.autoFetchRespBody {
res.data.full, _ = io.ReadAll(resp.Body)
res.data.isFull = true
resp.Body.Close()
}
return &res, r.errInfo
}
// NewReq creates a new Request with the specified URI and default method "GET".
func NewReq(uri string, opts ...RequestOpt) *Request {
return NewSimpleRequest(uri, "GET", opts...)
}
// NewReqWithContext creates a new Request with the specified URI and default method "GET" using the provided context.
func NewReqWithContext(ctx context.Context, uri string, opts ...RequestOpt) *Request {
return NewSimpleRequestWithContext(ctx, uri, "GET", opts...)
}
// NewSimpleRequest creates a new Request with the specified URI and method.
func NewSimpleRequest(uri string, method string, opts ...RequestOpt) *Request {
r, _ := newRequest(context.Background(), uri, method, opts...)
return r
}
// NewRequest creates a new Request with the specified URI and method.
func NewRequest(uri string, method string, opts ...RequestOpt) (*Request, error) {
return newRequest(context.Background(), uri, method, opts...)
}
// NewSimpleRequestWithContext creates a new Request with the specified URI and method using the provided context.
func NewSimpleRequestWithContext(ctx context.Context, uri string, method string, opts ...RequestOpt) *Request {
r, _ := newRequest(ctx, uri, method, opts...)
return r
}
// NewRequestWithContext creates a new Request with the specified URI and method using the provided context.
func NewRequestWithContext(ctx context.Context, uri string, method string, opts ...RequestOpt) (*Request, error) {
return newRequest(ctx, uri, method, opts...)
}
func newRequest(ctx context.Context, uri string, method string, opts ...RequestOpt) (*Request, error) {
var req *http.Request
var err error
if method == "" {
method = "GET"
}
method = strings.ToUpper(method)
req, err = http.NewRequestWithContext(ctx, method, uri, nil)
if err != nil {
return nil, err
}
var r = &Request{
ctx: ctx,
uri: uri,
method: method,
RequestOpts: RequestOpts{
rawRequest: req,
rawClient: nil,
timeout: DefaultTimeout,
dialTimeout: DefaultDialTimeout,
autoFetchRespBody: DefaultFetchRespBody,
lookUpIPfn: net.DefaultResolver.LookupIPAddr,
bodyFormData: make(map[string][]string),
queries: make(map[string][]string),
},
}
r.headers = make(http.Header)
if strings.ToUpper(method) == "POST" {
r.headers.Set("Content-Type", HEADER_FORM_URLENCODE)
}
r.headers.Set("User-Agent", "B612 / 1.2.0")
for _, v := range opts {
if v != nil {
err = v(&r.RequestOpts)
if err != nil {
return nil, err
}
}
}
if r.rawClient == nil {
r.rawClient = new(http.Client)
}
if r.tlsConfig == nil {
r.tlsConfig = &tls.Config{
NextProtos: []string{"h2", "http/1.1"},
}
}
r.tlsConfig.InsecureSkipVerify = r.skipTLSVerify
if r.transport == nil {
r.transport = &http.Transport{
ForceAttemptHTTP2: true,
DialContext: DefaultDialFunc,
DialTLSContext: DefaultDialTlsFunc,
Proxy: DefaultProxyURL(),
}
}
return r, nil
}
func applyDataReader(r *Request) error {
// 优先度为bodyDataReader > bodyDataBytes > bodyFormData > bodyFileData
if r.bodyDataReader != nil {
r.rawRequest.Body = io.NopCloser(r.bodyDataReader)
return nil
}
if len(r.bodyDataBytes) != 0 {
r.rawRequest.Body = io.NopCloser(bytes.NewReader(r.bodyDataBytes))
return nil
}
if len(r.bodyFormData) != 0 && len(r.bodyFileData) == 0 {
var body = url.Values{}
for k, v := range r.bodyFormData {
for _, vv := range v {
body.Add(k, vv)
}
}
r.rawRequest.Body = io.NopCloser(strings.NewReader(body.Encode()))
return nil
}
if len(r.bodyFileData) != 0 {
var pr, pw = io.Pipe()
var w = multipart.NewWriter(pw)
r.rawRequest.Header.Set("Content-Type", w.FormDataContentType())
go func() {
defer pw.Close() // ensure pipe writer is closed
if len(r.bodyFormData) != 0 {
for k, v := range r.bodyFormData {
for _, vv := range v {
if err := w.WriteField(k, vv); err != nil {
r.errInfo = err
pw.CloseWithError(err) // close pipe with error
return
}
}
}
}
for idx, v := range r.bodyFileData {
var fw, err = w.CreateFormFile(v.FormName, v.FileName)
if err != nil {
r.errInfo = err
pw.CloseWithError(err) // close pipe with error
return
}
if v.FileData == nil {
if v.FilePath != "" {
tmpFile, err := os.Open(v.FilePath)
if err != nil {
r.errInfo = err
pw.CloseWithError(err) // close pipe with error
return
}
defer tmpFile.Close()
v.FileData = tmpFile
} else {
r.errInfo = fmt.Errorf("io reader is nil")
pw.CloseWithError(fmt.Errorf("io reader is nil")) // close pipe with error
return
}
}
if _, err := copyWithContext(r.ctx, r.fileUploadRecallFn, v.FileName, v.FileSize, fw, v.FileData); err != nil {
r.errInfo = err
r.bodyFileData[idx] = v
pw.CloseWithError(err) // close pipe with error
return
}
}
if err := w.Close(); err != nil {
pw.CloseWithError(err) // close pipe with error if writer close fails
}
}()
r.rawRequest.Body = pr
return nil
}
return nil
}
func applyOptions(r *Request) error {
defer func() {
r.alreadyApply = true
}()
var req = r.rawRequest
if !r.doRawRequest {
if r.queries != nil {
sid := req.URL.Query()
for k, v := range r.queries {
for _, vv := range v {
sid.Add(k, vv)
}
}
req.URL.RawQuery = sid.Encode()
}
for k, v := range r.headers {
for _, vv := range v {
req.Header.Add(k, vv)
}
}
if len(r.cookies) != 0 {
for _, v := range r.cookies {
req.AddCookie(v)
}
}
if r.basicAuth[0] != "" || r.basicAuth[1] != "" {
req.SetBasicAuth(r.basicAuth[0], r.basicAuth[1])
}
err := applyDataReader(r)
if err != nil {
return fmt.Errorf("apply data reader error: %s", err)
}
if r.autoCalcContentLength {
if req.Body != nil {
data, err := io.ReadAll(req.Body)
if err != nil {
return fmt.Errorf("read data error: %s", err)
}
req.Header.Set("Content-Length", strconv.Itoa(len(data)))
req.Body = io.NopCloser(bytes.NewReader(data))
}
}
}
if !r.alreadySetLookUpIPfn && len(r.customDNS) > 0 {
resolver := net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, address string) (conn net.Conn, err error) {
for _, addr := range r.customDNS {
if conn, err = net.Dial("udp", addr+":53"); err != nil {
continue
} else {
return conn, nil
}
}
return
},
}
r.lookUpIPfn = resolver.LookupIPAddr
}
if r.tlsConfig == nil {
r.tlsConfig = &tls.Config{
NextProtos: []string{"h2", "http/1.1"},
}
}
if r.tlsConfig.ServerName == "" {
r.tlsConfig.ServerName = r.rawRequest.URL.Hostname()
}
r.tlsConfig.InsecureSkipVerify = r.skipTLSVerify
if r.rawClient.Transport == nil {
r.rawClient.Transport = &Transport{base: r.transport}
}
r.doCtx = context.WithValue(context.WithValue(r.ctx, "dialTimeout", r.dialTimeout), "timeout", r.timeout)
r.doCtx = context.WithValue(r.doCtx, "lookUpIP", r.lookUpIPfn)
if r.customIP != nil && len(r.customIP) > 0 {
r.doCtx = context.WithValue(r.doCtx, "customIP", r.customIP)
}
r.doCtx = context.WithValue(r.doCtx, "tlsConfig", r.tlsConfig)
if r.proxy != "" {
r.doCtx = context.WithValue(r.doCtx, "proxy", r.proxy)
}
if r.dialFn != nil {
r.doCtx = context.WithValue(r.doCtx, "dialFn", r.dialFn)
}
if r.customTransport {
r.doCtx = context.WithValue(r.doCtx, "custom", r.transport)
}
return nil
}
func copyWithContext(ctx context.Context, recall func(string, int64, int64), filename string, total int64, dst io.Writer, src io.Reader) (written int64, err error) {
pr, pw := io.Pipe()
defer pr.Close()
go func() {
defer pw.Close()
_, err := io.Copy(pw, src)
if err != nil {
pw.CloseWithError(err)
}
}()
var count int64
buf := make([]byte, 4096)
for {
select {
case <-ctx.Done():
return written, ctx.Err()
default:
nr, err := pr.Read(buf)
if err != nil {
if err == io.EOF {
if recall != nil {
go recall(filename, count, total)
}
return written, nil
}
return written, err
}
count += int64(nr)
if recall != nil {
go recall(filename, count, total)
}
nw, err := dst.Write(buf[:nr])
if err != nil {
return written, err
}
if nr != nw {
return written, io.ErrShortWrite
}
written += int64(nr)
}
}
}
func NewReqWithClient(client Client, uri string, opts ...RequestOpt) *Request {
return NewSimpleRequestWithClient(client, uri, "GET", opts...)
}
func NewReqWithContextWithClient(ctx context.Context, client Client, uri string, opts ...RequestOpt) *Request {
return NewSimpleRequestWithContextWithClient(ctx, client, uri, "GET", opts...)
}
func NewSimpleRequestWithClient(client Client, uri string, method string, opts ...RequestOpt) *Request {
r, _ := NewRequestWithContextWithClient(context.Background(), client, uri, method, opts...)
return r
}
func NewRequestWithClient(client Client, uri string, method string, opts ...RequestOpt) (*Request, error) {
return NewRequestWithContextWithClient(context.Background(), client, uri, method, opts...)
}
func NewSimpleRequestWithContextWithClient(ctx context.Context, client Client, uri string, method string, opts ...RequestOpt) *Request {
r, _ := NewRequestWithContextWithClient(ctx, client, uri, method, opts...)
return r
}
func NewRequestWithContextWithClient(ctx context.Context, client Client, uri string, method string, opts ...RequestOpt) (*Request, error) {
req, err := newRequest(ctx, uri, method, opts...)
if err != nil {
return nil, err
}
req.rawClient = client.Client
return req, err
}