New Upstream Release - golang-github-go-resty-resty

Ready changes

Summary

Merged new upstream version: 2.7.0 (was: 2.6.0).

Resulting package

Built on 2022-11-11T01:28 (took 6m22s)

The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:

apt install -t fresh-releases golang-github-go-resty-resty-dev

Lintian Result

Diff

diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..13b025b
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,41 @@
+name: CI
+
+on:
+  push:
+    branches:
+      - master
+    paths-ignore:
+      - '**.md'
+  pull_request:
+    branches:
+      - master
+    paths-ignore:
+      - '**.md'
+
+  # Allows you to run this workflow manually from the Actions tab
+  workflow_dispatch:
+
+jobs:
+  build:
+    name: Build
+    strategy:
+      matrix:
+        go: [ '1.17.x', '1.16.x' ]
+        os: [ ubuntu-latest ]
+
+    runs-on: ${{ matrix.os }}
+
+    steps:
+      - name: Checkout
+        uses: actions/checkout@v2
+
+      - name: Setup Go
+        uses: actions/setup-go@v2
+        with:
+          go-version: ${{ matrix.go }}
+
+      - name: Test
+        run: go test ./... -race -coverprofile=coverage.txt -covermode=atomic
+
+      - name: Coverage
+        run: bash <(curl -s https://codecov.io/bash)
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 583a039..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,21 +0,0 @@
-language: go
-
-os: linux
-
-go: # use travis ci resource effectively, keep always latest 2 versions and tip :) 
-  - 1.15.x
-  - 1.14.x
-  - tip
-
-install:
-  - go get -v -t ./...
-
-script:
-  - go test ./... -race -coverprofile=coverage.txt -covermode=atomic
-
-after_success:
-  - bash <(curl -s https://codecov.io/bash)
-
-jobs:
-  allow_failures:
-    - go: tip
diff --git a/BUILD.bazel b/BUILD.bazel
index 6c47cbb..03bb44c 100644
--- a/BUILD.bazel
+++ b/BUILD.bazel
@@ -1,36 +1,48 @@
-package(default_visibility = ["//visibility:private"])
-
-load("@bazel_gazelle//:def.bzl", "gazelle")
 load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+load("@bazel_gazelle//:def.bzl", "gazelle")
 
-gazelle(
-    name = "gazelle",
-    command = "fix",
-    prefix = "github.com/go-resty/resty/v2",
-)
+# gazelle:prefix github.com/go-resty/resty/v2
+# gazelle:go_naming_convention import_alias
+gazelle(name = "gazelle")
 
 go_library(
-    name = "go_default_library",
-    srcs = glob(
-        ["*.go"],
-        exclude = ["*_test.go"],
-    ),
+    name = "resty",
+    srcs = [
+        "client.go",
+        "middleware.go",
+        "redirect.go",
+        "request.go",
+        "response.go",
+        "resty.go",
+        "retry.go",
+        "trace.go",
+        "transport.go",
+        "transport112.go",
+        "util.go",
+    ],
     importpath = "github.com/go-resty/resty/v2",
     visibility = ["//visibility:public"],
     deps = ["@org_golang_x_net//publicsuffix:go_default_library"],
 )
 
 go_test(
-    name = "go_default_test",
-    srcs =
-        glob(
-            ["*_test.go"],
-            exclude = ["example_test.go"],
-        ),
-    data = glob([".testdata/*"]),
-    embed = [":go_default_library"],
-    importpath = "github.com/go-resty/resty/v2",
-    deps = [
-        "@org_golang_x_net//proxy:go_default_library",
+    name = "resty_test",
+    srcs = [
+        "client_test.go",
+        "context_test.go",
+        "example_test.go",
+        "request_test.go",
+        "resty_test.go",
+        "retry_test.go",
+        "util_test.go",
     ],
+    data = glob([".testdata/*"]),
+    embed = [":resty"],
+    deps = ["@org_golang_x_net//proxy:go_default_library"],
+)
+
+alias(
+    name = "go_default_library",
+    actual = ":resty",
+    visibility = ["//visibility:public"],
 )
diff --git a/README.md b/README.md
index 819fbf3..8ec6518 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://travis-ci.org/go-resty/resty"><img src="https://travis-ci.org/go-resty/resty.svg?branch=master" 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.6.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.7.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.6.0 [released](https://github.com/go-resty/resty/releases/tag/v2.6.0) and tagged on Apr 09, 2021.
+  * v2.7.0 [released](https://github.com/go-resty/resty/releases/tag/v2.7.0) and tagged on Nov 03, 2021.
   * 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).
@@ -36,6 +36,7 @@
         - Success scenario [Request.SetResult()](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.SetResult) and [Response.Result()](https://pkg.go.dev/github.com/go-resty/resty/v2#Response.Result).
         - Error scenario [Request.SetError()](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.SetError) and [Response.Error()](https://pkg.go.dev/github.com/go-resty/resty/v2#Response.Error).
         - Supports [RFC7807](https://tools.ietf.org/html/rfc7807) - `application/problem+json` & `application/problem+xml`
+    * Resty provides an option to override [JSON Marshal/Unmarshal and XML Marshal/Unmarshal](#override-json--xml-marshalunmarshal)
   * Easy to upload one or more file(s) via `multipart/form-data`
     * Auto detects file content type
   * Request URL [Path Params (aka URI Params)](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.SetPathParams)
@@ -107,7 +108,7 @@ Resty author also published following projects for Go Community.
 
 ```bash
 # Go Modules
-require github.com/go-resty/resty/v2 v2.4.0
+require github.com/go-resty/resty/v2 v2.7.0
 ```
 
 ## Usage
@@ -359,6 +360,24 @@ resp, err := client.R().
       Options("https://myapp.com/servers/nyc-dc-01")
 ```
 
+#### Override JSON & XML Marshal/Unmarshal
+
+User could register choice of JSON/XML library into resty or write your own. By default resty registers standard `encoding/json` and `encoding/xml` respectively.
+```go
+// Example of registering json-iterator
+import jsoniter "github.com/json-iterator/go"
+
+json := jsoniter.ConfigCompatibleWithStandardLibrary
+
+client := resty.New()
+client.JSONMarshal = json.Marshal
+client.JSONUnmarshal = json.Unmarshal
+
+// similarly user could do for XML too with -
+client.XMLMarshal
+client.XMLUnmarshal
+```
+
 ### Multipart File(s) upload
 
 #### Using io.Reader
@@ -829,13 +848,13 @@ client.SetTransport(&transport).SetScheme("http").SetHostURL(unixSocket)
 client.R().Get("/index.html")
 ```
 
-#### Bazel support
+#### Bazel Support
 
 Resty can be built, tested and depended upon via [Bazel](https://bazel.build).
 For example, to run all tests:
 
 ```shell
-bazel test :go_default_test
+bazel test :resty_test
 ```
 
 #### Mocking http requests using [httpmock](https://github.com/jarcoal/httpmock) library
@@ -876,7 +895,7 @@ BTW, I'd like to know what you think about `Resty`. Kindly open an issue or send
 
 ## Core Team
 
-Have a look on [Members](https://github.com/orgs/go-resty/teams/core/members) page.
+Have a look on [Members](https://github.com/orgs/go-resty/people) page.
 
 ## Contributors
 
diff --git a/WORKSPACE b/WORKSPACE
index 5459d63..9ef03e9 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -1,26 +1,30 @@
 workspace(name = "resty")
 
-git_repository(
+load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
+
+http_archive(
     name = "io_bazel_rules_go",
-    remote = "https://github.com/bazelbuild/rules_go.git",
-    tag = "0.13.0",
+    sha256 = "69de5c704a05ff37862f7e0f5534d4f479418afc21806c887db544a316f3cb6b",
+    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",
+    ],
 )
 
-git_repository(
+http_archive(
     name = "bazel_gazelle",
-    remote = "https://github.com/bazelbuild/bazel-gazelle.git",
-    tag = "0.13.0",
+    sha256 = "62ca106be173579c0a167deb23358fdfe71ffa1e4cfdddf5582af26520f1c66f",
+    urls = [
+        "https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.23.0/bazel-gazelle-v0.23.0.tar.gz",
+        "https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.23.0/bazel-gazelle-v0.23.0.tar.gz",
+    ],
 )
 
-load(
-    "@io_bazel_rules_go//go:def.bzl",
-    "go_rules_dependencies",
-    "go_register_toolchains",
-)
+load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
 
 go_rules_dependencies()
 
-go_register_toolchains()
+go_register_toolchains(version = "1.16")
 
 load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
 
diff --git a/client.go b/client.go
index 36b9f9c..1a03efa 100644
--- a/client.go
+++ b/client.go
@@ -10,6 +10,7 @@ import (
 	"crypto/tls"
 	"crypto/x509"
 	"encoding/json"
+	"encoding/xml"
 	"errors"
 	"fmt"
 	"io"
@@ -92,9 +93,11 @@ type (
 // Resty also provides an options to override most of the client settings
 // at request level.
 type Client struct {
-	HostURL               string
+	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
 	Header                http.Header
 	UserInfo              *User
 	Token                 string
@@ -112,6 +115,8 @@ type Client struct {
 	RetryAfter            RetryAfterFunc
 	JSONMarshal           func(v interface{}) ([]byte, error)
 	JSONUnmarshal         func(data []byte, v interface{}) error
+	XMLMarshal            func(v interface{}) ([]byte, error)
+	XMLUnmarshal          func(data []byte, v interface{}) error
 
 	// HeaderAuthorizationKey is used to set/access Request Authorization header
 	// value when `SetAuthToken` option is used.
@@ -125,7 +130,6 @@ type Client struct {
 	debugBodySizeLimit int64
 	outputDirectory    string
 	scheme             string
-	pathParams         map[string]string
 	log                Logger
 	httpClient         *http.Client
 	proxyURL           *url.URL
@@ -154,8 +158,25 @@ type User struct {
 //
 //		// 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.HostURL = strings.TrimRight(url, "/")
+	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
+//		// Setting HTTP address
+//		client.SetBaseURL("http://myjeeva.com")
+//
+//		// Setting HTTPS address
+//		client.SetBaseURL("https://myjeeva.com")
+//
+// Since v2.7.0
+func (c *Client) SetBaseURL(url string) *Client {
+	c.BaseURL = strings.TrimRight(url, "/")
+	c.HostURL = c.BaseURL
 	return c
 }
 
@@ -367,7 +388,7 @@ func (c *Client) R() *Request {
 		client:          c,
 		multipartFiles:  []*File{},
 		multipartFields: []*MultipartField{},
-		pathParams:      map[string]string{},
+		PathParams:      map[string]string{},
 		jsonEscapeHTML:  true,
 	}
 	return r
@@ -589,6 +610,9 @@ func (c *Client) SetRetryAfter(callback RetryAfterFunc) *Client {
 // AddRetryCondition method adds a retry condition function to array of functions
 // that are checked to determine if the request is retried. The request will
 // retry if any of the functions return true and error is nil.
+//
+// Note: These retry conditions are applied on all Request made using this Client.
+// For Request specific retry conditions check *Request.AddRetryCondition
 func (c *Client) AddRetryCondition(condition RetryConditionFunc) *Client {
 	c.RetryConditions = append(c.RetryConditions, condition)
 	return c
@@ -758,7 +782,7 @@ func (c *Client) SetTransport(transport http.RoundTripper) *Client {
 // 		client.SetScheme("http")
 func (c *Client) SetScheme(scheme string) *Client {
 	if !IsStringEmpty(scheme) {
-		c.scheme = scheme
+		c.scheme = strings.TrimSpace(scheme)
 	}
 	return c
 }
@@ -793,7 +817,7 @@ func (c *Client) SetDoNotParseResponse(parse bool) *Client {
 // Also it can be overridden at request level Path Params options,
 // see `Request.SetPathParam` or `Request.SetPathParams`.
 func (c *Client) SetPathParam(param, value string) *Client {
-	c.pathParams[param] = value
+	c.PathParams[param] = value
 	return c
 }
 
@@ -869,7 +893,6 @@ func (c *Client) GetClient() *http.Client {
 // Executes method executes the given `Request` object and returns response
 // error.
 func (c *Client) execute(req *Request) (*Response, error) {
-	defer releaseBuffer(req.bodyBuf)
 	// Apply Request middleware
 	var err error
 
@@ -903,6 +926,8 @@ func (c *Client) execute(req *Request) (*Response, error) {
 		return nil, wrapNoRetryErr(err)
 	}
 
+	req.RawRequest.Body = newRequestBodyReleaser(req.RawRequest.Body, req.bodyBuf)
+
 	req.Time = time.Now()
 	resp, err := c.httpClient.Do(req.RawRequest)
 
@@ -1052,13 +1077,16 @@ func createClient(hc *http.Client) *Client {
 		Cookies:                make([]*http.Cookie, 0),
 		RetryWaitTime:          defaultWaitTime,
 		RetryMaxWaitTime:       defaultMaxWaitTime,
+		PathParams:             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,
-		pathParams:             make(map[string]string),
+
+		jsonEscapeHTML:     true,
+		httpClient:         hc,
+		debugBodySizeLimit: math.MaxInt32,
 	}
 
 	// Logger
diff --git a/client_test.go b/client_test.go
index 790fcb4..84ae715 100644
--- a/client_test.go
+++ b/client_test.go
@@ -744,3 +744,46 @@ func TestResponseError(t *testing.T) {
 	assertNotNil(t, re.Unwrap())
 	assertEqual(t, err.Error(), re.Error())
 }
+
+func TestHostURLForGH318AndGH407(t *testing.T) {
+	ts := createPostServer(t)
+	defer ts.Close()
+
+	targetURL, _ := url.Parse(ts.URL)
+	t.Log("ts.URL:", ts.URL)
+	t.Log("targetURL.Host:", targetURL.Host)
+	// Sample output
+	// ts.URL: http://127.0.0.1:55967
+	// targetURL.Host: 127.0.0.1:55967
+
+	// Unable use the local http test server for this
+	// use case testing
+	//
+	// using `targetURL.Host` value or test case yield to ERROR
+	// "parse "127.0.0.1:55967": first path segment in URL cannot contain colon"
+
+	// test the functionality with httpbin.org locally
+	// will figure out later
+
+	c := dc()
+	// c.SetScheme("http")
+	// c.SetHostURL(targetURL.Host + "/")
+
+	// t.Log("with leading `/`")
+	// resp, err := c.R().Post("/login")
+	// assertNil(t, err)
+	// assertNotNil(t, resp)
+
+	// t.Log("\nwithout leading `/`")
+	// resp, err = c.R().Post("login")
+	// assertNil(t, err)
+	// assertNotNil(t, resp)
+
+	t.Log("with leading `/` on request & with trailing `/` on host url")
+	c.SetHostURL(ts.URL + "/")
+	resp, err := c.R().
+		SetBody(map[string]interface{}{"username": "testuser", "password": "testpass"}).
+		Post("/login")
+	assertNil(t, err)
+	assertNotNil(t, resp)
+}
diff --git a/debian/changelog b/debian/changelog
index ee6af1c..d01bd7b 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+golang-github-go-resty-resty (2.7.0-1) UNRELEASED; urgency=low
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Fri, 11 Nov 2022 01:23:04 -0000
+
 golang-github-go-resty-resty (2.6.0-1) unstable; urgency=medium
 
   * New upstream version.
diff --git a/go.mod b/go.mod
index 0383ef8..5e78bdc 100644
--- a/go.mod
+++ b/go.mod
@@ -1,5 +1,5 @@
 module github.com/go-resty/resty/v2
 
-require golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4
+require golang.org/x/net v0.0.0-20211029224645-99673261e6eb
 
 go 1.11
diff --git a/go.sum b/go.sum
index bba7211..07a4515 100644
--- a/go.sum
+++ b/go.sum
@@ -1,7 +1,7 @@
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 h1:4nGaVu0QrbjT/AK2PRLuQfQuh6DJve+pELhqTdAj3x0=
-golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM=
+golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/middleware.go b/middleware.go
index 06ed48b..0e8ac2b 100644
--- a/middleware.go
+++ b/middleware.go
@@ -6,7 +6,6 @@ package resty
 
 import (
 	"bytes"
-	"encoding/xml"
 	"errors"
 	"fmt"
 	"io"
@@ -29,13 +28,13 @@ const debugRequestLogKey = "__restyDebugRequestLog"
 
 func parseRequestURL(c *Client, r *Request) error {
 	// GitHub #103 Path Params
-	if len(r.pathParams) > 0 {
-		for p, v := range r.pathParams {
+	if len(r.PathParams) > 0 {
+		for p, v := range r.PathParams {
 			r.URL = strings.Replace(r.URL, "{"+p+"}", url.PathEscape(v), -1)
 		}
 	}
-	if len(c.pathParams) > 0 {
-		for p, v := range c.pathParams {
+	if len(c.PathParams) > 0 {
+		for p, v := range c.PathParams {
 			r.URL = strings.Replace(r.URL, "{"+p+"}", url.PathEscape(v), -1)
 		}
 	}
@@ -60,6 +59,11 @@ func parseRequestURL(c *Client, r *Request) error {
 		}
 	}
 
+	// GH #407 && #318
+	if reqURL.Scheme == "" && len(c.scheme) > 0 {
+		reqURL.Scheme = c.scheme
+	}
+
 	// Adding Query Param
 	query := make(url.Values)
 	for k, v := range c.QueryParam {
@@ -191,12 +195,6 @@ func createHTTPRequest(c *Client, r *Request) (err error) {
 		r.RawRequest.AddCookie(cookie)
 	}
 
-	// it's for non-http scheme option
-	if r.RawRequest.URL != nil && r.RawRequest.URL.Scheme == "" {
-		r.RawRequest.URL.Scheme = c.scheme
-		r.RawRequest.URL.Host = r.URL
-	}
-
 	// Enable trace
 	if c.trace || r.trace {
 		r.clientTrace = &clientTrace{}
@@ -458,12 +456,12 @@ func handleRequestBody(c *Client, r *Request) (err error) {
 		bodyBytes = []byte(s)
 	} else if IsJSONType(contentType) &&
 		(kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) {
-		bodyBytes, err = jsonMarshal(c, r, r.Body)
+		r.bodyBuf, err = jsonMarshal(c, r, r.Body)
 		if err != nil {
 			return
 		}
 	} else if IsXMLType(contentType) && (kind == reflect.Struct) {
-		bodyBytes, err = xml.Marshal(r.Body)
+		bodyBytes, err = c.XMLMarshal(r.Body)
 		if err != nil {
 			return
 		}
diff --git a/request.go b/request.go
index 41709cf..672df88 100644
--- a/request.go
+++ b/request.go
@@ -33,6 +33,7 @@ type Request struct {
 	AuthScheme string
 	QueryParam url.Values
 	FormData   url.Values
+	PathParams map[string]string
 	Header     http.Header
 	Time       time.Time
 	Body       interface{}
@@ -60,13 +61,13 @@ type Request struct {
 	fallbackContentType string
 	forceContentType    string
 	ctx                 context.Context
-	pathParams          map[string]string
 	values              map[string]interface{}
 	client              *Client
 	bodyBuf             *bytes.Buffer
 	clientTrace         *clientTrace
 	multipartFiles      []*File
 	multipartFields     []*MultipartField
+	retryConditions     []RetryConditionFunc
 }
 
 // Context method returns the Context if its already set in request
@@ -117,6 +118,22 @@ func (r *Request) SetHeaders(headers map[string]string) *Request {
 	return r
 }
 
+// SetHeaderMultiValues sets multiple headers fields and its values is list of strings at one go in the current request.
+//
+// For Example: To set `Accept` as `text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8`
+//
+// 		client.R().
+//			SetHeaderMultiValues(map[string][]string{
+//				"Accept": []string{"text/html", "application/xhtml+xml", "application/xml;q=0.9", "image/webp", "*/*;q=0.8"},
+//			})
+// Also you can override header value, which was set at client instance level.
+func (r *Request) SetHeaderMultiValues(headers map[string][]string) *Request {
+	for key, values := range headers {
+		r.SetHeader(key, strings.Join(values, ", "))
+	}
+	return r
+}
+
 // SetHeaderVerbatim method is to set a single header field and its value verbatim in the current request.
 //
 // For Example: To set `all_lowercase` and `UPPERCASE` as `available`.
@@ -494,7 +511,7 @@ func (r *Request) SetDoNotParseResponse(parse bool) *Request {
 // It replaces the value of the key while composing the request URL. Also you can
 // override Path Params value, which was set at client instance level.
 func (r *Request) SetPathParam(param, value string) *Request {
-	r.pathParams[param] = value
+	r.PathParams[param] = value
 	return r
 }
 
@@ -577,6 +594,18 @@ func (r *Request) SetCookies(rs []*http.Cookie) *Request {
 	return r
 }
 
+// AddRetryCondition method adds a retry condition function to the request's
+// array of functions that are checked to determine if the request is retried.
+// The request will retry if any of the functions return true and error is nil.
+//
+// Note: These retry conditions are checked before all retry conditions of the client.
+//
+// Since v2.7.0
+func (r *Request) AddRetryCondition(condition RetryConditionFunc) *Request {
+	r.retryConditions = append(r.retryConditions, condition)
+	return r
+}
+
 //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
 // HTTP request tracing
 //_______________________________________________________________________
@@ -747,7 +776,7 @@ func (r *Request) Execute(method, url string) (*Response, error) {
 		Retries(r.client.RetryCount),
 		WaitTime(r.client.RetryWaitTime),
 		MaxWaitTime(r.client.RetryMaxWaitTime),
-		RetryConditions(r.client.RetryConditions),
+		RetryConditions(append(r.retryConditions, r.client.RetryConditions...)),
 		RetryHooks(r.client.RetryHooks),
 	)
 
@@ -854,11 +883,14 @@ func (r *Request) initValuesMap() {
 	}
 }
 
-var noescapeJSONMarshal = func(v interface{}) ([]byte, error) {
+var noescapeJSONMarshal = func(v interface{}) (*bytes.Buffer, error) {
 	buf := acquireBuffer()
-	defer releaseBuffer(buf)
 	encoder := json.NewEncoder(buf)
 	encoder.SetEscapeHTML(false)
-	err := encoder.Encode(v)
-	return buf.Bytes(), err
+	if err := encoder.Encode(v); err != nil {
+		releaseBuffer(buf)
+		return nil, err
+	}
+
+	return buf, nil
 }
diff --git a/request_test.go b/request_test.go
index 368463a..3d0dc26 100644
--- a/request_test.go
+++ b/request_test.go
@@ -1371,6 +1371,19 @@ func TestSetHeaderVerbatim(t *testing.T) {
 	assertEqual(t, "value_standard", r.Header.Get("Header-Lowercase"))
 }
 
+func TestSetHeaderMultipleValue(t *testing.T) {
+	ts := createPostServer(t)
+	defer ts.Close()
+
+	r := dclr().
+		SetHeaderMultiValues(map[string][]string{
+			"Content":       {"text/*", "text/html", "*"},
+			"Authorization": {"Bearer xyz"},
+		})
+	assertEqual(t, "text/*, text/html, *", r.Header.Get("content"))
+	assertEqual(t, "Bearer xyz", r.Header.Get("authorization"))
+}
+
 func TestOutputFileWithBaseDirAndRelativePath(t *testing.T) {
 	ts := createGetServer(t)
 	defer ts.Close()
diff --git a/resty.go b/resty.go
index 2a53e06..6f9c8b4 100644
--- a/resty.go
+++ b/resty.go
@@ -14,7 +14,7 @@ import (
 )
 
 // Version # of resty
-const Version = "2.6.0"
+const Version = "2.7.0"
 
 // New method creates a new Resty client.
 func New() *Client {
diff --git a/retry.go b/retry.go
index a841c46..00b8514 100644
--- a/retry.go
+++ b/retry.go
@@ -129,6 +129,12 @@ func Backoff(operation func() (*Response, error), options ...Option) error {
 			hook(resp, err)
 		}
 
+		// Don't need to wait when no retries left.
+		// Still run retry hooks even on last retry to keep compatibility.
+		if attempt == opts.maxRetries {
+			return err
+		}
+
 		waitTime, err2 := sleepDuration(resp, opts.waitTime, opts.maxWaitTime, attempt)
 		if err2 != nil {
 			if err == nil {
diff --git a/retry_test.go b/retry_test.go
index e302216..9f8fb38 100644
--- a/retry_test.go
+++ b/retry_test.go
@@ -32,6 +32,39 @@ func TestBackoffSuccess(t *testing.T) {
 	assertEqual(t, externalCounter, attempts)
 }
 
+func TestBackoffNoWaitForLastRetry(t *testing.T) {
+	attempts := 1
+	externalCounter := 0
+	numRetries := 1
+
+	canceledCtx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+
+	resp := &Response{
+		Request: &Request{
+			ctx: canceledCtx,
+			client: &Client{
+				RetryAfter: func(*Client, *Response) (time.Duration, error) {
+					return 6, nil
+				},
+			},
+		},
+	}
+
+	retryErr := Backoff(func() (*Response, error) {
+		externalCounter++
+		return resp, nil
+	}, RetryConditions([]RetryConditionFunc{func(response *Response, err error) bool {
+		if externalCounter == attempts + numRetries {
+			// Backoff returns context canceled if goes to sleep after last retry.
+			cancel()
+		}
+		return true
+	}}), Retries(numRetries))
+
+	assertNil(t, retryErr)
+}
+
 func TestBackoffTenAttemptsSuccess(t *testing.T) {
 	attempts := 10
 	externalCounter := 0
@@ -169,8 +202,8 @@ func TestClientRetryGet(t *testing.T) {
 	assertNotNil(t, resp.Body())
 	assertEqual(t, 0, len(resp.Header()))
 
-	assertEqual(t, true, (strings.HasPrefix(err.Error(), "Get "+ts.URL+"/set-retrycount-test") ||
-		strings.HasPrefix(err.Error(), "Get \""+ts.URL+"/set-retrycount-test\"")))
+	assertEqual(t, true, strings.HasPrefix(err.Error(), "Get "+ts.URL+"/set-retrycount-test") ||
+		strings.HasPrefix(err.Error(), "Get \""+ts.URL+"/set-retrycount-test\""))
 }
 
 func TestClientRetryWait(t *testing.T) {
@@ -639,8 +672,8 @@ func TestClientRetryCount(t *testing.T) {
 	// 2 attempts were made
 	assertEqual(t, attempt, 2)
 
-	assertEqual(t, true, (strings.HasPrefix(err.Error(), "Get "+ts.URL+"/set-retrycount-test") ||
-		strings.HasPrefix(err.Error(), "Get \""+ts.URL+"/set-retrycount-test\"")))
+	assertEqual(t, true, strings.HasPrefix(err.Error(), "Get "+ts.URL+"/set-retrycount-test") ||
+		strings.HasPrefix(err.Error(), "Get \""+ts.URL+"/set-retrycount-test\""))
 }
 
 func TestClientErrorRetry(t *testing.T) {
@@ -693,8 +726,8 @@ func TestClientRetryHook(t *testing.T) {
 
 	assertEqual(t, 3, attempt)
 
-	assertEqual(t, true, (strings.HasPrefix(err.Error(), "Get "+ts.URL+"/set-retrycount-test") ||
-		strings.HasPrefix(err.Error(), "Get \""+ts.URL+"/set-retrycount-test\"")))
+	assertEqual(t, true, strings.HasPrefix(err.Error(), "Get "+ts.URL+"/set-retrycount-test") ||
+		strings.HasPrefix(err.Error(), "Get \""+ts.URL+"/set-retrycount-test\""))
 }
 
 func filler(*Response, error) bool {
diff --git a/util.go b/util.go
index b017256..1d563be 100644
--- a/util.go
+++ b/util.go
@@ -6,7 +6,6 @@ package resty
 
 import (
 	"bytes"
-	"encoding/xml"
 	"fmt"
 	"io"
 	"log"
@@ -19,6 +18,7 @@ import (
 	"runtime"
 	"sort"
 	"strings"
+	"sync"
 )
 
 //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
@@ -108,7 +108,7 @@ func Unmarshalc(c *Client, ct string, b []byte, d interface{}) (err error) {
 	if IsJSONType(ct) {
 		err = c.JSONUnmarshal(b, d)
 	} else if IsXMLType(ct) {
-		err = xml.Unmarshal(b, d)
+		err = c.XMLUnmarshal(b, d)
 	}
 
 	return
@@ -139,13 +139,19 @@ type ResponseLog struct {
 //_______________________________________________________________________
 
 // way to disable the HTML escape as opt-in
-func jsonMarshal(c *Client, r *Request, d interface{}) ([]byte, error) {
-	if !r.jsonEscapeHTML {
-		return noescapeJSONMarshal(d)
-	} else if !c.jsonEscapeHTML {
+func jsonMarshal(c *Client, r *Request, d interface{}) (*bytes.Buffer, error) {
+	if !r.jsonEscapeHTML || !c.jsonEscapeHTML {
 		return noescapeJSONMarshal(d)
 	}
-	return c.JSONMarshal(d)
+
+	data, err := c.JSONMarshal(d)
+	if err != nil {
+		return nil, err
+	}
+
+	buf := acquireBuffer()
+	_, _ = buf.Write(data)
+	return buf, nil
 }
 
 func firstNonEmpty(v ...string) string {
@@ -195,7 +201,7 @@ func writeMultipartFormFile(w *multipart.Writer, fieldName, fileName string, r i
 	// Auto detect actual multipart content type
 	cbuf := make([]byte, 512)
 	size, err := r.Read(cbuf)
-	if err != nil {
+	if err != nil && err != io.EOF {
 		return err
 	}
 
@@ -283,6 +289,34 @@ func releaseBuffer(buf *bytes.Buffer) {
 	}
 }
 
+// requestBodyReleaser wraps requests's body and implements custom Close for it.
+// The Close method closes original body and releases request body back to sync.Pool.
+type requestBodyReleaser struct {
+	releaseOnce sync.Once
+	reqBuf      *bytes.Buffer
+	io.ReadCloser
+}
+
+func newRequestBodyReleaser(respBody io.ReadCloser, reqBuf *bytes.Buffer) io.ReadCloser {
+	if reqBuf == nil {
+		return respBody
+	}
+
+	return &requestBodyReleaser{
+		reqBuf:     reqBuf,
+		ReadCloser: respBody,
+	}
+}
+
+func (rr *requestBodyReleaser) Close() error {
+	err := rr.ReadCloser.Close()
+	rr.releaseOnce.Do(func() {
+		releaseBuffer(rr.reqBuf)
+	})
+
+	return err
+}
+
 func closeq(v interface{}) {
 	if c, ok := v.(io.Closer); ok {
 		silently(c.Close())
diff --git a/util_test.go b/util_test.go
index e5690d4..ef2bb91 100644
--- a/util_test.go
+++ b/util_test.go
@@ -5,6 +5,8 @@
 package resty
 
 import (
+	"bytes"
+	"mime/multipart"
 	"testing"
 )
 
@@ -79,3 +81,17 @@ func TestIsXMLType(t *testing.T) {
 		}
 	}
 }
+
+func TestWriteMultipartFormFileReaderEmpty(t *testing.T) {
+	w := multipart.NewWriter(bytes.NewBuffer(nil))
+	defer func() { _ = w.Close() }()
+	if err := writeMultipartFormFile(w, "foo", "bar", bytes.NewReader(nil)); err != nil {
+		t.Errorf("Got unexpected error: %v", err)
+	}
+}
+
+func TestWriteMultipartFormFileReaderError(t *testing.T) {
+	err := writeMultipartFormFile(nil, "", "", &brokenReadCloser{})
+	assertNotNil(t, err)
+	assertEqual(t, "read error", err.Error())
+}

Debdiff

File lists identical (after any substitutions)

No differences were encountered in the control files

More details

Full run details