Repository: go-resty/resty
Pull request #839 (2024-09-01 04:21:05)

Bring updates from branch v2 to branch main (v3)


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 224dc3d..2449a72 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -6,12 +6,16 @@ on:
       - v2
     paths-ignore:
       - '**.md'
+      - '**.bazel'
+      - 'WORKSPACE'
   pull_request:
     branches:
       - main
       - v2
     paths-ignore:
       - '**.md'
+      - '**.bazel'
+      - 'WORKSPACE'
 
   # Allows you to run this workflow manually from the Actions tab
   workflow_dispatch:
@@ -21,7 +25,7 @@ jobs:
     name: Build
     strategy:
       matrix:
-        go: [ '1.21.x']
+        go: [ 'stable', '1.19.x' ]
         os: [ ubuntu-latest ]
 
     runs-on: ${{ matrix.os }}
@@ -43,7 +47,7 @@ jobs:
         run: diff -u <(echo -n) <(go fmt $(go list ./...))
 
       - name: Test
-        run: go test ./... -race -coverprofile=coverage.txt -covermode=atomic
+        run: go test ./... -race -coverprofile=coverage.txt -covermode=atomic -coverpkg=./...
 
       - name: Upload coverage to Codecov
         uses: codecov/codecov-action@v4
diff --git a/.github/workflows/label-actions.yml b/.github/workflows/label-actions.yml
index 5c4c873..a3ab38f 100644
--- a/.github/workflows/label-actions.yml
+++ b/.github/workflows/label-actions.yml
@@ -3,12 +3,16 @@ name: 'Label'
 on:
   pull_request:
     types: [labeled]
+    paths-ignore:
+      - '**.md'
+      - '**.bazel'
+      - 'WORKSPACE'
 
 jobs:
   build:
     strategy:
       matrix:
-        go: [ '1.21.x']
+        go: [ 'stable', '1.19.x' ]
         os: [ ubuntu-latest ]
 
     name: Run Build
@@ -28,8 +32,13 @@ jobs:
           cache: true
           cache-dependency-path: go.sum
 
+      - name: Format
+        run: diff -u <(echo -n) <(go fmt $(go list ./...))
+
       - name: Test
-        run: go test ./... -race -coverprofile=coverage.txt -covermode=atomic
+        run: go test ./... -race -coverprofile=coverage.txt -covermode=atomic -coverpkg=./...
 
-      - name: Coverage
-        run: bash <(curl -s https://codecov.io/bash)
+      - name: Upload coverage to Codecov
+        uses: codecov/codecov-action@v4
+        env:
+          CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
diff --git a/.gitignore b/.gitignore
index 9e856bd..7542ac8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,5 +26,6 @@ _testmain.go
 coverage.out
 coverage.txt
 
-# Exclude intellij IDE folders
+# Exclude IDE folders
 .idea/*
+.vscode/*
diff --git a/BUILD.bazel b/BUILD.bazel
index 6e73016..7248abc 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -1,5 +1,5 @@
-load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
 load("@bazel_gazelle//:def.bzl", "gazelle")
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
 
 # gazelle:prefix github.com/go-resty/resty/v2
 # gazelle:go_naming_convention import_alias
@@ -17,13 +17,19 @@ go_library(
         "resty.go",
         "retry.go",
         "trace.go",
+        "transport.go",
+        "transport112.go",
         "transport_js.go",
         "transport_other.go",
         "util.go",
+        "util_curl.go",
     ],
     importpath = "github.com/go-resty/resty/v2",
     visibility = ["//visibility:public"],
-    deps = ["@org_golang_x_net//publicsuffix:go_default_library"],
+    deps = [
+        "//shellescape",
+        "@org_golang_x_net//publicsuffix:go_default_library",
+    ],
 )
 
 go_test(
@@ -32,6 +38,7 @@ go_test(
         "client_test.go",
         "context_test.go",
         "example_test.go",
+        "middleware_test.go",
         "request_test.go",
         "resty_test.go",
         "retry_test.go",
@@ -39,7 +46,10 @@ go_test(
     ],
     data = glob([".testdata/*"]),
     embed = [":resty"],
-    deps = ["@org_golang_x_net//proxy:go_default_library"],
+    deps = [
+        "@org_golang_x_net//proxy:go_default_library",
+        "@org_golang_x_time//rate:go_default_library",
+    ],
 )
 
 alias(
diff --git a/LICENSE b/LICENSE
index 0c2d38a..de30fea 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2015-2023 Jeevanandam M., https://myjeeva.com <jeeva@myjeeva.com>
+Copyright (c) 2015-2024 Jeevanandam M., https://myjeeva.com <jeeva@myjeeva.com>
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index ef6ed1d..c0f387e 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
 <p align="center"><a href="#features">Features</a> section describes in detail about Resty capabilities</p>
 </p>
 <p align="center">
-<p align="center"><a href="https://github.com/go-resty/resty/actions/workflows/ci.yml?query=branch%3Amaster"><img src="https://github.com/go-resty/resty/actions/workflows/ci.yml/badge.svg" alt="Build Status"></a> <a href="https://codecov.io/gh/go-resty/resty/branch/master"><img src="https://codecov.io/gh/go-resty/resty/branch/master/graph/badge.svg" alt="Code Coverage"></a> <a href="https://goreportcard.com/report/go-resty/resty"><img src="https://goreportcard.com/badge/go-resty/resty" alt="Go Report Card"></a> <a href="https://github.com/go-resty/resty/releases/latest"><img src="https://img.shields.io/badge/version-2.13.0-blue.svg" alt="Release Version"></a> <a href="https://pkg.go.dev/github.com/go-resty/resty/v2"><img src="https://pkg.go.dev/badge/github.com/go-resty/resty" alt="GoDoc"></a> <a href="LICENSE"><img src="https://img.shields.io/github/license/go-resty/resty.svg" alt="License"></a> <a href="https://github.com/avelino/awesome-go"><img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome Go"></a></p>
+<p align="center"><a href="https://github.com/go-resty/resty/actions/workflows/ci.yml?query=branch%3Amaster"><img src="https://github.com/go-resty/resty/actions/workflows/ci.yml/badge.svg" alt="Build Status"></a> <a href="https://codecov.io/gh/go-resty/resty/branch/master"><img src="https://codecov.io/gh/go-resty/resty/branch/master/graph/badge.svg" alt="Code Coverage"></a> <a href="https://goreportcard.com/report/go-resty/resty"><img src="https://goreportcard.com/badge/go-resty/resty" alt="Go Report Card"></a> <a href="https://github.com/go-resty/resty/releases/latest"><img src="https://img.shields.io/badge/version-2.14.0-blue.svg" alt="Release Version"></a> <a href="https://pkg.go.dev/github.com/go-resty/resty/v2"><img src="https://pkg.go.dev/badge/github.com/go-resty/resty" alt="GoDoc"></a> <a href="LICENSE"><img src="https://img.shields.io/github/license/go-resty/resty.svg" alt="License"></a> <a href="https://github.com/avelino/awesome-go"><img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome Go"></a></p>
 </p>
 <p align="center">
 <h4 align="center">Resty Communication Channels</h4>
@@ -13,7 +13,7 @@
 
 ## News
 
-  * v2.13.0 [released](https://github.com/go-resty/resty/releases/tag/v2.13.0) and tagged on May 08, 2024.
+  * v2.14.0 [released](https://github.com/go-resty/resty/releases/tag/v2.14.0) and tagged on Aug 04, 2024.
   * v2.0.0 [released](https://github.com/go-resty/resty/releases/tag/v2.0.0) and tagged on Jul 16, 2019.
   * v1.12.0 [released](https://github.com/go-resty/resty/releases/tag/v1.12.0) and tagged on Feb 27, 2019.
   * v1.0 released and tagged on Sep 25, 2017. - Resty's first version was released on Sep 15, 2015 then it grew gradually as a very handy and helpful library. Its been a two years since first release. I'm very thankful to Resty users and its [contributors](https://github.com/go-resty/resty/graphs/contributors).
@@ -62,6 +62,7 @@
     * goroutine concurrent safe
     * Resty Client trace, see [Client.EnableTrace](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.EnableTrace) and [Request.EnableTrace](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.EnableTrace)
       * Since v2.4.0, trace info contains a `RequestAttempt` value, and the `Request` object contains an `Attempt` attribute
+    * Supports `GenerateCurlCommand`(**You should turn on `EnableTrace`**, otherwise the curl command will not contain the body) 
     * Debug mode - clean and informative logging presentation
     * Gzip - Go does it automatically also resty has fallback handling too
     * Works fine with `HTTP/2` and `HTTP/1.1`
@@ -122,15 +123,21 @@ The following samples will assist you to become as comfortable as possible with
 import "github.com/go-resty/resty/v2"
 ```
 
-#### Simple GET
+#### Simple POST
+>Refer: [debug_curl_test.go](https://github.com/go-resty/resty/blob/v2/examples/debug_curl_test.go)
 
 ```go
 // Create a Resty Client
 client := resty.New()
 
 resp, err := client.R().
-    EnableTrace().
-    Get("https://httpbin.org/get")
+    EnableTrace(). // You should turn on `EnableTrace`, otherwise the curl command will not contain the body
+    SetBody(map[string]string{"name": "Alex"}).
+    Post("https://httpbin.org/post")
+curlCmdExecuted := resp.Request.GenerateCurlCommand()
+
+// Explore curl command
+fmt.Println("Curl Command:\n  ", curlCmdExecuted+"\n")
 
 // Explore response object
 fmt.Println("Response Info:")
@@ -160,25 +167,36 @@ fmt.Println("  RequestAttempt:", ti.RequestAttempt)
 fmt.Println("  RemoteAddr    :", ti.RemoteAddr.String())
 
 /* Output
+Curl Command:
+   curl -X POST -H 'Content-Type: application/json' -H 'User-Agent: go-resty/2.14.0 (https://github.com/go-resty/resty)' -d '{"name":"Alex"}' https://httpbin.org/post
+
 Response Info:
   Error      : <nil>
   Status Code: 200
   Status     : 200 OK
   Proto      : HTTP/2.0
   Time       : 457.034718ms
-  Received At: 2020-09-14 15:35:29.784681 -0700 PDT m=+0.458137045
+  Received At: 2024-08-09 13:02:57.187544 +0800 CST m=+1.304888501
   Body       :
-  {
-    "args": {},
-    "headers": {
-      "Accept-Encoding": "gzip",
-      "Host": "httpbin.org",
-      "User-Agent": "go-resty/2.4.0 (https://github.com/go-resty/resty)",
-      "X-Amzn-Trace-Id": "Root=1-5f5ff031-000ff6292204aa6898e4de49"
-    },
-    "origin": "0.0.0.0",
-    "url": "https://httpbin.org/get"
-  }
+ {
+  "args": {}, 
+  "data": "{\"name\":\"Alex\"}", 
+  "files": {}, 
+  "form": {}, 
+  "headers": {
+    "Accept-Encoding": "gzip", 
+    "Content-Length": "15", 
+    "Content-Type": "application/json", 
+    "Host": "httpbin.org", 
+    "User-Agent": "go-resty/2.14.0 (https://github.com/go-resty/resty)", 
+    "X-Amzn-Trace-Id": "Root=1-66b5a301-567c83c86562abd3092f5e19"
+  }, 
+  "json": {
+    "name": "Alex"
+  }, 
+  "origin": "0.0.0.0",
+  "url": "https://httpbin.org/post"
+}
 
 Request Trace Info:
   DNSLookup     : 4.074657ms
diff --git a/WORKSPACE b/WORKSPACE
index 9ef03e9..504de14 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -4,10 +4,10 @@ load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
 
 http_archive(
     name = "io_bazel_rules_go",
-    sha256 = "69de5c704a05ff37862f7e0f5534d4f479418afc21806c887db544a316f3cb6b",
+    sha256 = "80a98277ad1311dacd837f9b16db62887702e9f1d1c4c9f796d0121a46c8e184",
     urls = [
-        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz",
-        "https://github.com/bazelbuild/rules_go/releases/download/v0.27.0/rules_go-v0.27.0.tar.gz",
+        "https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.46.0/rules_go-v0.46.0.zip",
+        "https://github.com/bazelbuild/rules_go/releases/download/v0.46.0/rules_go-v0.46.0.zip",
     ],
 )
 
@@ -24,7 +24,7 @@ load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_depe
 
 go_rules_dependencies()
 
-go_register_toolchains(version = "1.16")
+go_register_toolchains(version = "1.19")
 
 load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
 
diff --git a/client.go b/client.go
index a55fb6b..d33d629 100644
--- a/client.go
+++ b/client.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
 // resty source code and usage is governed by a MIT style
 // license that can be found in the LICENSE file.
 
@@ -9,9 +9,12 @@ import (
 	"compress/gzip"
 	"crypto/tls"
 	"crypto/x509"
+	"encoding/json"
+	"encoding/xml"
 	"errors"
 	"fmt"
 	"io"
+	"math"
 	"net/http"
 	"net/url"
 	"os"
@@ -89,60 +92,6 @@ type (
 	SuccessHook func(*Client, *Response)
 )
 
-// TransportSettings struct is used to define custom dialer and transport
-// values for the Resty client. Please refer to individual
-// struct fields to know the default values.
-//
-// Also, refer to https://pkg.go.dev/net/http#Transport for more details.
-type TransportSettings struct {
-	// DialerTimeout, default value is `30` seconds.
-	DialerTimeout time.Duration
-
-	// DialerKeepAlive, default value is `30` seconds.
-	DialerKeepAlive time.Duration
-
-	// IdleConnTimeout, default value is `90` seconds.
-	IdleConnTimeout time.Duration
-
-	// TLSHandshakeTimeout, default value is `10` seconds.
-	TLSHandshakeTimeout time.Duration
-
-	// ExpectContinueTimeout, default value is `1` seconds.
-	ExpectContinueTimeout time.Duration
-
-	// ResponseHeaderTimeout, added to provide ability to
-	// set value. No default value in Resty, the Go
-	// HTTP client default value applies.
-	ResponseHeaderTimeout time.Duration
-
-	// MaxIdleConns, default value is `100`.
-	MaxIdleConns int
-
-	// MaxIdleConnsPerHost, default value is `runtime.GOMAXPROCS(0) + 1`.
-	MaxIdleConnsPerHost int
-
-	// DisableKeepAlives, default value is `false`.
-	DisableKeepAlives bool
-
-	// DisableCompression, default value is `false`.
-	DisableCompression bool
-
-	// MaxResponseHeaderBytes, added to provide ability to
-	// set value. No default value in Resty, the Go
-	// HTTP client default value applies.
-	MaxResponseHeaderBytes int64
-
-	// WriteBufferSize, added to provide ability to
-	// set value. No default value in Resty, the Go
-	// HTTP client default value applies.
-	WriteBufferSize int
-
-	// ReadBufferSize, added to provide ability to
-	// set value. No default value in Resty, the Go
-	// HTTP client default value applies.
-	ReadBufferSize int
-}
-
 // Client struct is used to create Resty client with client level settings,
 // these settings are applicable to all the request raised from the client.
 //
@@ -150,6 +99,7 @@ type TransportSettings struct {
 // at request level.
 type Client struct {
 	BaseURL               string
+	HostURL               string // Deprecated: use BaseURL instead. To be removed in v3.0.0 release.
 	QueryParam            url.Values
 	FormData              url.Values
 	PathParams            map[string]string
@@ -178,6 +128,7 @@ type Client struct {
 	// HeaderAuthorizationKey is used to set/access Request Authorization header
 	// value when `SetAuthToken` option is used.
 	HeaderAuthorizationKey string
+	ResponseBodyLimit      int
 
 	jsonEscapeHTML      bool
 	setContentLength    bool
@@ -214,6 +165,21 @@ type User struct {
 // Client methods
 //___________________________________
 
+// SetHostURL method is to set Host URL in the client instance. It will be used with request
+// raised from this client with relative URL
+//
+//	// Setting HTTP address
+//	client.SetHostURL("http://myjeeva.com")
+//
+//	// Setting HTTPS address
+//	client.SetHostURL("https://myjeeva.com")
+//
+// Deprecated: use SetBaseURL instead. To be removed in v3.0.0 release.
+func (c *Client) SetHostURL(url string) *Client {
+	c.SetBaseURL(url)
+	return c
+}
+
 // SetBaseURL method is to set Base URL in the client instance. It will be used with request
 // raised from this client with relative URL
 //
@@ -226,6 +192,7 @@ type User struct {
 // Since v2.7.0
 func (c *Client) SetBaseURL(url string) *Client {
 	c.BaseURL = strings.TrimRight(url, "/")
+	c.HostURL = c.BaseURL
 	return c
 }
 
@@ -476,11 +443,12 @@ func (c *Client) R() *Request {
 		RawPathParams: map[string]string{},
 		Debug:         c.Debug,
 
-		client:          c,
-		multipartFiles:  []*File{},
-		multipartFields: []*MultipartField{},
-		jsonEscapeHTML:  c.jsonEscapeHTML,
-		log:             c.log,
+		client:            c,
+		multipartFiles:    []*File{},
+		multipartFields:   []*MultipartField{},
+		jsonEscapeHTML:    c.jsonEscapeHTML,
+		log:               c.log,
+		responseBodyLimit: c.ResponseBodyLimit,
 	}
 	return r
 }
@@ -1123,6 +1091,20 @@ func (c *Client) SetJSONEscapeHTML(b bool) *Client {
 	return c
 }
 
+// SetResponseBodyLimit set a max body size limit on response, avoid reading too many data to memory.
+//
+// Client will return [resty.ErrResponseBodyTooLarge] if uncompressed response body size if larger than limit.
+// Body size limit will not be enforced in following case:
+//   - ResponseBodyLimit <= 0, which is the default behavior.
+//   - [Request.SetOutput] is called to save a response data to file.
+//   - "DoNotParseResponse" is set for client or request.
+//
+// this can be overridden at client level with [Request.SetResponseBodyLimit]
+func (c *Client) SetResponseBodyLimit(v int) *Client {
+	c.ResponseBodyLimit = v
+	return c
+}
+
 // EnableTrace method enables the Resty client trace for the requests fired from
 // the client using `httptrace.ClientTrace` and provides insights.
 //
@@ -1155,23 +1137,10 @@ func (c *Client) IsProxySet() bool {
 }
 
 // GetClient method returns the current `http.Client` used by the resty client.
-//
-// Since v1.1.0
 func (c *Client) GetClient() *http.Client {
 	return c.httpClient
 }
 
-// Transport method returns `*http.Transport` currently in use or error
-// in case currently used `transport` is not a `*http.Transport`.
-//
-// Since v2.8.0 become exported method.
-func (c *Client) Transport() (*http.Transport, error) {
-	if transport, ok := c.httpClient.Transport.(*http.Transport); ok {
-		return transport, nil
-	}
-	return nil, errors.New("current transport is not an *http.Transport instance")
-}
-
 // Clone returns a clone of the original client.
 //
 // Be careful when using this function:
@@ -1195,9 +1164,7 @@ func (c *Client) Clone() *Client {
 // Client Unexported methods
 //_______________________________________________________________________
 
-// Executes method executes the given `Request` object and returns response
-// error.
-func (c *Client) execute(req *Request) (*Response, error) {
+func (c *Client) executeBefore(req *Request) error {
 	// Lock the user-defined pre-request hooks.
 	c.udBeforeRequestLock.RLock()
 	defer c.udBeforeRequestLock.RUnlock()
@@ -1213,7 +1180,7 @@ func (c *Client) execute(req *Request) (*Response, error) {
 	// to modify the *resty.Request object
 	for _, f := range c.udBeforeRequest {
 		if err = f(c, req); err != nil {
-			return nil, wrapNoRetryErr(err)
+			return wrapNoRetryErr(err)
 		}
 	}
 
@@ -1221,14 +1188,14 @@ func (c *Client) execute(req *Request) (*Response, error) {
 	// will return an error if the rate limit is exceeded.
 	if req.client.rateLimiter != nil {
 		if !req.client.rateLimiter.Allow() {
-			return nil, wrapNoRetryErr(ErrRateLimitExceeded)
+			return wrapNoRetryErr(ErrRateLimitExceeded)
 		}
 	}
 
 	// resty middlewares
 	for _, f := range c.beforeRequest {
 		if err = f(c, req); err != nil {
-			return nil, wrapNoRetryErr(err)
+			return wrapNoRetryErr(err)
 		}
 	}
 
@@ -1239,15 +1206,24 @@ func (c *Client) execute(req *Request) (*Response, error) {
 	// call pre-request if defined
 	if c.preReqHook != nil {
 		if err = c.preReqHook(c, req.RawRequest); err != nil {
-			return nil, wrapNoRetryErr(err)
+			return wrapNoRetryErr(err)
 		}
 	}
 
 	if err = requestLogger(c, req); err != nil {
-		return nil, wrapNoRetryErr(err)
+		return wrapNoRetryErr(err)
 	}
 
 	req.RawRequest.Body = newRequestBodyReleaser(req.RawRequest.Body, req.bodyBuf)
+	return nil
+}
+
+// Executes method executes the given `Request` object and returns response
+// error.
+func (c *Client) execute(req *Request) (*Response, error) {
+	if err := c.executeBefore(req); err != nil {
+		return nil, err
+	}
 
 	req.Time = time.Now()
 	resp, err := c.httpClient.Do(req.RawRequest)
@@ -1278,7 +1254,7 @@ func (c *Client) execute(req *Request) (*Response, error) {
 			}
 		}
 
-		if response.body, err = io.ReadAll(body); err != nil {
+		if response.body, err = readAllWithLimit(body, req.responseBodyLimit); err != nil {
 			response.setReceivedAt()
 			return response, err
 		}
@@ -1298,6 +1274,39 @@ func (c *Client) execute(req *Request) (*Response, error) {
 	return response, wrapNoRetryErr(err)
 }
 
+var ErrResponseBodyTooLarge = errors.New("resty: response body too large")
+
+// https://github.com/golang/go/issues/51115
+// [io.LimitedReader] can only return [io.EOF]
+func readAllWithLimit(r io.Reader, maxSize int) ([]byte, error) {
+	if maxSize <= 0 {
+		return io.ReadAll(r)
+	}
+
+	var buf [512]byte // make buf stack allocated
+	result := make([]byte, 0, 512)
+	total := 0
+	for {
+		n, err := r.Read(buf[:])
+		total += n
+		if total > maxSize {
+			return nil, ErrResponseBodyTooLarge
+		}
+
+		if err != nil {
+			if err == io.EOF {
+				result = append(result, buf[:n]...)
+				break
+			}
+			return nil, err
+		}
+
+		result = append(result, buf[:n]...)
+	}
+
+	return result, nil
+}
+
 // getting TLS client config if not exists then create one
 func (c *Client) tlsConfig() (*tls.Config, error) {
 	transport, err := c.Transport()
@@ -1310,6 +1319,17 @@ func (c *Client) tlsConfig() (*tls.Config, error) {
 	return transport.TLSClientConfig, nil
 }
 
+// Transport method returns `*http.Transport` currently in use or error
+// in case currently used `transport` is not a `*http.Transport`.
+//
+// Since v2.8.0 become exported method.
+func (c *Client) Transport() (*http.Transport, error) {
+	if transport, ok := c.httpClient.Transport.(*http.Transport); ok {
+		return transport, nil
+	}
+	return nil, errors.New("current transport is not an *http.Transport instance")
+}
+
 // just an internal helper method
 func (c *Client) outputLogTo(w io.Writer) *Client {
 	c.log.(*logger).l.SetOutput(w)
@@ -1390,3 +1410,60 @@ type MultipartField struct {
 	ContentType string
 	io.Reader
 }
+
+//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
+// Unexported package methods
+//_______________________________________________________________________
+
+func createClient(hc *http.Client) *Client {
+	if hc.Transport == nil {
+		hc.Transport = createTransport(nil)
+	}
+
+	c := &Client{ // not setting lang default values
+		QueryParam:             url.Values{},
+		FormData:               url.Values{},
+		Header:                 http.Header{},
+		Cookies:                make([]*http.Cookie, 0),
+		RetryWaitTime:          defaultWaitTime,
+		RetryMaxWaitTime:       defaultMaxWaitTime,
+		PathParams:             make(map[string]string),
+		RawPathParams:          make(map[string]string),
+		JSONMarshal:            json.Marshal,
+		JSONUnmarshal:          json.Unmarshal,
+		XMLMarshal:             xml.Marshal,
+		XMLUnmarshal:           xml.Unmarshal,
+		HeaderAuthorizationKey: http.CanonicalHeaderKey("Authorization"),
+
+		jsonEscapeHTML:      true,
+		httpClient:          hc,
+		debugBodySizeLimit:  math.MaxInt32,
+		udBeforeRequestLock: &sync.RWMutex{},
+		afterResponseLock:   &sync.RWMutex{},
+	}
+
+	// Logger
+	c.SetLogger(createLogger())
+
+	// default before request middlewares
+	c.beforeRequest = []RequestMiddleware{
+		parseRequestURL,
+		parseRequestHeader,
+		parseRequestBody,
+		createHTTPRequest,
+		addCredentials,
+		createCurlCmd,
+	}
+
+	// user defined request middlewares
+	c.udBeforeRequest = []RequestMiddleware{}
+
+	// default after response middlewares
+	c.afterResponse = []ResponseMiddleware{
+		responseLogger,
+		parseResponseBody,
+		saveResponseIntoFile,
+	}
+
+	return c
+}
diff --git a/digest.go b/digest.go
index 3cd1963..3a08477 100644
--- a/digest.go
+++ b/digest.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com)
+// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com)
 // 2023 Segev Dagan (https://github.com/segevda)
 // 2024 Philipp Wolfer (https://github.com/phw)
 // All rights reserved.
diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel
new file mode 100644
index 0000000..849ea4e
--- /dev/null
+++ b/examples/BUILD.bazel
@@ -0,0 +1,10 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_test")
+
+go_test(
+    name = "examples_test",
+    srcs = [
+        "debug_curl_test.go",
+        "server_test.go",
+    ],
+    deps = ["//:resty"],
+)
diff --git a/go.mod b/go.mod
index 2959f04..d8b08d4 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,8 @@
-module github.com/go-resty/resty/v3
+module github.com/go-resty/resty/v2
 
-go 1.18
+go 1.19
 
 require (
-	golang.org/x/net v0.25.0
-	golang.org/x/time v0.5.0
+	golang.org/x/net v0.27.0
+	golang.org/x/time v0.6.0
 )
diff --git a/go.sum b/go.sum
index f0bd8d7..66793eb 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,4 @@
-golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
-golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
-golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
-golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
+golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
+golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
+golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
diff --git a/middleware.go b/middleware.go
index d116ae0..805f4ce 100644
--- a/middleware.go
+++ b/middleware.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
 // resty source code and usage is governed by a MIT style
 // license that can be found in the LICENSE file.
 
@@ -114,7 +114,12 @@ func parseRequestURL(c *Client, r *Request) error {
 			r.URL = "/" + r.URL
 		}
 
-		reqURL, err = url.Parse(c.BaseURL + r.URL)
+		// TODO: change to use c.BaseURL only in v3.0.0
+		baseURL := c.BaseURL
+		if len(baseURL) == 0 {
+			baseURL = c.HostURL
+		}
+		reqURL, err = url.Parse(baseURL + r.URL)
 		if err != nil {
 			return err
 		}
@@ -302,6 +307,16 @@ func addCredentials(c *Client, r *Request) error {
 	return nil
 }
 
+func createCurlCmd(c *Client, r *Request) (err error) {
+	if r.trace {
+		if r.resultCurlCmd == nil {
+			r.resultCurlCmd = new(string)
+		}
+		*r.resultCurlCmd = buildCurlRequest(r.RawRequest, c.httpClient.Jar)
+	}
+	return nil
+}
+
 func requestLogger(c *Client, r *Request) error {
 	if r.Debug {
 		rr := r.RawRequest
@@ -324,6 +339,8 @@ func requestLogger(c *Client, r *Request) error {
 		}
 
 		reqLog := "\n==============================================================================\n" +
+			"~~~ REQUEST(curl) ~~~\n" +
+			fmt.Sprintf("CURL:\n	%v\n", buildCurlRequest(r.RawRequest, r.client.httpClient.Jar)) +
 			"~~~ REQUEST ~~~\n" +
 			fmt.Sprintf("%s  %s  %s\n", r.Method, rr.URL.RequestURI(), rr.Proto) +
 			fmt.Sprintf("HOST   : %s\n", rr.URL.Host) +
@@ -412,6 +429,13 @@ func handleMultipart(c *Client, r *Request) error {
 	r.bodyBuf = acquireBuffer()
 	w := multipart.NewWriter(r.bodyBuf)
 
+	// Set boundary if not set by user
+	if r.multipartBoundary != "" {
+		if err := w.SetBoundary(r.multipartBoundary); err != nil {
+			return err
+		}
+	}
+
 	for k, v := range c.FormData {
 		for _, iv := range v {
 			if err := w.WriteField(k, iv); err != nil {
diff --git a/redirect.go b/redirect.go
index ed58d73..19bd587 100644
--- a/redirect.go
+++ b/redirect.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
 // resty source code and usage is governed by a MIT style
 // license that can be found in the LICENSE file.
 
diff --git a/request.go b/request.go
index 4e13ff0..cfbe89b 100644
--- a/request.go
+++ b/request.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
 // resty source code and usage is governed by a MIT style
 // license that can be found in the LICENSE file.
 
@@ -39,6 +39,7 @@ type Request struct {
 	Time          time.Time
 	Body          interface{}
 	Result        interface{}
+	resultCurlCmd *string
 	Error         interface{}
 	RawRequest    *http.Request
 	SRV           *SRVRecord
@@ -68,9 +69,29 @@ type Request struct {
 	bodyBuf             *bytes.Buffer
 	clientTrace         *clientTrace
 	log                 Logger
+	multipartBoundary   string
 	multipartFiles      []*File
 	multipartFields     []*MultipartField
 	retryConditions     []RetryConditionFunc
+	responseBodyLimit   int
+}
+
+// Generate curl command for the request.
+func (r *Request) GenerateCurlCommand() string {
+	if r.resultCurlCmd != nil {
+		return *r.resultCurlCmd
+	} else {
+		if r.RawRequest == nil {
+			r.client.executeBefore(r) // mock with r.Get("/")
+		}
+		if r.resultCurlCmd == nil {
+			r.resultCurlCmd = new(string)
+		}
+		if *r.resultCurlCmd == "" {
+			*r.resultCurlCmd = buildCurlRequest(r.RawRequest, r.client.httpClient.Jar)
+		}
+		return *r.resultCurlCmd
+	}
 }
 
 // Context method returns the Context if its already set in request
@@ -439,6 +460,15 @@ func (r *Request) SetMultipartFields(fields ...*MultipartField) *Request {
 	return r
 }
 
+// SetMultipartBoundary method sets the custom multipart boundary for the multipart request.
+// Typically, the `mime/multipart` package generates a random multipart boundary, if not provided.
+//
+// Since v2.15.0
+func (r *Request) SetMultipartBoundary(boundary string) *Request {
+	r.multipartBoundary = boundary
+	return r
+}
+
 // SetContentLength method sets the HTTP header `Content-Length` value for current request.
 // By default Resty won't set `Content-Length`. Also you have an option to enable for every
 // request.
@@ -571,6 +601,20 @@ func (r *Request) SetDoNotParseResponse(parse bool) *Request {
 	return r
 }
 
+// SetResponseBodyLimit set a max body size limit on response, avoid reading too many data to memory.
+//
+// Request will return [resty.ErrResponseBodyTooLarge] if uncompressed response body size if larger than limit.
+// Body size limit will not be enforced in following case:
+//   - ResponseBodyLimit <= 0, which is the default behavior.
+//   - [Request.SetOutput] is called to save a response data to file.
+//   - "DoNotParseResponse" is set for client or request.
+//
+// This will override Client config.
+func (r *Request) SetResponseBodyLimit(v int) *Request {
+	r.responseBodyLimit = v
+	return r
+}
+
 // SetPathParam method sets single URL path key-value pair in the
 // Resty current request instance.
 //
@@ -886,7 +930,7 @@ func (r *Request) Patch(url string) (*Response, error) {
 // for current `Request`.
 //
 //	req := client.R()
-//	req.Method = resty.GET
+//	req.Method = resty.MethodGet
 //	req.URL = "http://httpbin.org/get"
 //	resp, err := req.Send()
 func (r *Request) Send() (*Response, error) {
@@ -896,7 +940,7 @@ func (r *Request) Send() (*Response, error) {
 // Execute method performs the HTTP request with given HTTP method and URL
 // for current `Request`.
 //
-//	resp, err := client.R().Execute(resty.GET, "http://httpbin.org/get")
+//	resp, err := client.R().Execute(resty.MethodGet, "http://httpbin.org/get")
 func (r *Request) Execute(method, url string) (*Response, error) {
 	var addrs []*net.SRV
 	var resp *Response
diff --git a/response.go b/response.go
index 63c95c4..58a8e81 100644
--- a/response.go
+++ b/response.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
 // resty source code and usage is governed by a MIT style
 // license that can be found in the LICENSE file.
 
diff --git a/resty.go b/resty.go
index 0abbbd0..7458af7 100644
--- a/resty.go
+++ b/resty.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
 // resty source code and usage is governed by a MIT style
 // license that can be found in the LICENSE file.
 
@@ -6,32 +6,22 @@
 package resty
 
 import (
-	"encoding/json"
-	"encoding/xml"
-	"math"
 	"net"
 	"net/http"
 	"net/http/cookiejar"
-	"net/url"
-	"runtime"
-	"sync"
-	"time"
 
 	"golang.org/x/net/publicsuffix"
 )
 
 // Version # of resty
-const Version = "3.0.0-dev"
+const Version = "2.14.0"
 
 // New method creates a new Resty client.
 func New() *Client {
-	return NewWithTransportSettings(nil)
-}
-
-// NewWithTransportSettings method creates a new Resty client with provided
-// timeout values.
-func NewWithTransportSettings(transportSettings *TransportSettings) *Client {
-	return NewWithDialerAndTransportSettings(nil, transportSettings)
+	cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
+	return createClient(&http.Client{
+		Jar: cookieJar,
+	})
 }
 
 // NewWithClient method creates a new Resty client with given `http.Client`.
@@ -39,170 +29,12 @@ func NewWithClient(hc *http.Client) *Client {
 	return createClient(hc)
 }
 
-// NewWithDialer method creates a new Resty client with given Local Address
-// to dial from.
-func NewWithDialer(dialer *net.Dialer) *Client {
-	return NewWithDialerAndTransportSettings(dialer, nil)
-}
-
 // NewWithLocalAddr method creates a new Resty client with given Local Address
 // to dial from.
 func NewWithLocalAddr(localAddr net.Addr) *Client {
-	return NewWithDialerAndTransportSettings(
-		&net.Dialer{LocalAddr: localAddr},
-		nil,
-	)
-}
-
-// NewWithDialerAndTransportSettings method creates a new Resty client with given Local Address
-// to dial from.
-func NewWithDialerAndTransportSettings(dialer *net.Dialer, transportSettings *TransportSettings) *Client {
+	cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
 	return createClient(&http.Client{
-		Jar:       createCookieJar(),
-		Transport: createTransport(dialer, transportSettings),
+		Jar:       cookieJar,
+		Transport: createTransport(localAddr),
 	})
 }
-
-//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
-// Unexported methods
-//_______________________________________________________________________
-
-func createTransport(dialer *net.Dialer, transportSettings *TransportSettings) *http.Transport {
-	if transportSettings == nil {
-		transportSettings = &TransportSettings{}
-	}
-
-	// Dialer
-
-	if dialer == nil {
-		dialer = &net.Dialer{}
-	}
-
-	if transportSettings.DialerTimeout > 0 {
-		dialer.Timeout = transportSettings.DialerTimeout
-	} else {
-		dialer.Timeout = 30 * time.Second
-	}
-
-	if transportSettings.DialerKeepAlive > 0 {
-		dialer.KeepAlive = transportSettings.DialerKeepAlive
-	} else {
-		dialer.KeepAlive = 30 * time.Second
-	}
-
-	// Transport
-	t := &http.Transport{
-		Proxy:              http.ProxyFromEnvironment,
-		DialContext:        transportDialContext(dialer),
-		DisableKeepAlives:  transportSettings.DisableKeepAlives,
-		DisableCompression: transportSettings.DisableCompression,
-		ForceAttemptHTTP2:  true,
-	}
-
-	if transportSettings.IdleConnTimeout > 0 {
-		t.IdleConnTimeout = transportSettings.IdleConnTimeout
-	} else {
-		t.IdleConnTimeout = 90 * time.Second
-	}
-
-	if transportSettings.TLSHandshakeTimeout > 0 {
-		t.TLSHandshakeTimeout = transportSettings.TLSHandshakeTimeout
-	} else {
-		t.TLSHandshakeTimeout = 10 * time.Second
-	}
-
-	if transportSettings.ExpectContinueTimeout > 0 {
-		t.ExpectContinueTimeout = transportSettings.ExpectContinueTimeout
-	} else {
-		t.ExpectContinueTimeout = 1 * time.Second
-	}
-
-	if transportSettings.MaxIdleConns > 0 {
-		t.MaxIdleConns = transportSettings.MaxIdleConns
-	} else {
-		t.MaxIdleConns = 100
-	}
-
-	if transportSettings.MaxIdleConnsPerHost > 0 {
-		t.MaxIdleConnsPerHost = transportSettings.MaxIdleConnsPerHost
-	} else {
-		t.MaxIdleConnsPerHost = runtime.GOMAXPROCS(0) + 1
-	}
-
-	//
-	// No default value in Resty for following settings, added to
-	// provide ability to set value otherwise the Go HTTP client
-	// default value applies.
-	//
-
-	if transportSettings.ResponseHeaderTimeout > 0 {
-		t.ResponseHeaderTimeout = transportSettings.ResponseHeaderTimeout
-	}
-
-	if transportSettings.MaxResponseHeaderBytes > 0 {
-		t.MaxResponseHeaderBytes = transportSettings.MaxResponseHeaderBytes
-	}
-
-	if transportSettings.WriteBufferSize > 0 {
-		t.WriteBufferSize = transportSettings.WriteBufferSize
-	}
-
-	if transportSettings.ReadBufferSize > 0 {
-		t.ReadBufferSize = transportSettings.ReadBufferSize
-	}
-
-	return t
-}
-
-func createCookieJar() *cookiejar.Jar {
-	cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
-	return cookieJar
-}
-
-func createClient(hc *http.Client) *Client {
-	c := &Client{ // not setting language default values
-		QueryParam:             url.Values{},
-		FormData:               url.Values{},
-		Header:                 http.Header{},
-		Cookies:                make([]*http.Cookie, 0),
-		RetryWaitTime:          defaultWaitTime,
-		RetryMaxWaitTime:       defaultMaxWaitTime,
-		PathParams:             make(map[string]string),
-		RawPathParams:          make(map[string]string),
-		JSONMarshal:            json.Marshal,
-		JSONUnmarshal:          json.Unmarshal,
-		XMLMarshal:             xml.Marshal,
-		XMLUnmarshal:           xml.Unmarshal,
-		HeaderAuthorizationKey: http.CanonicalHeaderKey("Authorization"),
-
-		jsonEscapeHTML:      true,
-		httpClient:          hc,
-		debugBodySizeLimit:  math.MaxInt32,
-		udBeforeRequestLock: &sync.RWMutex{},
-		afterResponseLock:   &sync.RWMutex{},
-	}
-
-	// Logger
-	c.SetLogger(createLogger())
-
-	// default before request middlewares
-	c.beforeRequest = []RequestMiddleware{
-		parseRequestURL,
-		parseRequestHeader,
-		parseRequestBody,
-		createHTTPRequest,
-		addCredentials,
-	}
-
-	// user defined request middlewares
-	c.udBeforeRequest = []RequestMiddleware{}
-
-	// default after response middlewares
-	c.afterResponse = []ResponseMiddleware{
-		responseLogger,
-		parseResponseBody,
-		saveResponseIntoFile,
-	}
-
-	return c
-}
diff --git a/retry.go b/retry.go
index c5eda26..932a266 100644
--- a/retry.go
+++ b/retry.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
 // resty source code and usage is governed by a MIT style
 // license that can be found in the LICENSE file.
 
diff --git a/shellescape/BUILD.bazel b/shellescape/BUILD.bazel
new file mode 100644
index 0000000..fe829e3
--- /dev/null
+++ b/shellescape/BUILD.bazel
@@ -0,0 +1,14 @@
+load("@io_bazel_rules_go//go:def.bzl", "go_library")
+
+go_library(
+    name = "shellescape",
+    srcs = ["shellescape.go"],
+    importpath = "github.com/go-resty/resty/v2/shellescape",
+    visibility = ["//visibility:public"],
+)
+
+alias(
+    name = "go_default_library",
+    actual = ":shellescape",
+    visibility = ["//visibility:public"],
+)
diff --git a/shellescape/shellescape.go b/shellescape/shellescape.go
new file mode 100644
index 0000000..5e6a379
--- /dev/null
+++ b/shellescape/shellescape.go
@@ -0,0 +1,34 @@
+/*
+Package shellescape provides the shellescape.Quote to escape arbitrary
+strings for a safe use as command line arguments in the most common
+POSIX shells.
+
+The original Python package which this work was inspired by can be found
+at https://pypi.python.org/pypi/shellescape.
+*/
+package shellescape
+
+import (
+	"regexp"
+	"strings"
+)
+
+var pattern *regexp.Regexp
+
+func init() {
+	pattern = regexp.MustCompile(`[^\w@%+=:,./-]`)
+}
+
+// Quote returns a shell-escaped version of the string s. The returned value
+// is a string that can safely be used as one token in a shell command line.
+func Quote(s string) string {
+	if len(s) == 0 {
+		return "''"
+	}
+
+	if pattern.MatchString(s) {
+		return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'"
+	}
+
+	return s
+}
diff --git a/trace.go b/trace.go
index be7555c..7798a39 100644
--- a/trace.go
+++ b/trace.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
 // resty source code and usage is governed by a MIT style
 // license that can be found in the LICENSE file.
 
diff --git a/transport.go b/transport.go
new file mode 100644
index 0000000..13c3de3
--- /dev/null
+++ b/transport.go
@@ -0,0 +1,36 @@
+//go:build go1.13
+// +build go1.13
+
+// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// resty source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package resty
+
+import (
+	"net"
+	"net/http"
+	"runtime"
+	"time"
+)
+
+func createTransport(localAddr net.Addr) *http.Transport {
+	dialer := &net.Dialer{
+		Timeout:   30 * time.Second,
+		KeepAlive: 30 * time.Second,
+		DualStack: true,
+	}
+	if localAddr != nil {
+		dialer.LocalAddr = localAddr
+	}
+	return &http.Transport{
+		Proxy:                 http.ProxyFromEnvironment,
+		DialContext:           transportDialContext(dialer),
+		ForceAttemptHTTP2:     true,
+		MaxIdleConns:          100,
+		IdleConnTimeout:       90 * time.Second,
+		TLSHandshakeTimeout:   10 * time.Second,
+		ExpectContinueTimeout: 1 * time.Second,
+		MaxIdleConnsPerHost:   runtime.GOMAXPROCS(0) + 1,
+	}
+}
diff --git a/transport112.go b/transport112.go
new file mode 100644
index 0000000..beb0301
--- /dev/null
+++ b/transport112.go
@@ -0,0 +1,35 @@
+//go:build !go1.13
+// +build !go1.13
+
+// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// resty source code and usage is governed by a MIT style
+// license that can be found in the LICENSE file.
+
+package resty
+
+import (
+	"net"
+	"net/http"
+	"runtime"
+	"time"
+)
+
+func createTransport(localAddr net.Addr) *http.Transport {
+	dialer := &net.Dialer{
+		Timeout:   30 * time.Second,
+		KeepAlive: 30 * time.Second,
+		DualStack: true,
+	}
+	if localAddr != nil {
+		dialer.LocalAddr = localAddr
+	}
+	return &http.Transport{
+		Proxy:                 http.ProxyFromEnvironment,
+		DialContext:           dialer.DialContext,
+		MaxIdleConns:          100,
+		IdleConnTimeout:       90 * time.Second,
+		TLSHandshakeTimeout:   10 * time.Second,
+		ExpectContinueTimeout: 1 * time.Second,
+		MaxIdleConnsPerHost:   runtime.GOMAXPROCS(0) + 1,
+	}
+}
diff --git a/util.go b/util.go
index 5a69e4f..7bbba91 100644
--- a/util.go
+++ b/util.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
 // resty source code and usage is governed by a MIT style
 // license that can be found in the LICENSE file.
 
diff --git a/util_curl.go b/util_curl.go
new file mode 100644
index 0000000..e50c3b3
--- /dev/null
+++ b/util_curl.go
@@ -0,0 +1,76 @@
+package resty
+
+import (
+	"bytes"
+	"io"
+	"net/http"
+	"net/http/cookiejar"
+
+	"net/url"
+	"strings"
+
+	"github.com/go-resty/resty/v2/shellescape"
+)
+
+func buildCurlRequest(req *http.Request, httpCookiejar http.CookieJar) (curl string) {
+	// 1. Generate curl raw headers
+	curl = "curl -X " + req.Method + " "
+	// req.Host + req.URL.Path + "?" + req.URL.RawQuery + " " + req.Proto + " "
+	headers := dumpCurlHeaders(req)
+	for _, kv := range *headers {
+		curl += `-H ` + shellescape.Quote(kv[0]+": "+kv[1]) + ` `
+	}
+
+	// 2. Generate curl cookies
+	if cookieJar, ok := httpCookiejar.(*cookiejar.Jar); ok {
+		cookies := cookieJar.Cookies(req.URL)
+		if len(cookies) > 0 {
+			curl += ` -H ` + shellescape.Quote(dumpCurlCookies(cookies)) + " "
+		}
+	}
+
+	// 3. Generate curl body
+	if req.Body != nil {
+		buf, _ := io.ReadAll(req.Body)
+		req.Body = io.NopCloser(bytes.NewBuffer(buf)) // important!!
+		curl += `-d ` + shellescape.Quote(string(buf))
+	}
+
+	urlString := shellescape.Quote(req.URL.String())
+	if urlString == "''" {
+		urlString = "'http://unexecuted-request'"
+	}
+	curl += " " + urlString
+	return curl
+}
+
+// dumpCurlCookies dumps cookies to curl format
+func dumpCurlCookies(cookies []*http.Cookie) string {
+	sb := strings.Builder{}
+	sb.WriteString("Cookie: ")
+	for _, cookie := range cookies {
+		sb.WriteString(cookie.Name + "=" + url.QueryEscape(cookie.Value) + "&")
+	}
+	return strings.TrimRight(sb.String(), "&")
+}
+
+// dumpCurlHeaders dumps headers to curl format
+func dumpCurlHeaders(req *http.Request) *[][2]string {
+	headers := [][2]string{}
+	for k, vs := range req.Header {
+		for _, v := range vs {
+			headers = append(headers, [2]string{k, v})
+		}
+	}
+	n := len(headers)
+	for i := 0; i < n; i++ {
+		for j := n - 1; j > i; j-- {
+			jj := j - 1
+			h1, h2 := headers[j], headers[jj]
+			if h1[0] < h2[0] {
+				headers[jj], headers[j] = headers[j], headers[jj]
+			}
+		}
+	}
+	return &headers
+}


Test patch:
diff --git a/client_test.go b/client_test.go
index 90789e0..acd31d4 100644
--- a/client_test.go
+++ b/client_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
 // resty source code and usage is governed by a MIT style
 // license that can be found in the LICENSE file.
 
@@ -6,6 +6,8 @@ package resty
 
 import (
 	"bytes"
+	"compress/gzip"
+	"crypto/rand"
 	"crypto/tls"
 	"errors"
 	"fmt"
@@ -29,7 +31,7 @@ func TestClientBasicAuth(t *testing.T) {
 
 	c := dc()
 	c.SetBasicAuth("myuser", "basicauth").
-		SetBaseURL(ts.URL).
+		SetHostURL(ts.URL).
 		SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
 
 	resp, err := c.R().
@@ -50,7 +52,7 @@ func TestClientAuthToken(t *testing.T) {
 	c := dc()
 	c.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).
 		SetAuthToken("004DDB79-6801-4587-B976-F093E6AC44FF").
-		SetBaseURL(ts.URL + "/")
+		SetHostURL(ts.URL + "/")
 
 	resp, err := c.R().Get("/profile")
 
@@ -66,7 +68,7 @@ func TestClientAuthScheme(t *testing.T) {
 	// Ensure default Bearer
 	c.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).
 		SetAuthToken("004DDB79-6801-4587-B976-F093E6AC44FF").
-		SetBaseURL(ts.URL + "/")
+		SetHostURL(ts.URL + "/")
 
 	resp, err := c.R().Get("/profile")
 
@@ -384,8 +386,8 @@ func TestClientOptions(t *testing.T) {
 	client.SetContentLength(true)
 	assertEqual(t, client.setContentLength, true)
 
-	client.SetBaseURL("http://httpbin.org")
-	assertEqual(t, "http://httpbin.org", client.BaseURL)
+	client.SetHostURL("http://httpbin.org")
+	assertEqual(t, "http://httpbin.org", client.HostURL)
 
 	client.SetHeader(hdrContentTypeKey, "application/json; charset=utf-8")
 	client.SetHeaders(map[string]string{
@@ -791,77 +793,13 @@ func TestDebugLogSimultaneously(t *testing.T) {
 	}
 }
 
-func TestCustomTransportSettings(t *testing.T) {
-	ts := createGetServer(t)
-	defer ts.Close()
-
-	customTransportSettings := &TransportSettings{
-		DialerTimeout:          30 * time.Second,
-		DialerKeepAlive:        15 * time.Second,
-		IdleConnTimeout:        120 * time.Second,
-		TLSHandshakeTimeout:    20 * time.Second,
-		ExpectContinueTimeout:  1 * time.Second,
-		MaxIdleConns:           50,
-		MaxIdleConnsPerHost:    3,
-		ResponseHeaderTimeout:  10 * time.Second,
-		MaxResponseHeaderBytes: 1 << 10,
-		WriteBufferSize:        2 << 10,
-		ReadBufferSize:         2 << 10,
-	}
-	client := NewWithTransportSettings(customTransportSettings)
-	client.SetBaseURL(ts.URL)
-
-	resp, err := client.R().Get("/")
-	assertNil(t, err)
-	assertEqual(t, resp.String(), "TestGet: text response")
-}
-
-func TestDefaultDialerTransportSettings(t *testing.T) {
-	ts := createGetServer(t)
-	defer ts.Close()
-
-	t.Run("transport-default", func(t *testing.T) {
-		client := NewWithTransportSettings(nil)
-		client.SetBaseURL(ts.URL)
-
-		resp, err := client.R().Get("/")
-		assertNil(t, err)
-		assertEqual(t, resp.String(), "TestGet: text response")
-	})
-
-	t.Run("dialer-transport-default", func(t *testing.T) {
-		client := NewWithDialerAndTransportSettings(nil, nil)
-		client.SetBaseURL(ts.URL)
-
-		resp, err := client.R().Get("/")
-		assertNil(t, err)
-		assertEqual(t, resp.String(), "TestGet: text response")
-	})
-}
-
-func TestNewWithDialer(t *testing.T) {
-	ts := createGetServer(t)
-	defer ts.Close()
-
-	dialer := &net.Dialer{
-		Timeout:   15 * time.Second,
-		KeepAlive: 15 * time.Second,
-	}
-	client := NewWithDialer(dialer)
-	client.SetBaseURL(ts.URL)
-
-	resp, err := client.R().Get("/")
-	assertNil(t, err)
-	assertEqual(t, resp.String(), "TestGet: text response")
-}
-
 func TestNewWithLocalAddr(t *testing.T) {
 	ts := createGetServer(t)
 	defer ts.Close()
 
 	localAddress, _ := net.ResolveTCPAddr("tcp", "127.0.0.1")
 	client := NewWithLocalAddr(localAddress)
-	client.SetBaseURL(ts.URL)
+	client.SetHostURL(ts.URL)
 
 	resp, err := client.R().Get("/")
 	assertNil(t, err)
@@ -1076,7 +1014,7 @@ func TestHostURLForGH318AndGH407(t *testing.T) {
 	// assertNotNil(t, resp)
 
 	t.Log("with leading `/` on request & with trailing `/` on host url")
-	c.SetBaseURL(ts.URL + "/")
+	c.SetHostURL(ts.URL + "/")
 	resp, err := c.R().
 		SetBody(map[string]interface{}{"username": "testuser", "password": "testpass"}).
 		Post("/login")
@@ -1124,7 +1062,7 @@ func TestUnixSocket(t *testing.T) {
 
 	// Set the previous transport that we created, set the scheme of the communication to the
 	// socket and set the unixSocket as the HostURL.
-	client.SetTransport(&transport).SetScheme("http").SetBaseURL(unixSocketAddr)
+	client.SetTransport(&transport).SetScheme("http").SetHostURL(unixSocketAddr)
 
 	// No need to write the host's URL on the request, just the path.
 	res, err := client.R().Get("http://localhost/")
@@ -1161,3 +1099,56 @@ func TestClone(t *testing.T) {
 	assertEqual(t, "clone", parent.UserInfo.Username)
 	assertEqual(t, "clone", clone.UserInfo.Username)
 }
+
+func TestResponseBodyLimit(t *testing.T) {
+	ts := createTestServer(func(w http.ResponseWriter, r *http.Request) {
+		io.CopyN(w, rand.Reader, 100*800)
+	})
+	defer ts.Close()
+
+	t.Run("Client body limit", func(t *testing.T) {
+		c := dc().SetResponseBodyLimit(1024)
+
+		_, err := c.R().Get(ts.URL + "/")
+		assertNotNil(t, err)
+		assertEqual(t, err, ErrResponseBodyTooLarge)
+	})
+
+	t.Run("request body limit", func(t *testing.T) {
+		c := dc()
+
+		_, err := c.R().SetResponseBodyLimit(1024).Get(ts.URL + "/")
+		assertNotNil(t, err)
+		assertEqual(t, err, ErrResponseBodyTooLarge)
+	})
+
+	t.Run("body less than limit", func(t *testing.T) {
+		c := dc()
+
+		res, err := c.R().SetResponseBodyLimit(800*100 + 10).Get(ts.URL + "/")
+		assertNil(t, err)
+		assertEqual(t, 800*100, len(res.body))
+	})
+
+	t.Run("no body limit", func(t *testing.T) {
+		c := dc()
+
+		res, err := c.R().Get(ts.URL + "/")
+		assertNil(t, err)
+		assertEqual(t, 800*100, len(res.body))
+	})
+
+	t.Run("read error", func(t *testing.T) {
+		tse := createTestServer(func(w http.ResponseWriter, r *http.Request) {
+			w.Header().Set(hdrContentEncodingKey, "gzip")
+			var buf [1024]byte
+			w.Write(buf[:])
+		})
+		defer tse.Close()
+
+		c := dc()
+
+		_, err := c.R().SetResponseBodyLimit(10240).Get(tse.URL + "/")
+		assertErrorIs(t, err, gzip.ErrHeader)
+	})
+}
diff --git a/context_test.go b/context_test.go
index f3c5345..5d60cff 100644
--- a/context_test.go
+++ b/context_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com)
+// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com)
 // 2016 Andrew Grigorev (https://github.com/ei-grad)
 // All rights reserved.
 // resty source code and usage is governed by a MIT style
diff --git a/example_test.go b/example_test.go
index d7d91a1..7b7514d 100644
--- a/example_test.go
+++ b/example_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2023 Jeevanandam M. (jeeva@myjeeva.com), All rights reserved.
+// Copyright (c) 2015-2024 Jeevanandam M. (jeeva@myjeeva.com), All rights reserved.
 // resty source code and usage is governed by a MIT style
 // license that can be found in the LICENSE file.
 
@@ -15,7 +15,7 @@ import (
 
 	"golang.org/x/net/proxy"
 
-	"github.com/go-resty/resty/v3"
+	"github.com/go-resty/resty/v2"
 )
 
 type DropboxError struct {
diff --git a/examples/debug_curl_test.go b/examples/debug_curl_test.go
new file mode 100644
index 0000000..72bb68d
--- /dev/null
+++ b/examples/debug_curl_test.go
@@ -0,0 +1,132 @@
+package examples
+
+import (
+	"io"
+	"net/http"
+	"os"
+	"strings"
+	"testing"
+
+	"github.com/go-resty/resty/v2"
+)
+
+// 1. Generate curl for unexecuted request(dry-run)
+func TestGenerateUnexcutedCurl(t *testing.T) {
+	ts := createHttpbinServer(0)
+	defer ts.Close()
+
+	req := resty.New().R().
+		SetBody(map[string]string{
+			"name": "Alex",
+		}).
+		SetCookies(
+			[]*http.Cookie{
+				{Name: "count", Value: "1"},
+			},
+		)
+
+	curlCmdUnexecuted := req.GenerateCurlCommand()
+
+	if !strings.Contains(curlCmdUnexecuted, "Cookie: count=1") ||
+		!strings.Contains(curlCmdUnexecuted, "curl -X GET") ||
+		!strings.Contains(curlCmdUnexecuted, `-d '{"name":"Alex"}'`) {
+		t.Fatal("Incomplete curl:", curlCmdUnexecuted)
+	} else {
+		t.Log("curlCmdUnexecuted: \n", curlCmdUnexecuted)
+	}
+
+}
+
+// 2. Generate curl for executed request
+func TestGenerateExecutedCurl(t *testing.T) {
+	ts := createHttpbinServer(0)
+	defer ts.Close()
+
+	data := map[string]string{
+		"name": "Alex",
+	}
+	req := resty.New().R().
+		SetBody(data).
+		SetCookies(
+			[]*http.Cookie{
+				{Name: "count", Value: "1"},
+			},
+		)
+
+	url := ts.URL + "/post"
+	resp, err := req.
+		EnableTrace().
+		Post(url)
+	if err != nil {
+		t.Fatal(err)
+	}
+	curlCmdExecuted := resp.Request.GenerateCurlCommand()
+	if !strings.Contains(curlCmdExecuted, "Cookie: count=1") ||
+		!strings.Contains(curlCmdExecuted, "curl -X POST") ||
+		!strings.Contains(curlCmdExecuted, `-d '{"name":"Alex"}'`) ||
+		!strings.Contains(curlCmdExecuted, url) {
+		t.Fatal("Incomplete curl:", curlCmdExecuted)
+	} else {
+		t.Log("curlCmdExecuted: \n", curlCmdExecuted)
+	}
+}
+
+// 3. Generate curl in debug mode
+func TestDebugModeCurl(t *testing.T) {
+	ts := createHttpbinServer(0)
+	defer ts.Close()
+
+	// 1. Capture stderr
+	getOutput, restore := captureStderr()
+	defer restore()
+
+	// 2. Build request
+	req := resty.New().R().
+		SetBody(map[string]string{
+			"name": "Alex",
+		}).
+		SetCookies(
+			[]*http.Cookie{
+				{Name: "count", Value: "1"},
+			},
+		)
+
+	// 3. Execute request: set debug mode
+	url := ts.URL + "/post"
+	_, err := req.SetDebug(true).Post(url)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	// 4. test output curl
+	output := getOutput()
+	if !strings.Contains(output, "Cookie: count=1") ||
+		!strings.Contains(output, `-d '{"name":"Alex"}'`) {
+		t.Fatal("Incomplete debug curl info:", output)
+	} else {
+		t.Log("Normal debug curl info: \n", output)
+	}
+}
+
+func captureStderr() (getOutput func() string, restore func()) {
+	old := os.Stderr
+	r, w, err := os.Pipe()
+	if err != nil {
+		panic(err)
+	}
+	os.Stderr = w
+	getOutput = func() string {
+		w.Close()
+		buf := make([]byte, 2048)
+		n, err := r.Read(buf)
+		if err != nil && err != io.EOF {
+			panic(err)
+		}
+		return string(buf[:n])
+	}
+	restore = func() {
+		os.Stderr = old
+		w.Close()
+	}
+	return getOutput, restore
+}
diff --git a/examples/server_test.go b/examples/server_test.go
new file mode 100644
index 0000000..285f8b6
--- /dev/null
+++ b/examples/server_test.go
@@ -0,0 +1,162 @@
+package examples
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	ioutil "io"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"strings"
+)
+
+const maxMultipartMemory = 4 << 30 // 4MB
+
+// tlsCert:
+//
+//	0 No certificate
+//	1 With self-signed certificate
+//	2 With custom certificate from CA(todo)
+func createHttpbinServer(tlsCert int) (ts *httptest.Server) {
+	ts = createTestServer(func(w http.ResponseWriter, r *http.Request) {
+		httpbinHandler(w, r)
+	}, tlsCert)
+
+	return ts
+}
+
+func httpbinHandler(w http.ResponseWriter, r *http.Request) {
+	w.Header().Set("Content-Type", "application/json")
+	body, _ := ioutil.ReadAll(r.Body)
+	r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) // important!!
+	m := map[string]interface{}{
+		"args":    parseRequestArgs(r),
+		"headers": dumpRequestHeader(r),
+		"data":    string(body),
+		"json":    nil,
+		"form":    map[string]string{},
+		"files":   map[string]string{},
+		"method":  r.Method,
+		"origin":  r.RemoteAddr,
+		"url":     r.URL.String(),
+	}
+
+	// 1. parse text/plain
+	if strings.HasPrefix(r.Header.Get("Content-Type"), "text/plain") {
+		m["data"] = string(body)
+	}
+
+	// 2. parse application/json
+	if strings.HasPrefix(r.Header.Get("Content-Type"), "application/json") {
+		var data interface{}
+		if err := json.Unmarshal(body, &data); err != nil {
+			m["err"] = err.Error()
+		} else {
+			m["json"] = data
+		}
+	}
+
+	// 3. parse application/x-www-form-urlencoded
+	if strings.HasPrefix(r.Header.Get("Content-Type"), "application/x-www-form-urlencoded") {
+		m["form"] = parseQueryString(string(body))
+	}
+
+	// 4. parse multipart/form-data
+	if strings.HasPrefix(r.Header.Get("Content-Type"), "multipart/form-data") {
+		form, files := readMultipartForm(r)
+		m["form"] = form
+		m["files"] = files
+	}
+	buf, _ := json.Marshal(m)
+	_, _ = w.Write(buf)
+}
+
+func readMultipartForm(r *http.Request) (map[string]string, map[string]string) {
+	if err := r.ParseMultipartForm(maxMultipartMemory); err != nil {
+		if err != http.ErrNotMultipart {
+			panic(fmt.Sprintf("error on parse multipart form array: %v", err))
+		}
+	}
+	// parse form data
+	formData := make(map[string]string)
+	for k, vs := range r.PostForm {
+		for _, v := range vs {
+			formData[k] = v
+		}
+	}
+	// parse files
+	files := make(map[string]string)
+	if r.MultipartForm != nil && r.MultipartForm.File != nil {
+		for key, fhs := range r.MultipartForm.File {
+			// if len(fhs)>0
+			// f, err := fhs[0].Open()
+			files[key] = fhs[0].Filename
+		}
+	}
+	return formData, files
+}
+
+func dumpRequestHeader(req *http.Request) string {
+	var res strings.Builder
+	headers := sortHeaders(req)
+	for _, kv := range headers {
+		res.WriteString(kv[0] + ": " + kv[1] + "\n")
+	}
+	return res.String()
+}
+
+// sortHeaders
+func sortHeaders(request *http.Request) [][2]string {
+	headers := [][2]string{}
+	for k, vs := range request.Header {
+		for _, v := range vs {
+			headers = append(headers, [2]string{k, v})
+		}
+	}
+	n := len(headers)
+	for i := 0; i < n; i++ {
+		for j := n - 1; j > i; j-- {
+			jj := j - 1
+			h1, h2 := headers[j], headers[jj]
+			if h1[0] < h2[0] {
+				headers[jj], headers[j] = headers[j], headers[jj]
+			}
+		}
+	}
+	return headers
+}
+
+func parseRequestArgs(request *http.Request) map[string]string {
+	query := request.URL.RawQuery
+	return parseQueryString(query)
+}
+
+func parseQueryString(query string) map[string]string {
+	params := map[string]string{}
+	paramsList, _ := url.ParseQuery(query)
+	for key, vals := range paramsList {
+		// params[key] = vals[len(vals)-1]
+		params[key] = strings.Join(vals, ",")
+	}
+	return params
+}
+
+/*
+*
+  - tlsCert:
+    0 no certificate
+    1 with self-signed certificate
+    2 with custom certificate from CA(todo)
+*/
+func createTestServer(fn func(w http.ResponseWriter, r *http.Request), tlsCert int) (ts *httptest.Server) {
+	if tlsCert == 0 {
+		// 1. http test server
+		ts = httptest.NewServer(http.HandlerFunc(fn))
+	} else if tlsCert == 1 {
+		// 2. https test server: https://stackoverflow.com/questions/54899550/create-https-test-server-for-any-client
+		ts = httptest.NewUnstartedServer(http.HandlerFunc(fn))
+		ts.StartTLS()
+	}
+	return ts
+}
diff --git a/middleware_test.go b/middleware_test.go
index d585221..49733e9 100644
--- a/middleware_test.go
+++ b/middleware_test.go
@@ -182,7 +182,7 @@ func Test_parseRequestURL(t *testing.T) {
 		{
 			name: "using deprecated HostURL with relative path in request URL",
 			init: func(c *Client, r *Request) {
-				c.BaseURL = "https://example.com"
+				c.HostURL = "https://example.com"
 				r.URL = "foo/bar"
 			},
 			expectedURL: "https://example.com/foo/bar",
diff --git a/request_test.go b/request_test.go
index 01e0784..f220939 100644
--- a/request_test.go
+++ b/request_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
 // resty source code and usage is governed by a MIT style
 // license that can be found in the LICENSE file.
 
@@ -164,7 +164,7 @@ func TestGetRelativePath(t *testing.T) {
 	defer ts.Close()
 
 	c := dc()
-	c.SetBaseURL(ts.URL)
+	c.SetHostURL(ts.URL)
 
 	resp, err := c.R().Get("mypage2")
 
@@ -594,7 +594,7 @@ func TestRequestBasicAuth(t *testing.T) {
 	defer ts.Close()
 
 	c := dc()
-	c.SetBaseURL(ts.URL).
+	c.SetHostURL(ts.URL).
 		SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})
 
 	resp, err := c.R().
@@ -618,7 +618,7 @@ func TestRequestInsecureBasicAuth(t *testing.T) {
 	logger.l.SetOutput(&logBuf)
 
 	c := dc()
-	c.SetBaseURL(ts.URL)
+	c.SetHostURL(ts.URL)
 
 	resp, err := c.R().
 		SetBasicAuth("myuser", "basicauth").
@@ -1034,12 +1034,35 @@ func TestMultiPartMultipartFields(t *testing.T) {
 	assertEqual(t, true, strings.Contains(responseStr, "upload-file-2.json"))
 }
 
+func TestMultiPartCustomBoundary(t *testing.T) {
+	ts := createFormPostServer(t)
+	defer ts.Close()
+	defer cleanupFiles(".testdata/upload")
+
+	_, err := dclr().
+		SetMultipartFormData(map[string]string{"first_name": "Jeevanandam", "last_name": "M", "zip_code": "00001"}).
+		SetMultipartBoundary(`"my-custom-boundary"`).
+		SetBasicAuth("myuser", "mypass").
+		Post(ts.URL + "/profile")
+
+	assertEqual(t, "mime: invalid boundary character", err.Error())
+
+	resp, err := dclr().
+		SetMultipartFormData(map[string]string{"first_name": "Jeevanandam", "last_name": "M", "zip_code": "00001"}).
+		SetMultipartBoundary("my-custom-boundary").
+		Post(ts.URL + "/profile")
+
+	assertError(t, err)
+	assertEqual(t, http.StatusOK, resp.StatusCode())
+	assertEqual(t, "Success", resp.String())
+}
+
 func TestGetWithCookie(t *testing.T) {
 	ts := createGetServer(t)
 	defer ts.Close()
 
 	c := dcl()
-	c.SetBaseURL(ts.URL)
+	c.SetHostURL(ts.URL)
 	c.SetCookie(&http.Cookie{
 		Name:  "go-resty-1",
 		Value: "This is cookie 1 value",
@@ -1070,7 +1093,7 @@ func TestGetWithCookies(t *testing.T) {
 	defer ts.Close()
 
 	c := dc()
-	c.SetBaseURL(ts.URL).SetDebug(true)
+	c.SetHostURL(ts.URL).SetDebug(true)
 
 	tu, _ := url.Parse(ts.URL)
 	c.GetClient().Jar.SetCookies(tu, []*http.Cookie{
@@ -1369,7 +1392,7 @@ func TestIncorrectURL(t *testing.T) {
 	assertEqual(t, true, (strings.Contains(err.Error(), "parse //not.a.user@%66%6f%6f.com/just/a/path/also") ||
 		strings.Contains(err.Error(), "parse \"//not.a.user@%66%6f%6f.com/just/a/path/also\"")))
 
-	c.SetBaseURL("//not.a.user@%66%6f%6f.com")
+	c.SetHostURL("//not.a.user@%66%6f%6f.com")
 	_, err1 := c.R().Get("/just/a/path/also")
 	assertEqual(t, true, (strings.Contains(err1.Error(), "parse //not.a.user@%66%6f%6f.com/just/a/path/also") ||
 		strings.Contains(err1.Error(), "parse \"//not.a.user@%66%6f%6f.com/just/a/path/also\"")))
@@ -1765,7 +1788,7 @@ func TestRequestOverridesClientAuthorizationHeader(t *testing.T) {
 	c := dc()
 	c.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).
 		SetHeader("Authorization", "some token").
-		SetBaseURL(ts.URL + "/")
+		SetHostURL(ts.URL + "/")
 
 	resp, err := c.R().
 		SetHeader("Authorization", "Bearer 004DDB79-6801-4587-B976-F093E6AC44FF").
@@ -1929,7 +1952,7 @@ func TestTraceInfo(t *testing.T) {
 	serverAddr := ts.URL[strings.LastIndex(ts.URL, "/")+1:]
 
 	client := dc()
-	client.SetBaseURL(ts.URL).EnableTrace()
+	client.SetHostURL(ts.URL).EnableTrace()
 	for _, u := range []string{"/", "/json", "/long-text", "/long-json"} {
 		resp, err := client.R().Get(u)
 		assertNil(t, err)
@@ -1974,7 +1997,7 @@ func TestTraceInfoWithoutEnableTrace(t *testing.T) {
 	defer ts.Close()
 
 	client := dc()
-	client.SetBaseURL(ts.URL)
+	client.SetHostURL(ts.URL)
 	for _, u := range []string{"/", "/json", "/long-text", "/long-json"} {
 		resp, err := client.R().Get(u)
 		assertNil(t, err)
@@ -1992,7 +2015,7 @@ func TestTraceInfoWithoutEnableTrace(t *testing.T) {
 
 func TestTraceInfoOnTimeout(t *testing.T) {
 	client := dc()
-	client.SetBaseURL("http://resty-nowhere.local").EnableTrace()
+	client.SetHostURL("http://resty-nowhere.local").EnableTrace()
 
 	resp, err := client.R().Get("/")
 	assertNotNil(t, err)
diff --git a/resty_test.go b/resty_test.go
index c00ad1f..95ef0b5 100644
--- a/resty_test.go
+++ b/resty_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
 // resty source code and usage is governed by a MIT style
 // license that can be found in the LICENSE file.
 
@@ -809,6 +809,7 @@ func dclr() *Request {
 }
 
 func assertNil(t *testing.T, v interface{}) {
+	t.Helper()
 	if !isNil(v) {
 		t.Errorf("[%v] was expected to be nil", v)
 	}
@@ -841,6 +842,7 @@ func assertErrorIs(t *testing.T, e, g error) (r bool) {
 }
 
 func assertEqual(t *testing.T, e, g interface{}) (r bool) {
+	t.Helper()
 	if !equal(e, g) {
 		t.Errorf("Expected [%v], got [%v]", e, g)
 	}
diff --git a/retry_test.go b/retry_test.go
index 8d58cc1..84c12a4 100644
--- a/retry_test.go
+++ b/retry_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
 // resty source code and usage is governed by a MIT style
 // license that can be found in the LICENSE file.
 
diff --git a/util_test.go b/util_test.go
index 74cf4b1..6c030fd 100644
--- a/util_test.go
+++ b/util_test.go
@@ -1,4 +1,4 @@
-// Copyright (c) 2015-2023 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
+// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
 // resty source code and usage is governed by a MIT style
 // license that can be found in the LICENSE file.