[
  {
    "path": ".github/FUNDING.yml",
    "content": "github: [jeevatkm]\ncustom: [\"https://www.paypal.com/donate/?cmd=_donations&business=QWMZG74FW4QYC&lc=US&item_name=Resty+Library+for+Go&currency_code=USD\"]\n"
  },
  {
    "path": ".github/workflows/ci.yml",
    "content": "name: CI\n\non:\n  push:\n    branches:\n      - v3\n      - v2\n    paths-ignore:\n      - '**.md'\n      - '**.bazel'\n      - 'WORKSPACE'\n  pull_request:\n    branches:\n      - main\n      - v3\n      - v2\n    paths-ignore:\n      - '**.md'\n      - '**.bazel'\n      - 'WORKSPACE'\n\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\njobs:\n  build:\n    name: Build\n    strategy:\n      matrix:\n        go: [ 'stable', '1.23.x' ]\n        os: [ ubuntu-latest ]\n\n    runs-on: ${{ matrix.os }}\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ matrix.go }}\n          cache: true\n          cache-dependency-path: go.sum\n\n      - name: Format\n        run: diff -u <(echo -n) <(go fmt $(go list ./...))\n\n      - name: Test\n        run: go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=1 -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... -shuffle=on\n\n      - name: Upload coverage to Codecov\n        if: ${{ matrix.os == 'ubuntu-latest' && matrix.go == 'stable' }}\n        uses: codecov/codecov-action@v4\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          file: ./coverage.txt\n          flags: unittests\n"
  },
  {
    "path": ".github/workflows/label-actions.yml",
    "content": "name: 'Label'\n\non:\n  pull_request:\n    types: [labeled]\n    paths-ignore:\n      - '**.md'\n      - '**.bazel'\n      - 'WORKSPACE'\n\njobs:\n  build:\n    strategy:\n      matrix:\n        go: [ 'stable', '1.23.x' ]\n        os: [ ubuntu-latest ]\n\n    name: Run Build\n    if: ${{ github.event.label.name == 'run-build' }}\n    runs-on: ${{ matrix.os }}\n\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n        with:\n          fetch-depth: 0\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: ${{ matrix.go }}\n          cache: true\n          cache-dependency-path: go.sum\n\n      - name: Format\n        run: diff -u <(echo -n) <(go fmt $(go list ./...))\n\n      - name: Test\n        run: go run gotest.tools/gotestsum@latest -f testname -- ./... -race -count=1 -coverprofile=coverage.txt -covermode=atomic -coverpkg=./... -shuffle=on\n\n      - name: Upload coverage to Codecov\n        if: ${{ matrix.os == 'ubuntu-latest' && matrix.go == 'stable' }}\n        uses: codecov/codecov-action@v4\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          file: ./coverage.txt\n          flags: unittests\n"
  },
  {
    "path": ".gitignore",
    "content": "# Compiled Object files, Static and Dynamic libs (Shared Objects)\n*.o\n*.a\n*.so\n\n# Folders\n_obj\n_test\n\n# Architecture specific extensions/prefixes\n*.[568vq]\n[568vq].out\n\n*.cgo1.go\n*.cgo2.c\n_cgo_defun.c\n_cgo_gotypes.go\n_cgo_export.*\n\n_testmain.go\n\n*.exe\n*.test\n*.prof\n\ncoverage.out\ncoverage.txt\n\n# Exclude IDE folders\n.idea/*\n.vscode/*\n"
  },
  {
    "path": ".testdata/cert.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIC+jCCAeKgAwIBAgIRAJce5ewsoW44j0qvSABmq7owDQYJKoZIhvcNAQELBQAw\nEjEQMA4GA1UEChMHQWNtZSBDbzAeFw0yNTAxMDQwNzA3MTNaFw0yNjAxMDQwNzA3\nMTNaMBIxEDAOBgNVBAoTB0FjbWUgQ28wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw\nggEKAoIBAQCYkTN1g/0Z3KkS3w0lX9yhZkwiA0obXCeFs7hpRP0p4WlW3uADyXQ5\nh2MaYx8OCA7oGU7/dWOPhtE3rgFEz7IwLxcP5d02ukLGlFD69D6KLyTXwCFmvOWQ\n5fbOq4s73WTNDfYSTYNzeujDCjeu/Bk0OVhdxbyZdyrpdm+UBfH8uIDoGeCRXnji\nnqG9HNOQx6r/S6FqC5j/7PrVl1i66WlqRzKEJB94uejfujrHq8RjQm/wzEutU5df\nC39zEEEx75qQt7Jc0asm1AqAKSq34xn4rVajWrBZ/WudUUizHfaBDP61uPFvPyKW\nJDvTSdeoM9TPX0y0cjo6AwSrdLl7flrRAgMBAAGjSzBJMA4GA1UdDwEB/wQEAwIF\noDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuC\nCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAdHvPQe3EJ4/X6K/bklJUhIfM\nKBauH8VMBfri7xLawleKssm7GdiFivSA0g1pArkl8SALBlPqhrx7rwlyyivLTZaR\nVFvXaQ9eU0zGnSnDnKVz6CX/zn3TKfcgZPEBclayh0ldm7A8xSJWaWbRZ+s9e9x1\nXcQTn2KkMZfBDMnGEWQ3KZrClvO5ZfkqSiyzEm9+eF0m0E7ujTyfSVMsPdyldA6U\npHG8omQTyOzJl2I4z7DlS0AEsL0TJHV4iKr9rDei2xQz/wtful5qU/taYp2Y6zMH\n8ytnDldJhmcCwmvtqvK5p6CbkatE7TFyw2CxQJHnQef+Y4W94sSZWg9CGRKDIQ==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": ".testdata/key.pem",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCYkTN1g/0Z3KkS\n3w0lX9yhZkwiA0obXCeFs7hpRP0p4WlW3uADyXQ5h2MaYx8OCA7oGU7/dWOPhtE3\nrgFEz7IwLxcP5d02ukLGlFD69D6KLyTXwCFmvOWQ5fbOq4s73WTNDfYSTYNzeujD\nCjeu/Bk0OVhdxbyZdyrpdm+UBfH8uIDoGeCRXnjinqG9HNOQx6r/S6FqC5j/7PrV\nl1i66WlqRzKEJB94uejfujrHq8RjQm/wzEutU5dfC39zEEEx75qQt7Jc0asm1AqA\nKSq34xn4rVajWrBZ/WudUUizHfaBDP61uPFvPyKWJDvTSdeoM9TPX0y0cjo6AwSr\ndLl7flrRAgMBAAECggEAJPTPNUEilxgncGXNZmdBJ2uDN536XoRFIpL1MbK/bFyo\nyp00QFaVK7ZK4EJwbFKxYbF3vFOwKT0sAsPIlOWGsTtG59fzbOVTdYzJzPBLEef3\nkbd9n8hUB3RdA5T0Ji0r1Kv0FlzmYZu9NDmOYXm5lTfq2tQiKj5+i4zf3EhQZLng\n4wVxBT7yQUQcstJv5K1L6HVzunSYtbHx8ZVxmw+tJ4lMCK23KPlvncZZTT8chWdT\n3GOp5nYIHk9E5jQnBnj7p73sxZUCZlb8uhLtdcgAXc4scptEVO+7n5zOaXIv40Oz\nyfkESgHcZWAMDvnkxdySHlD38Z2LIKDGbqR6O9wcwQKBgQDBO6fFPXO41nsxdVCB\nnhCgL2hsGjaxJzGBLaOJNVMMFRASN3Yqvs4N1Hn7lawRI/FRRffxjLkZfNGEBSF2\nOipdvX19Oe2hCZxvwHPoe5sb/Dh6KE7If1hRLOCXg/8E7ADBtAp94dam1WF4Kh6N\nVa6+n2YKif2rqye1YtRoUU46iQKBgQDKH/eMcMRUe9IySxHLogidOUwa0X7WrxF/\nPkXGpPbHQtMOJF5cVzh+L+foUKXNM60lgmCH0438GKU7kirC/dVtD/bwE598/XFZ\nvnjPV7Adf9vBz9NN8cS/4uEfQYbvTRmrnrQK+ZhOe8hmwjapxqdWrVHNUtvx18vL\nqBwR4YjsCQKBgCycMx1MFJ1FludSKCXkcf4pM7hRTPMVE065VJnmn6eYbT9nYnZ3\n2mZC+W5lnXXPkHSs7JLtZAZIVK5f6Nu8je9aQdBZQUz+RQlfquKvNp39WqSJDbcn\n/yGudKNGK+fc/Ee74vgw3Tdi57+wKaGDeHY1on8oYFHzj5VGnbb/nknRAoGBAK2Z\nhyQ4NmfZcU+A6mfbY0qmS5c9F5OMCZsgAQ374XiDDIK4+dKVlw/KVYRSwBTerXfp\n4r7GFMzQ3hmsEM4o9YYWkCDiubjAdPp/fYOX7MtpZXWw6euoGzQzyObvgNVHgyTD\nyh8jAI1oA1c+t3RaCp+HfRq8b+vnTEI+wN0auF8BAoGBAJmw+GgHCZGpw2XPNu+X\n8kuVGbQYAjTOXhBM4WzZyhfH1TWKLGn7C9YixhE2AW0UWKDvy+6OqPhe8q3KVms3\n8YZ1W+vbUNEZNGE0XrB5ZMXfePiqisCz0jgP9OAuT+ii4aI3MAm3zgCEC6UTMvLq\ngNBu3Tcy6udxnUf7czzJDRtE\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": ".testdata/sample-root.pem",
    "content": "-----BEGIN CERTIFICATE-----\nMIIEBDCCAuygAwIBAgIDAjppMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT\nMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i\nYWwgQ0EwHhcNMTMwNDA1MTUxNTU1WhcNMTUwNDA0MTUxNTU1WjBJMQswCQYDVQQG\nEwJVUzETMBEGA1UEChMKR29vZ2xlIEluYzElMCMGA1UEAxMcR29vZ2xlIEludGVy\nbmV0IEF1dGhvcml0eSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAJwqBHdc2FCROgajguDYUEi8iT/xGXAaiEZ+4I/F8YnOIe5a/mENtzJEiaB0C1NP\nVaTOgmKV7utZX8bhBYASxF6UP7xbSDj0U/ck5vuR6RXEz/RTDfRK/J9U3n2+oGtv\nh8DQUB8oMANA2ghzUWx//zo8pzcGjr1LEQTrfSTe5vn8MXH7lNVg8y5Kr0LSy+rE\nahqyzFPdFUuLH8gZYR/Nnag+YyuENWllhMgZxUYi+FOVvuOAShDGKuy6lyARxzmZ\nEASg8GF6lSWMTlJ14rbtCMoU/M4iarNOz0YDl5cDfsCx3nuvRTPPuj5xt970JSXC\nDTWJnZ37DhF5iR43xa+OcmkCAwEAAaOB+zCB+DAfBgNVHSMEGDAWgBTAephojYn7\nqwVkDBF9qn1luMrMTjAdBgNVHQ4EFgQUSt0GFhu89mi1dvWBtrtiGrpagS8wEgYD\nVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAQYwOgYDVR0fBDMwMTAvoC2g\nK4YpaHR0cDovL2NybC5nZW90cnVzdC5jb20vY3Jscy9ndGdsb2JhbC5jcmwwPQYI\nKwYBBQUHAQEEMTAvMC0GCCsGAQUFBzABhiFodHRwOi8vZ3RnbG9iYWwtb2NzcC5n\nZW90cnVzdC5jb20wFwYDVR0gBBAwDjAMBgorBgEEAdZ5AgUBMA0GCSqGSIb3DQEB\nBQUAA4IBAQA21waAESetKhSbOHezI6B1WLuxfoNCunLaHtiONgaX4PCVOzf9G0JY\n/iLIa704XtE7JW4S615ndkZAkNoUyHgN7ZVm2o6Gb4ChulYylYbc3GrKBIxbf/a/\nzG+FA1jDaFETzf3I93k9mTXwVqO94FntT0QJo544evZG0R0SnU++0ED8Vf4GXjza\nHFa9llF7b1cq26KqltyMdMKVvvBulRP/F/A8rLIQjcxz++iPAsbw+zOzlTvjwsto\nWHPbqCRiOwY1nQ2pM714A5AuTHhdUDqB1O6gyHA43LL5Z/qHQF1hwFGPa4NrzQU6\nyuGnBXj8ytqU0CwIPX4WecigUCAkVDNx\n-----END CERTIFICATE-----\n"
  },
  {
    "path": ".testdata/text-file.txt",
    "content": " THIS IS TEXT FILE FOR MULTIPART UPLOAD TEST :)\n\n- go-resty\n"
  },
  {
    "path": "BUILD.bazel",
    "content": "load(\"@bazel_gazelle//:def.bzl\", \"gazelle\")\nload(\"@io_bazel_rules_go//go:def.bzl\", \"go_library\", \"go_test\")\n\n# gazelle:prefix resty.dev/v3\n# gazelle:go_naming_convention import_alias\ngazelle(name = \"gazelle\")\n\ngo_library(\n    name = \"resty\",\n    srcs = [\n        \"circuit_breaker.go\",\n        \"client.go\",\n        \"curl.go\",\n        \"debug.go\",\n        \"digest.go\",\n        \"hedging.go\",\n        \"load_balancer.go\",\n        \"middleware.go\",\n        \"multipart.go\",\n        \"redirect.go\",\n        \"request.go\",\n        \"response.go\",\n        \"resty.go\",\n        \"retry.go\",\n        \"sse.go\",\n        \"stream.go\",\n        \"trace.go\",\n        \"transport_dial.go\",\n        \"transport_dial_wasm.go\",\n        \"util.go\",\n    ],\n    importpath = \"resty.dev/v3\",\n    visibility = [\"//visibility:public\"],\n    deps = [\"@org_golang_x_net//publicsuffix:go_default_library\"],\n)\n\ngo_test(\n    name = \"resty_test\",\n    srcs = [\n        \"benchmark_test.go\",\n        \"cert_watcher_test.go\",\n        \"client_test.go\",\n        \"context_test.go\",\n        \"curl_test.go\",\n        \"digest_test.go\",\n        \"hedging_test.go\",\n        \"load_balancer_test.go\",\n        \"middleware_test.go\",\n        \"multipart_test.go\",\n        \"request_test.go\",\n        \"resty_test.go\",\n        \"retry_test.go\",\n        \"sse_test.go\",\n        \"util_test.go\",\n    ],\n    data = glob([\".testdata/*\"]),\n    embed = [\":resty\"],\n)\n\nalias(\n    name = \"go_default_library\",\n    actual = \":resty\",\n    visibility = [\"//visibility:public\"],\n)\n"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015-present Jeevanandam M., https://myjeeva.com <jeeva@myjeeva.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "<p align=\"center\">\n  <img src=\"https://resty.dev/svg/resty-logo.svg\" width=\"175\" alt=\"Resty Logo\">\n</p>\n<p align=\"center\"><strong>Simple HTTP, REST, and SSE client library for Go</strong></p>\n\n<p align=\"center\" style=\"margin-top:3rem\"><a href=\"https://github.com/go-resty/resty/actions/workflows/ci.yml?query=branch%3Av3\" target=\"_blank\"><img src=\"https://github.com/go-resty/resty/actions/workflows/ci.yml/badge.svg?branch=v3\" alt=\"Resty Build Status\">\n</a><a href=\"https://app.codecov.io/gh/go-resty/resty/tree/v3\" target=\"_blank\"><img src=\"https://codecov.io/gh/go-resty/resty/branch/v3/graph/badge.svg\" alt=\"Resty Code Coverage\">\n</a><a href=\"https://goreportcard.com/report/resty.dev/v3\" target=\"_blank\"><img src=\"https://goreportcard.com/badge/resty.dev/v3\" alt=\"Go Report Card\">\n</a><a href=\"https://pkg.go.dev/resty.dev/v3\" target=\"_blank\"><img src=\"https://pkg.go.dev/badge/resty.dev/v3\" alt=\"Resty GoDoc\">\n</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\" target=\"_blank\"><img src=\"https://awesome.re/mentioned-badge.svg\" alt=\"Mentioned in Awesome Go\"></a></p>\n<p align=\"center\" style=\"margin-bottom:1rem\"><a href=\"https://app.fossa.com/projects/git%2Bgithub.com%2Fgo-resty%2Fresty?ref=badge_shield&amp;issueType=license\" alt=\"FOSSA Status\"><img src=\"https://app.fossa.com/api/projects/git%2Bgithub.com%2Fgo-resty%2Fresty.svg?type=shield&amp;issueType=license\"></a>\n<a href=\"https://app.fossa.com/projects/git%2Bgithub.com%2Fgo-resty%2Fresty?ref=badge_shield&amp;issueType=security\" alt=\"FOSSA Status\"><img src=\"https://app.fossa.com/api/projects/git%2Bgithub.com%2Fgo-resty%2Fresty.svg?type=shield&amp;issueType=security\"></a></p>\n\n\n## Documentation\n\nGo to https://resty.dev and refer to godoc.\n\n## Minimum Go Version\n\nUse `go1.23` and above.\n\n## Support & Donate\n\n* Sponsor via [GitHub](https://github.com/sponsors/jeevatkm)\n* Donate via [PayPal](https://www.paypal.com/donate/?cmd=_donations&business=QWMZG74FW4QYC&lc=US&item_name=Resty+Library+for+Go&currency_code=USD)\n\n## Versioning\n\nResty releases versions according to [Semantic Versioning](http://semver.org)\n\n  * Resty v3 provides Go Vanity URL `resty.dev/v3`.\n  * Resty v2 migrated away from `gopkg.in` service, `github.com/go-resty/resty/v2`.\n  * Resty fully adapted to `go mod` capabilities since `v1.10.0` release.\n  * Resty v1 series was using `gopkg.in` to provide versioning. `gopkg.in/resty.vX` points to appropriate tagged versions; `X` denotes version series number and it's a stable release for production use. For e.g. `gopkg.in/resty.v0`.\n\n## Contribution\n\nI would welcome your contribution!\n\n* If you find any improvement or issue you want to fix, feel free to send a pull request.\n* The pull requests must include test cases for feature/fix/enhancement with patch coverage of 100%.\n* I have done my best to bring pretty good coverage. I would request contributors to do the same for their contribution.\n\nI always look forward to hearing feedback, appreciation, and real-world usage stories from Resty users on [GitHub Discussions](https://github.com/go-resty/resty/discussions). It means a lot to me.\n\n## Creator\n\n[Jeevanandam M.](https://github.com/jeevatkm) (jeeva@myjeeva.com)\n\n\n## Contributors\n\nHave a look on [Contributors](https://github.com/go-resty/resty/graphs/contributors) page.\n\n## License Info\n\nResty released under MIT [LICENSE](LICENSE).\n\nResty [Documentation](https://github.com/go-resty/docs) and website released under Apache-2.0 [LICENSE](https://github.com/go-resty/docs/blob/main/LICENSE).\n"
  },
  {
    "path": "WORKSPACE",
    "content": "workspace(name = \"resty\")\n\nload(\"@bazel_tools//tools/build_defs/repo:http.bzl\", \"http_archive\")\n\nhttp_archive(\n    name = \"io_bazel_rules_go\",\n    sha256 = \"80a98277ad1311dacd837f9b16db62887702e9f1d1c4c9f796d0121a46c8e184\",\n    urls = [\n        \"https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.46.0/rules_go-v0.46.0.zip\",\n        \"https://github.com/bazelbuild/rules_go/releases/download/v0.46.0/rules_go-v0.46.0.zip\",\n    ],\n)\n\nhttp_archive(\n    name = \"bazel_gazelle\",\n    sha256 = \"62ca106be173579c0a167deb23358fdfe71ffa1e4cfdddf5582af26520f1c66f\",\n    urls = [\n        \"https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.23.0/bazel-gazelle-v0.23.0.tar.gz\",\n        \"https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.23.0/bazel-gazelle-v0.23.0.tar.gz\",\n    ],\n)\n\nload(\"@io_bazel_rules_go//go:deps.bzl\", \"go_register_toolchains\", \"go_rules_dependencies\")\n\ngo_rules_dependencies()\n\ngo_register_toolchains(version = \"1.21\")\n\nload(\"@bazel_gazelle//:deps.bzl\", \"gazelle_dependencies\")\n\ngazelle_dependencies()\n"
  },
  {
    "path": "benchmark_test.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bytes\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc Benchmark_parseRequestURL_PathParams(b *testing.B) {\n\tc := New().SetPathParams(map[string]string{\n\t\t\"foo\": \"1\",\n\t\t\"bar\": \"2\",\n\t}).SetPathRawParams(map[string]string{\n\t\t\"foo\": \"3\",\n\t\t\"xyz\": \"4\",\n\t})\n\tr := c.R().SetPathParams(map[string]string{\n\t\t\"foo\": \"5\",\n\t\t\"qwe\": \"6\",\n\t}).SetPathRawParams(map[string]string{\n\t\t\"foo\": \"7\",\n\t\t\"asd\": \"8\",\n\t})\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tr.URL = \"https://example.com/{foo}/{bar}/{xyz}/{qwe}/{asd}\"\n\t\tif err := parseRequestURL(c, r); err != nil {\n\t\t\tb.Errorf(\"parseRequestURL() error = %v\", err)\n\t\t}\n\t}\n}\n\nfunc Benchmark_parseRequestURL_QueryParams(b *testing.B) {\n\tc := New().SetQueryParams(map[string]string{\n\t\t\"foo\": \"1\",\n\t\t\"bar\": \"2\",\n\t})\n\tr := c.R().SetQueryParams(map[string]string{\n\t\t\"foo\": \"5\",\n\t\t\"qwe\": \"6\",\n\t})\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tr.URL = \"https://example.com/\"\n\t\tif err := parseRequestURL(c, r); err != nil {\n\t\t\tb.Errorf(\"parseRequestURL() error = %v\", err)\n\t\t}\n\t}\n}\n\nfunc Benchmark_parseRequestHeader(b *testing.B) {\n\tc := New()\n\tr := c.R()\n\tc.SetHeaders(map[string]string{\n\t\t\"foo\": \"1\", // ignored, because of the same header in the request\n\t\t\"bar\": \"2\",\n\t})\n\tr.SetHeaders(map[string]string{\n\t\t\"foo\": \"3\",\n\t\t\"xyz\": \"4\",\n\t})\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tif err := parseRequestHeader(c, r); err != nil {\n\t\t\tb.Errorf(\"parseRequestHeader() error = %v\", err)\n\t\t}\n\t}\n}\n\nfunc Benchmark_parseRequestBody_string(b *testing.B) {\n\tc := New()\n\tr := c.R()\n\tr.SetBody(\"foo\")\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tif err := parseRequestBody(c, r); err != nil {\n\t\t\tb.Errorf(\"parseRequestBody() error = %v\", err)\n\t\t}\n\t}\n}\n\nfunc Benchmark_parseRequestBody_byte(b *testing.B) {\n\tc := New()\n\tr := c.R()\n\tr.SetBody([]byte(\"foo\"))\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tif err := parseRequestBody(c, r); err != nil {\n\t\t\tb.Errorf(\"parseRequestBody() error = %v\", err)\n\t\t}\n\t}\n}\n\nfunc Benchmark_parseRequestBody_reader(b *testing.B) {\n\tc := New()\n\tr := c.R()\n\tr.SetBody(bytes.NewBufferString(\"foo\"))\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tif err := parseRequestBody(c, r); err != nil {\n\t\t\tb.Errorf(\"parseRequestBody() error = %v\", err)\n\t\t}\n\t}\n}\n\nfunc Benchmark_parseRequestBody_struct(b *testing.B) {\n\ttype FooBar struct {\n\t\tFoo string `json:\"foo\"`\n\t\tBar string `json:\"bar\"`\n\t}\n\tc := New()\n\tr := c.R()\n\tr.SetBody(FooBar{Foo: \"1\", Bar: \"2\"}).SetHeader(hdrContentTypeKey, jsonContentType)\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tif err := parseRequestBody(c, r); err != nil {\n\t\t\tb.Errorf(\"parseRequestBody() error = %v\", err)\n\t\t}\n\t}\n}\n\nfunc Benchmark_parseRequestBody_struct_xml(b *testing.B) {\n\ttype FooBar struct {\n\t\tFoo string `xml:\"foo\"`\n\t\tBar string `xml:\"bar\"`\n\t}\n\tc := New()\n\tr := c.R()\n\tr.SetBody(FooBar{Foo: \"1\", Bar: \"2\"}).SetHeader(hdrContentTypeKey, \"text/xml\")\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tif err := parseRequestBody(c, r); err != nil {\n\t\t\tb.Errorf(\"parseRequestBody() error = %v\", err)\n\t\t}\n\t}\n}\n\nfunc Benchmark_parseRequestBody_map(b *testing.B) {\n\tc := New()\n\tr := c.R()\n\tr.SetBody(map[string]string{\n\t\t\"foo\": \"1\",\n\t\t\"bar\": \"2\",\n\t}).SetHeader(hdrContentTypeKey, jsonContentType)\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tif err := parseRequestBody(c, r); err != nil {\n\t\t\tb.Errorf(\"parseRequestBody() error = %v\", err)\n\t\t}\n\t}\n}\n\nfunc Benchmark_parseRequestBody_slice(b *testing.B) {\n\tc := New()\n\tr := c.R()\n\tr.SetBody([]string{\"1\", \"2\"}).SetHeader(hdrContentTypeKey, jsonContentType)\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tif err := parseRequestBody(c, r); err != nil {\n\t\t\tb.Errorf(\"parseRequestBody() error = %v\", err)\n\t\t}\n\t}\n}\n\nfunc Benchmark_parseRequestBody_FormData(b *testing.B) {\n\tc := New()\n\tr := c.R()\n\tc.SetFormData(map[string]string{\"foo\": \"1\", \"bar\": \"2\"})\n\tr.SetFormData(map[string]string{\"foo\": \"3\", \"baz\": \"4\"})\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tif err := parseRequestBody(c, r); err != nil {\n\t\t\tb.Errorf(\"parseRequestBody() error = %v\", err)\n\t\t}\n\t}\n}\n\nfunc Benchmark_parseRequestBody_MultiPart(b *testing.B) {\n\tc := New()\n\tr := c.R()\n\tc.SetFormData(map[string]string{\"foo\": \"1\", \"bar\": \"2\"})\n\tr.SetFormData(map[string]string{\"foo\": \"3\", \"baz\": \"4\"}).\n\t\tSetMultipartFormData(map[string]string{\"foo\": \"5\", \"xyz\": \"6\"}).\n\t\tSetFileReader(\"qwe\", \"qwe.txt\", strings.NewReader(\"7\")).\n\t\tSetMultipartFields(\n\t\t\t&MultipartField{\n\t\t\t\tName:        \"sdj\",\n\t\t\t\tContentType: \"text/plain\",\n\t\t\t\tReader:      strings.NewReader(\"8\"),\n\t\t\t},\n\t\t).\n\t\tSetMethod(MethodPost)\n\tb.ResetTimer()\n\tfor i := 0; i < b.N; i++ {\n\t\tif err := parseRequestBody(c, r); err != nil {\n\t\t\tb.Errorf(\"parseRequestBody() error = %v\", err)\n\t\t}\n\t}\n}\n\n// benchmarkStringer implements fmt.Stringer for benchmarking\ntype benchmarkStringer struct {\n\tvalue string\n}\n\nfunc (s benchmarkStringer) String() string {\n\treturn s.value\n}\n\n// Tier 1: most common URL types\nfunc Benchmark_formatAnyToString_string(b *testing.B) {\n\tv := \"hello world\"\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n\nfunc Benchmark_formatAnyToString_int(b *testing.B) {\n\tv := 12345\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n\nfunc Benchmark_formatAnyToString_bool(b *testing.B) {\n\tv := true\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n\nfunc Benchmark_formatAnyToString_int64(b *testing.B) {\n\tv := int64(9223372036854775807)\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n\nfunc Benchmark_formatAnyToString_stringSlice(b *testing.B) {\n\tv := []string{\"a\", \"b\", \"c\"}\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n\n// Tier 2: common stdlib types\nfunc Benchmark_formatAnyToString_time(b *testing.B) {\n\tv := time.Date(2024, 6, 15, 10, 30, 0, 0, time.UTC)\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n\nfunc Benchmark_formatAnyToString_byteSlice(b *testing.B) {\n\tv := []byte(\"binary data\")\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n\nfunc Benchmark_formatAnyToString_float64(b *testing.B) {\n\tv := 3.14159265359\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n\n// Tier 3: less common integers (signed)\nfunc Benchmark_formatAnyToString_int32(b *testing.B) {\n\tv := int32(2147483647)\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n\nfunc Benchmark_formatAnyToString_int16(b *testing.B) {\n\tv := int16(32767)\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n\nfunc Benchmark_formatAnyToString_int8(b *testing.B) {\n\tv := int8(127)\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n\n// Tier 4: less common integers (unsigned)\nfunc Benchmark_formatAnyToString_uint64(b *testing.B) {\n\tv := uint64(18446744073709551615)\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n\nfunc Benchmark_formatAnyToString_uint32(b *testing.B) {\n\tv := uint32(4294967295)\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n\nfunc Benchmark_formatAnyToString_uint16(b *testing.B) {\n\tv := uint16(65535)\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n\nfunc Benchmark_formatAnyToString_uint8(b *testing.B) {\n\tv := uint8(255)\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n\nfunc Benchmark_formatAnyToString_uint(b *testing.B) {\n\tv := uint(12345)\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n\n// Tier 5: rare types and fallbacks\nfunc Benchmark_formatAnyToString_float32(b *testing.B) {\n\tv := float32(3.14)\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n\nfunc Benchmark_formatAnyToString_stringer(b *testing.B) {\n\tv := benchmarkStringer{value: \"custom value\"}\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n\nfunc Benchmark_formatAnyToString_default(b *testing.B) {\n\tv := struct{ Name string }{Name: \"test\"}\n\tfor i := 0; i < b.N; i++ {\n\t\t_ = formatAnyToString(v)\n\t}\n}\n"
  },
  {
    "path": "cert_watcher_test.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/pem\"\n\t\"math/big\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\ntype certPaths struct {\n\tRootCAKey  string\n\tRootCACert string\n\tTLSKey     string\n\tTLSCert    string\n}\n\nfunc TestClient_SetRootCertificateWatcher(t *testing.T) {\n\t// For this test, we want to:\n\t// - Generate root CA\n\t// - Generate TLS cert signed with root CA\n\t// - Start a Test HTTPS server\n\t// - Create a Resty client with SetRootCertificateWatcher and SetClientRootCertificateWatcher\n\t// - Send multiple requests and re-generate the certs periodically to reproduce renewal\n\n\tcertDir := t.TempDir()\n\tpaths := certPaths{\n\t\tRootCAKey:  filepath.Join(certDir, \"root-ca.key\"),\n\t\tRootCACert: filepath.Join(certDir, \"root-ca.crt\"),\n\t\tTLSKey:     filepath.Join(certDir, \"tls.key\"),\n\t\tTLSCert:    filepath.Join(certDir, \"tls.crt\"),\n\t}\n\n\tgenerateCerts(t, paths)\n\n\tts := createTestTLSServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t}, paths.TLSCert, paths.TLSKey)\n\tdefer ts.Close()\n\n\tpoolingInterval := 100 * time.Millisecond\n\n\tclient := NewWithTransportSettings(&TransportSettings{\n\t\t// Make sure that TLS handshake happens for all request\n\t\t// (otherwise, test may succeed because 1st TLS session is re-used)\n\t\tDisableKeepAlives: true,\n\t}).SetRootCertificatesWatcher(\n\t\t&CertWatcherOptions{PoolInterval: poolingInterval},\n\t\tpaths.RootCACert,\n\t).SetClientRootCertificatesWatcher(\n\t\t&CertWatcherOptions{PoolInterval: poolingInterval},\n\t\tpaths.RootCACert,\n\t).SetDebug(false)\n\n\turl := strings.Replace(ts.URL, \"127.0.0.1\", \"localhost\", 1)\n\tt.Log(\"Test URL:\", url)\n\n\tt.Run(\"Cert Watcher should handle certs rotation\", func(t *testing.T) {\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tres, err := client.R().Get(url)\n\t\t\tif err != nil {\n\t\t\t\tt.Fatal(err)\n\t\t\t}\n\n\t\t\tassertEqual(t, res.StatusCode(), http.StatusOK)\n\n\t\t\tif i%2 == 1 {\n\t\t\t\t// Re-generate certs to simulate renewal scenario\n\t\t\t\tgenerateCerts(t, paths)\n\t\t\t\ttime.Sleep(50 * time.Millisecond)\n\t\t\t}\n\n\t\t}\n\t})\n\n\tt.Run(\"Cert Watcher should recover on failure\", func(t *testing.T) {\n\t\t// Delete root cert and re-create it to ensure that cert watcher is able to recover\n\n\t\t// Re-generate certs to invalidate existing cert\n\t\tgenerateCerts(t, paths)\n\t\t// Delete root cert so that Cert Watcher will fail\n\t\terr := os.RemoveAll(paths.RootCACert)\n\t\tassertNil(t, err)\n\n\t\t// Reset TLS config to ensure that previous root cert is not re-used\n\t\ttr, err := client.HTTPTransport()\n\t\tassertNil(t, err)\n\t\ttr.TLSClientConfig = nil\n\t\tclient.SetTransport(tr)\n\n\t\ttime.Sleep(50 * time.Millisecond)\n\n\t\t_, err = client.R().Get(url)\n\t\t// We expect an error since root cert has been deleted\n\t\tassertNotNil(t, err)\n\n\t\t// Re-generate certs. We except cert watcher to reload the new root cert.\n\t\tgenerateCerts(t, paths)\n\t\ttime.Sleep(50 * time.Millisecond)\n\t\t_, err = client.R().Get(url)\n\t\tassertNil(t, err)\n\t})\n\n\terr := client.Close()\n\tassertNil(t, err)\n}\n\nfunc generateCerts(t *testing.T, paths certPaths) {\n\trootKey, rootCert, err := generateRootCA(paths.RootCAKey, paths.RootCACert)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tif err := generateTLSCert(paths.TLSKey, paths.TLSCert, rootKey, rootCert); err != nil {\n\t\tt.Fatal(err)\n\t}\n}\n\n// Generate a Root Certificate Authority (CA)\nfunc generateRootCA(keyPath, certPath string) (*rsa.PrivateKey, []byte, error) {\n\t// Generate the key for the Root CA\n\trootKey, err := generateKey()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// Define the maximum value you want for the random big integer\n\tmax := new(big.Int).Lsh(big.NewInt(1), 256) // Example: 256 bits\n\n\t// Generate a random big.Int\n\trandomBigInt, err := rand.Int(rand.Reader, max)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// Create the root certificate template\n\trootCertTemplate := &x509.Certificate{\n\t\tSerialNumber: randomBigInt,\n\t\tSubject: pkix.Name{\n\t\t\tOrganization: []string{\"YourOrg\"},\n\t\t\tCountry:      []string{\"US\"},\n\t\t\tProvince:     []string{\"State\"},\n\t\t\tLocality:     []string{\"City\"},\n\t\t\tCommonName:   \"YourRootCA\",\n\t\t},\n\t\tNotBefore:             time.Now(),\n\t\tNotAfter:              time.Now().Add(time.Hour * 10),\n\t\tKeyUsage:              x509.KeyUsageCertSign | x509.KeyUsageCRLSign,\n\t\tIsCA:                  true,\n\t\tBasicConstraintsValid: true,\n\t}\n\n\t// Self-sign the root certificate\n\trootCert, err := x509.CreateCertificate(rand.Reader, rootCertTemplate, rootCertTemplate, &rootKey.PublicKey, rootKey)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\t// Save the Root CA key and certificate\n\tif err := savePEMKey(keyPath, rootKey); err != nil {\n\t\treturn nil, nil, err\n\t}\n\tif err := savePEMCert(certPath, rootCert); err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\treturn rootKey, rootCert, nil\n}\n\n// Generate a TLS Certificate signed by the Root CA\nfunc generateTLSCert(keyPath, certPath string, rootKey *rsa.PrivateKey, rootCert []byte) error {\n\t// Generate a key for the server\n\tserverKey, err := generateKey()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Parse the Root CA certificate\n\tparsedRootCert, err := x509.ParseCertificate(rootCert)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Create the server certificate template\n\tserverCertTemplate := &x509.Certificate{\n\t\tSerialNumber: big.NewInt(2),\n\t\tSubject: pkix.Name{\n\t\t\tOrganization: []string{\"YourOrg\"},\n\t\t\tCommonName:   \"localhost\",\n\t\t},\n\t\tNotBefore:   time.Now(),\n\t\tNotAfter:    time.Now().Add(time.Hour * 10),\n\t\tKeyUsage:    x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,\n\t\tExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},\n\t\tIPAddresses: []net.IP{net.ParseIP(\"127.0.0.1\")},\n\t\tDNSNames:    []string{\"localhost\"},\n\t}\n\n\t// Sign the server certificate with the Root CA\n\tserverCert, err := x509.CreateCertificate(rand.Reader, serverCertTemplate, parsedRootCert, &serverKey.PublicKey, rootKey)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Save the server key and certificate\n\tif err := savePEMKey(keyPath, serverKey); err != nil {\n\t\treturn err\n\t}\n\tif err := savePEMCert(certPath, serverCert); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc generateKey() (*rsa.PrivateKey, error) {\n\treturn rsa.GenerateKey(rand.Reader, 2048)\n}\n\nfunc savePEMKey(fileName string, key *rsa.PrivateKey) error {\n\tkeyFile, err := os.Create(fileName)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer keyFile.Close()\n\n\tprivateKeyPEM := &pem.Block{\n\t\tType:  \"RSA PRIVATE KEY\",\n\t\tBytes: x509.MarshalPKCS1PrivateKey(key),\n\t}\n\n\treturn pem.Encode(keyFile, privateKeyPEM)\n}\n\nfunc savePEMCert(fileName string, cert []byte) error {\n\tcertFile, err := os.Create(fileName)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer certFile.Close()\n\n\tcertPEM := &pem.Block{\n\t\tType:  \"CERTIFICATE\",\n\t\tBytes: cert,\n\t}\n\n\treturn pem.Encode(certFile, certPEM)\n}\n"
  },
  {
    "path": "circuit_breaker.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\n// ErrCircuitBreakerOpen is returned when the circuit breaker is open.\nvar ErrCircuitBreakerOpen = errors.New(\"resty: circuit breaker open\")\n\ntype (\n\t// CircuitBreakerTriggerHook type is for reacting to circuit breaker trigger hooks.\n\tCircuitBreakerTriggerHook func(*Request, error)\n\n\t// CircuitBreakerStateChangeHook type is for reacting to circuit breaker state change hooks.\n\tCircuitBreakerStateChangeHook func(oldState, newState CircuitBreakerState)\n\n\t// CircuitBreakerState type represents the state of the circuit breaker.\n\tCircuitBreakerState uint32\n)\n\n// group is an interface for types that can be combined and inverted\ntype group[T any] interface {\n\top(T) T\n\tempty() T\n\tinverse() T\n}\n\n// totalAndFailures tracks total requests and failures\ntype totalAndFailures struct {\n\ttotal    int\n\tfailures int\n}\n\nfunc (tf totalAndFailures) op(g totalAndFailures) totalAndFailures {\n\ttf.total += g.total\n\ttf.failures += g.failures\n\treturn tf\n}\n\nfunc (tf totalAndFailures) empty() totalAndFailures {\n\treturn totalAndFailures{}\n}\n\nfunc (tf totalAndFailures) inverse() totalAndFailures {\n\ttf.total = -tf.total\n\ttf.failures = -tf.failures\n\treturn tf\n}\n\n// slidingWindow implements a time-based sliding window for tracking values\ntype slidingWindow[G group[G]] struct {\n\tmutex     sync.RWMutex\n\ttotal     G\n\tvalues    []G\n\tidx       int\n\tlastStart time.Time\n\tinterval  time.Duration\n}\n\nfunc newSlidingWindow[G group[G]](empty func() G, interval time.Duration, buckets int) *slidingWindow[G] {\n\treturn &slidingWindow[G]{\n\t\ttotal:     empty(),\n\t\tvalues:    make([]G, buckets),\n\t\tidx:       0,\n\t\tlastStart: time.Now(),\n\t\tinterval:  interval,\n\t}\n}\n\nfunc (sw *slidingWindow[G]) Add(val G) {\n\tsw.mutex.Lock()\n\tdefer sw.mutex.Unlock()\n\n\tnow := time.Now()\n\telapsed := now.Sub(sw.lastStart)\n\tbucketDuration := sw.interval / time.Duration(len(sw.values))\n\n\t// Advance window if needed\n\tif elapsed >= bucketDuration {\n\t\tbucketsToAdvance := int(elapsed / bucketDuration)\n\t\tif bucketsToAdvance >= len(sw.values) {\n\t\t\t// Reset all buckets\n\t\t\tfor i := range sw.values {\n\t\t\t\tsw.values[i] = sw.total.empty()\n\t\t\t}\n\t\t\tsw.total = sw.total.empty()\n\t\t\tsw.idx = 0\n\t\t} else {\n\t\t\t// Remove old buckets\n\t\t\tfor i := 0; i < bucketsToAdvance; i++ {\n\t\t\t\tsw.idx = (sw.idx + 1) % len(sw.values)\n\t\t\t\tsw.total = sw.total.op(sw.values[sw.idx].inverse())\n\t\t\t\tsw.values[sw.idx] = sw.total.empty()\n\t\t\t}\n\t\t}\n\t\tsw.lastStart = now\n\t}\n\n\t// Add to current bucket\n\tsw.values[sw.idx] = sw.values[sw.idx].op(val)\n\tsw.total = sw.total.op(val)\n}\n\nfunc (sw *slidingWindow[G]) Get() G {\n\tsw.mutex.RLock()\n\tdefer sw.mutex.RUnlock()\n\treturn sw.total\n}\n\nfunc (sw *slidingWindow[G]) SetInterval(interval time.Duration) {\n\tsw.mutex.Lock()\n\tdefer sw.mutex.Unlock()\n\tsw.interval = interval\n}\n\nconst (\n\t// CircuitBreakerStateClosed represents the closed state of the circuit breaker.\n\tCircuitBreakerStateClosed CircuitBreakerState = iota\n\n\t// CircuitBreakerStateOpen represents the open state of the circuit breaker.\n\tCircuitBreakerStateOpen\n\n\t// CircuitBreakerStateHalfOpen represents the half-open state of the circuit breaker.\n\tCircuitBreakerStateHalfOpen\n)\n\n// CircuitBreaker struct implements a state machine to monitor and manage the\n// states of circuit breakers. The three states are:\n//   - Closed: requests are allowed\n//   - Open: requests are blocked\n//   - Half-Open: a single request is allowed to determine\n//\n// Transitions\n//   - To Closed State: when the success count reaches the success threshold.\n//   - To Open State: when the failure count reaches the failure threshold.\n//   - Half-Open Check: when the specified timeout reaches, a single request is allowed\n//     to determine the transition state; if failed, it goes back to the open state.\n//\n// Use [NewCircuitBreakerWithCount] or [NewCircuitBreakerWithRatio] to create a new [CircuitBreaker]\n// instance accordingly.\ntype CircuitBreaker struct {\n\tlock         *sync.RWMutex\n\tpolicies     []CircuitBreakerPolicy\n\tresetTimeout time.Duration\n\tstate        atomic.Value // CircuitBreakerState\n\tsw           *slidingWindow[totalAndFailures]\n\n\t// Hooks\n\ttriggerHooks     []CircuitBreakerTriggerHook\n\tstateChangeHooks []CircuitBreakerStateChangeHook\n\n\t// Count-based\n\tfailureThreshold uint64\n\tsuccessThreshold uint64\n\n\t// Ratio-based\n\tisRatioBased bool\n\tfailureRatio float64 // Threshold, e.g., 0.5 for 50% failure\n\tminRequests  uint64  // Minimum number of requests to consider failure ratio\n}\n\n// NewCircuitBreakerWithCount method creates a new [CircuitBreaker] instance with Count settings.\n//\n// The default settings are:\n//   - Policies: CircuitBreaker5xxPolicy\nfunc NewCircuitBreakerWithCount(failureThreshold uint64, successThreshold uint64,\n\tresetTimeout time.Duration, policies ...CircuitBreakerPolicy) *CircuitBreaker {\n\tcb := newCircuitBreaker(resetTimeout, policies...)\n\tcb.failureThreshold = failureThreshold\n\tcb.successThreshold = successThreshold\n\treturn cb\n}\n\n// NewCircuitBreakerWithRatio method creates a new [CircuitBreaker] instance with Ratio settings.\n//\n// The default settings are:\n//   - Policies: CircuitBreaker5xxPolicy\nfunc NewCircuitBreakerWithRatio(failureRatio float64, minRequests uint64,\n\tresetTimeout time.Duration, policies ...CircuitBreakerPolicy) *CircuitBreaker {\n\tcb := newCircuitBreaker(resetTimeout, policies...)\n\tcb.failureRatio = failureRatio\n\tcb.minRequests = minRequests\n\tcb.isRatioBased = true\n\treturn cb\n}\n\nfunc newCircuitBreaker(resetTimeout time.Duration, policies ...CircuitBreakerPolicy) *CircuitBreaker {\n\tcb := &CircuitBreaker{\n\t\tlock:         &sync.RWMutex{},\n\t\tresetTimeout: resetTimeout,\n\t\tpolicies:     []CircuitBreakerPolicy{CircuitBreaker5xxPolicy},\n\t}\n\tcb.state.Store(CircuitBreakerStateClosed)\n\tcb.sw = newSlidingWindow(\n\t\tfunc() totalAndFailures { return totalAndFailures{} },\n\t\tresetTimeout,\n\t\t10,\n\t)\n\tif len(policies) > 0 {\n\t\tcb.policies = policies\n\t}\n\treturn cb\n}\n\n// OnTrigger method adds a [CircuitBreakerTriggerHook] to the [CircuitBreaker] instance.\nfunc (cb *CircuitBreaker) OnTrigger(hooks ...CircuitBreakerTriggerHook) *CircuitBreaker {\n\tcb.lock.Lock()\n\tdefer cb.lock.Unlock()\n\tcb.triggerHooks = append(cb.triggerHooks, hooks...)\n\treturn cb\n}\n\n// onTriggerHooks method executes all registered trigger hooks.\nfunc (cb *CircuitBreaker) onTriggerHooks(req *Request, err error) {\n\tcb.lock.RLock()\n\tdefer cb.lock.RUnlock()\n\tfor _, h := range cb.triggerHooks {\n\t\th(req, err)\n\t}\n}\n\n// OnStateChange method adds a [CircuitBreakerStateChangeHook] to the [CircuitBreaker] instance.\nfunc (cb *CircuitBreaker) OnStateChange(hooks ...CircuitBreakerStateChangeHook) *CircuitBreaker {\n\tcb.lock.Lock()\n\tdefer cb.lock.Unlock()\n\tcb.stateChangeHooks = append(cb.stateChangeHooks, hooks...)\n\treturn cb\n}\n\n// onStateChangeHooks method executes all registered state change hooks.\nfunc (cb *CircuitBreaker) onStateChangeHooks(oldState, newState CircuitBreakerState) {\n\tcb.lock.RLock()\n\tdefer cb.lock.RUnlock()\n\tfor _, h := range cb.stateChangeHooks {\n\t\th(oldState, newState)\n\t}\n}\n\n// CircuitBreakerPolicy is a function type that determines whether a response should\n// trip the [CircuitBreaker].\ntype CircuitBreakerPolicy func(resp *http.Response) bool\n\n// CircuitBreaker5xxPolicy is a [CircuitBreakerPolicy] that trips the [CircuitBreaker] if\n// the response status code is 500 or greater.\nfunc CircuitBreaker5xxPolicy(resp *http.Response) bool {\n\treturn resp.StatusCode > 499\n}\n\nfunc (cb *CircuitBreaker) getState() CircuitBreakerState {\n\treturn cb.state.Load().(CircuitBreakerState)\n}\n\nfunc (cb *CircuitBreaker) allow() error {\n\tif cb.getState() == CircuitBreakerStateOpen {\n\t\treturn ErrCircuitBreakerOpen\n\t}\n\n\treturn nil\n}\n\nfunc (cb *CircuitBreaker) applyPolicies(resp *http.Response) {\n\tfailed := false\n\tfor _, policy := range cb.policies {\n\t\tif policy(resp) {\n\t\t\tfailed = true\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif failed {\n\t\tcb.sw.Add(totalAndFailures{total: 1, failures: 1})\n\n\t\tswitch cb.getState() {\n\t\tcase CircuitBreakerStateClosed:\n\t\t\ttf := cb.sw.Get()\n\n\t\t\tif cb.isRatioBased {\n\t\t\t\tif tf.total >= int(cb.minRequests) {\n\t\t\t\t\tcurrentFailureRatio := float64(tf.failures) / float64(tf.total)\n\t\t\t\t\tif currentFailureRatio >= cb.failureRatio {\n\t\t\t\t\t\tcb.open()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif tf.failures >= int(cb.failureThreshold) {\n\t\t\t\t\tcb.open()\n\t\t\t\t}\n\t\t\t}\n\t\tcase CircuitBreakerStateHalfOpen:\n\t\t\tcb.open()\n\t\t}\n\n\t\treturn\n\t}\n\n\tcb.sw.Add(totalAndFailures{total: 1, failures: 0})\n\n\tswitch cb.getState() {\n\tcase CircuitBreakerStateClosed:\n\t\treturn\n\tcase CircuitBreakerStateHalfOpen:\n\t\ttf := cb.sw.Get()\n\t\tif tf.total-tf.failures >= int(cb.successThreshold) {\n\t\t\tcb.changeState(CircuitBreakerStateClosed)\n\t\t}\n\t}\n}\n\nfunc (cb *CircuitBreaker) open() {\n\tcb.changeState(CircuitBreakerStateOpen)\n\tgo func() {\n\t\ttime.Sleep(cb.resetTimeout)\n\t\tcb.changeState(CircuitBreakerStateHalfOpen)\n\t}()\n}\n\nfunc (cb *CircuitBreaker) changeState(state CircuitBreakerState) {\n\toldState := cb.getState()\n\tcb.sw = newSlidingWindow(\n\t\tfunc() totalAndFailures { return totalAndFailures{} },\n\t\tcb.resetTimeout,\n\t\t10,\n\t)\n\tcb.state.Store(state)\n\tif oldState != state {\n\t\tcb.onStateChangeHooks(oldState, state)\n\t}\n}\n"
  },
  {
    "path": "circuit_breaker_test.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"net/http\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\nvar _ CircuitBreakerPolicy = CircuitBreaker5xxPolicy\n\nfunc TestCircuitBreakerCountBased(t *testing.T) {\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tt.Logf(\"Method: %v\", r.Method)\n\t\tt.Logf(\"Path: %v\", r.URL.Path)\n\n\t\tswitch r.URL.Path {\n\t\tcase \"/200\":\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\treturn\n\t\tcase \"/500\":\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t})\n\tdefer ts.Close()\n\n\tfailThreshold := uint64(2)\n\tsuccessThreshold := uint64(1)\n\tresetTimeout := 100 * time.Millisecond\n\n\tcb := NewCircuitBreakerWithCount(failThreshold, successThreshold, resetTimeout)\n\n\tc := dcnl().SetCircuitBreaker(cb)\n\n\tfor i := uint64(0); i < failThreshold; i++ {\n\t\t_, err := c.R().Get(ts.URL + \"/500\")\n\t\tassertNil(t, err)\n\t}\n\tresp, err := c.R().Get(ts.URL + \"/500\")\n\tassertErrorIs(t, ErrCircuitBreakerOpen, err)\n\tassertNil(t, resp)\n\tassertEqual(t, CircuitBreakerStateOpen, c.circuitBreaker.getState(), \"expected open state after reaching failure threshold\")\n\n\ttime.Sleep(resetTimeout + 50*time.Millisecond)\n\tassertEqual(t, CircuitBreakerStateHalfOpen, c.circuitBreaker.getState(), \"expected half-open state\")\n\n\t_, err = c.R().Get(ts.URL + \"/500\")\n\tassertError(t, err)\n\tassertEqual(t, CircuitBreakerStateOpen, c.circuitBreaker.getState(), \"expected open state after failure in half-open\")\n\n\ttime.Sleep(resetTimeout + 50*time.Millisecond)\n\tassertEqual(t, CircuitBreakerStateHalfOpen, c.circuitBreaker.getState(), \"expected half-open state\")\n\n\tfor i := uint64(0); i < successThreshold; i++ {\n\t\t_, err := c.R().Get(ts.URL + \"/200\")\n\t\tassertNil(t, err)\n\t}\n\tassertEqual(t, CircuitBreakerStateClosed, c.circuitBreaker.getState(), \"expected closed state after success threshold\")\n\n\tresp, err = c.R().Get(ts.URL + \"/200\")\n\tassertNil(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\t_, err = c.R().Get(ts.URL + \"/500\")\n\tassertError(t, err)\n\tassertEqual(t, 1, c.circuitBreaker.sw.Get().failures, \"expected failure count to be 1 after single failure in closed state\")\n\n\ttime.Sleep(resetTimeout)\n\n\t_, err = c.R().Get(ts.URL + \"/500\")\n\tassertError(t, err)\n\tassertEqual(t, 1, c.circuitBreaker.sw.Get().failures, \"expected failure count to be 1 after single failure in closed state\")\n}\n\nfunc TestCircuitBreaker5xxPolicy(t *testing.T) {\n\tres1 := CircuitBreaker5xxPolicy(&http.Response{StatusCode: 500})\n\tassertTrue(t, res1, \"expected true for 5xx status code\")\n\n\tres2 := CircuitBreaker5xxPolicy(&http.Response{StatusCode: 200})\n\tassertFalse(t, res2, \"expected false for non-5xx status code\")\n}\n\nfunc TestCircuitBreakerCountBasedOpensAndAllow(t *testing.T) {\n\tcb := NewCircuitBreakerWithCount(2, 1, 20*time.Millisecond)\n\tfail := &http.Response{StatusCode: 500}\n\n\t// expected allow when state is closed\n\terr1 := cb.allow()\n\tassertNil(t, err1)\n\tassertEqual(t, 0, cb.sw.Get().failures, \"expected allow when no failures initially\")\n\n\t// expected still closed after 1 failure\n\tcb.applyPolicies(fail)\n\terr2 := cb.allow()\n\tassertNil(t, err2)\n\tassertEqual(t, 1, cb.sw.Get().failures, \"expected still closed after 1 failure\")\n\n\t// expected open after reaching failure threshold\n\tcb.applyPolicies(fail)\n\terr3 := cb.allow()\n\tassertErrorIs(t, ErrCircuitBreakerOpen, err3, \"expected open after reaching failure threshold\")\n\n\t// time.Sleep to half-open state\n\ttime.Sleep(25 * time.Millisecond)\n\tassertEqual(t, CircuitBreakerStateHalfOpen, cb.getState(), \"expected half-open state after reset timeout\")\n\n\t// expected still half-open after a failure\n\tcb.applyPolicies(fail)\n\tassertEqual(t, CircuitBreakerStateOpen, cb.getState(), \"expected open state after failure in half-open\")\n\n\t// expected open state on allow\n\terr4 := cb.allow()\n\tassertErrorIs(t, ErrCircuitBreakerOpen, err4, \"expected open state on allow after failure in half-open\")\n}\n\nfunc TestCircuitBreakerCountBasedHalfOpenToClosedOnSuccess(t *testing.T) {\n\tcb := NewCircuitBreakerWithCount(1, 1, 30*time.Millisecond)\n\tfail := &http.Response{StatusCode: 500}\n\tok := &http.Response{StatusCode: 200}\n\n\t// expected open after failing threshold\n\tcb.applyPolicies(fail)\n\terr1 := cb.allow()\n\tassertErrorIs(t, ErrCircuitBreakerOpen, err1, \"expected open after failing threshold\")\n\n\t// wait for resetTimeout to transition to half-open\n\tdeadline := time.Now().Add(200 * time.Millisecond)\n\tfor time.Now().Before(deadline) {\n\t\tif cb.getState() == CircuitBreakerStateHalfOpen {\n\t\t\tbreak\n\t\t}\n\t\ttime.Sleep(5 * time.Millisecond)\n\t}\n\t// expected half-open state after reset timeout\n\tassertEqual(t, CircuitBreakerStateHalfOpen, cb.getState(), \"expected half-open state after reset timeout\")\n\n\t// on success in half-open, should move to closed\n\tcb.applyPolicies(ok)\n\tassertEqual(t, CircuitBreakerStateClosed, cb.getState(), \"expected closed state after success in half-open\")\n\n\t// expected allow when closed\n\terr := cb.allow()\n\tassertNil(t, err)\n}\n\nfunc TestCircuitBreakerRatioBasedOpenToClosed(t *testing.T) {\n\tcb := NewCircuitBreakerWithRatio(0.5, 2, 20*time.Millisecond)\n\tfail := &http.Response{StatusCode: 500}\n\tok := &http.Response{StatusCode: 200}\n\n\t// two failures should open (2/2 = 1.0 >= 0.5)\n\tcb.applyPolicies(fail)\n\terr1 := cb.allow()\n\tassertNil(t, err1)\n\tif err1 == ErrCircuitBreakerOpen {\n\t\tt.Errorf(\"expected still closed after 1 failure (minRequests not met)\")\n\t}\n\n\t// expected open after failures exceed ratio threshold\n\tcb.applyPolicies(fail)\n\terr2 := cb.allow()\n\tassertErrorIs(t, ErrCircuitBreakerOpen, err2, \"expected open after failures exceed ratio threshold\")\n\n\ttime.Sleep(25 * time.Millisecond)\n\n\t// expected half-open state after reset timeout\n\tassertEqual(t, CircuitBreakerStateHalfOpen, cb.getState(), \"expected half-open state after reset timeout\")\n\n\t// on success in half-open, should move to closed\n\tcb.applyPolicies(ok)\n\tassertEqual(t, CircuitBreakerStateClosed, cb.getState(), \"expected closed state after success in half-open\")\n}\n\nfunc TestCircuitBreakerNewStateAndPolicies(t *testing.T) {\n\tcb := NewCircuitBreakerWithCount(3, 2, 10*time.Millisecond, CircuitBreaker5xxPolicy)\n\tassertEqual(t, CircuitBreakerStateClosed, cb.getState())\n\tassertEqual(t, uint64(3), cb.failureThreshold)\n\tassertEqual(t, uint64(2), cb.successThreshold)\n\tassertEqual(t, 10*time.Millisecond, cb.resetTimeout)\n\tassertEqual(t, 1, len(cb.policies))\n}\n\nfunc TestCircuitBreakerChangeStateClearsCounts(t *testing.T) {\n\tcb := NewCircuitBreakerWithCount(2, 1, 10*time.Millisecond)\n\tfail := &http.Response{StatusCode: 500}\n\n\tcb.applyPolicies(fail)\n\tassertEqual(t, 1, cb.sw.Get().failures)\n\n\tcb.changeState(CircuitBreakerStateHalfOpen)\n\tassertEqual(t, CircuitBreakerStateHalfOpen, cb.getState())\n\tassertEqual(t, 0, cb.sw.Get().failures)\n\tassertEqual(t, 0, cb.sw.Get().total)\n}\n\nfunc TestCircuitBreakerAllowDuringHalfOpen(t *testing.T) {\n\tcb := NewCircuitBreakerWithCount(1, 1, 20*time.Millisecond)\n\tfail := &http.Response{StatusCode: 500}\n\n\tcb.applyPolicies(fail) // opens\n\tassertErrorIs(t, ErrCircuitBreakerOpen, cb.allow(), \"expected open state\")\n\n\ttime.Sleep(25 * time.Millisecond) // wait to transition to half-open\n\tassertEqual(t, CircuitBreakerStateHalfOpen, cb.getState(), \"expected half-open state\")\n\tassertNil(t, cb.allow())\n}\n\nfunc TestCircuitBreakerOnTriggerHooks(t *testing.T) {\n\tcb := NewCircuitBreakerWithCount(1, 1, 10*time.Millisecond)\n\n\tcalled := false\n\tvar gotErr error\n\tcb.OnTrigger(func(r *Request, e error) {\n\t\tcalled = true\n\t\tgotErr = e\n\t})\n\n\tcb.onTriggerHooks(nil, ErrCircuitBreakerOpen)\n\n\tassertTrue(t, called, \"expected onTrigger hook to be called\")\n\tassertEqual(t, ErrCircuitBreakerOpen, gotErr, \"expected error to be passed to onTrigger hook\")\n}\n\nfunc TestCircuitBreakerOnStateChangeHooks(t *testing.T) {\n\tcb := NewCircuitBreakerWithCount(1, 1, 10*time.Millisecond)\n\n\tcalled := false\n\tvar oldState, newState CircuitBreakerState\n\tcb.OnStateChange(func(o, n CircuitBreakerState) {\n\t\tcalled = true\n\t\toldState = o\n\t\tnewState = n\n\t})\n\n\tcb.onStateChangeHooks(CircuitBreakerStateClosed, CircuitBreakerStateOpen)\n\n\tassertTrue(t, called)\n\tassertEqual(t, CircuitBreakerStateClosed, oldState, \"expected old state to be passed to onStateChange hook\")\n\tassertEqual(t, CircuitBreakerStateOpen, newState, \"expected new state to be passed to onStateChange hook\")\n}\n\nfunc TestCircuitBreakerMultipleHooksAreCalled(t *testing.T) {\n\tcb := NewCircuitBreakerWithCount(1, 1, 10*time.Millisecond)\n\n\ttriggerCount := 0\n\tcb.OnTrigger(func(_ *Request, _ error) { triggerCount++ })\n\tcb.OnTrigger(func(_ *Request, _ error) { triggerCount++ })\n\n\tcb.onTriggerHooks(nil, ErrCircuitBreakerOpen)\n\tassertEqual(t, 2, triggerCount, \"expected both trigger hooks to be called\")\n\n\tstateCount := 0\n\tcb.OnStateChange(func(_, _ CircuitBreakerState) { stateCount++ })\n\tcb.OnStateChange(func(_, _ CircuitBreakerState) { stateCount++ })\n\n\tcb.onStateChangeHooks(CircuitBreakerStateClosed, CircuitBreakerStateHalfOpen)\n\tassertEqual(t, 2, stateCount, \"expected both state change hooks to be called\")\n}\n\nfunc TestCircuitBreakerConcurrentOnTriggerRegistration(t *testing.T) {\n\tcb := NewCircuitBreakerWithCount(1, 1, 10*time.Millisecond)\n\tvar wg sync.WaitGroup\n\tvar cnt int32\n\tn := 100\n\n\twg.Add(n)\n\tfor i := 0; i < n; i++ {\n\t\tgo func() {\n\t\t\tcb.OnTrigger(func(_ *Request, _ error) {\n\t\t\t\tatomic.AddInt32(&cnt, 1)\n\t\t\t})\n\t\t\twg.Done()\n\t\t}()\n\t}\n\twg.Wait()\n\n\tcb.onTriggerHooks(nil, ErrCircuitBreakerOpen)\n\tgot := atomic.LoadInt32(&cnt)\n\tassertEqual(t, int32(n), got, \"expected N hooks executed\")\n}\n\nfunc TestCircuitBreakerConcurrentOnStateChangeRegistration(t *testing.T) {\n\tcb := NewCircuitBreakerWithCount(1, 1, 10*time.Millisecond)\n\tvar wg sync.WaitGroup\n\tvar cnt int32\n\tn := 100\n\n\twg.Add(n)\n\tfor i := 0; i < n; i++ {\n\t\tgo func() {\n\t\t\tcb.OnStateChange(func(_, _ CircuitBreakerState) {\n\t\t\t\tatomic.AddInt32(&cnt, 1)\n\t\t\t})\n\t\t\twg.Done()\n\t\t}()\n\t}\n\twg.Wait()\n\n\tcb.onStateChangeHooks(CircuitBreakerStateClosed, CircuitBreakerStateOpen)\n\tgot := atomic.LoadInt32(&cnt)\n\tassertEqual(t, int32(n), got, \"expected N state change hooks executed\")\n}\n\nfunc TestCircuitBreakerSlidingWindow1SetInterval(t *testing.T) {\n\tcb := NewCircuitBreakerWithCount(2, 1, 100*time.Millisecond)\n\n\t// Verify initial interval\n\tassertEqual(t, 100*time.Millisecond, cb.sw.interval, \"initial interval mismatch\")\n\n\t// Change interval to a longer duration\n\tcb.sw.SetInterval(200 * time.Millisecond)\n\n\t// Verify interval was changed\n\tassertEqual(t, 200*time.Millisecond, cb.sw.interval, \"interval not updated correctly\")\n}\n\nfunc TestCircuitBreakerSlidingWindow2SetInterval(t *testing.T) {\n\tsw := newSlidingWindow(func() totalAndFailures { return totalAndFailures{} }, 100*time.Millisecond, 5)\n\tassertEqual(t, 100*time.Millisecond, sw.interval, \"initial interval mismatch\")\n\n\tsw.SetInterval(250 * time.Millisecond)\n\tassertEqual(t, 250*time.Millisecond, sw.interval, \"interval not updated correctly\")\n}\n\nfunc TestCircuitBreakerSlidingWindowConcurrentAddGet(t *testing.T) {\n\tsw := newSlidingWindow(func() totalAndFailures { return totalAndFailures{} }, 200*time.Millisecond, 10)\n\n\tvar wg sync.WaitGroup\n\tn := 200\n\twg.Add(n)\n\tfor i := 0; i < n; i++ {\n\t\tgo func() {\n\t\t\tsw.Add(totalAndFailures{total: 1, failures: 0})\n\t\t\twg.Done()\n\t\t}()\n\t}\n\twg.Wait()\n\n\tgot := sw.Get()\n\tassertEqual(t, n, got.total, \"concurrent adds: expected total count mismatch\")\n}\n\nfunc TestCircuitBreakerTotalAndFailuresOperations(t *testing.T) {\n\ta := totalAndFailures{total: 2, failures: 1}\n\tb := totalAndFailures{total: 3, failures: 2}\n\n\tc := a.op(b)\n\tassertEqual(t, 5, c.total, \"op result incorrect, want total 5\")\n\tassertEqual(t, 3, c.failures, \"op result incorrect, want failures 3\")\n\n\tinv := c.inverse()\n\tassertEqual(t, -5, inv.total, \"inverse result incorrect, want total -5\")\n\tassertEqual(t, -3, inv.failures, \"inverse result incorrect, want failures -3\")\n\n\tempty := c.empty()\n\tassertEqual(t, 0, empty.total, \"empty result incorrect, want total 0\")\n\tassertEqual(t, 0, empty.failures, \"empty result incorrect, want failures 0\")\n}\n\nfunc TestCircuitBreakerSlidingWindowResetWhenElapsedExceedsBuckets(t *testing.T) {\n\tinterval := 100 * time.Millisecond\n\tsw := newSlidingWindow(func() totalAndFailures { return totalAndFailures{} }, interval, 4)\n\n\t// Pre-populate total and buckets to non-zero values\n\tsw.values[0] = totalAndFailures{total: 5, failures: 2}\n\tsw.values[1] = totalAndFailures{total: 3, failures: 1}\n\tsw.total = sw.values[0].op(sw.values[1]).op(sw.total)\n\n\t// Force lastStart far in the past so bucketsToAdvance >= len(values) path is taken\n\tsw.lastStart = sw.lastStart.Add(-time.Duration(10) * interval)\n\n\t// Add a new value; should reset buckets and only this value remains\n\tsw.Add(totalAndFailures{total: 1, failures: 1})\n\n\tgot := sw.Get()\n\tassertEqual(t, 1, got.total, \"after reset expected total=1\")\n\tassertEqual(t, 1, got.failures, \"after reset expected failures=1\")\n\tassertEqual(t, 0, sw.idx, \"expected idx reset to 0\")\n}\n"
  },
  {
    "path": "client.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"errors\"\n\t\"io\"\n\t\"maps\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"reflect\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\nconst (\n\t// MethodGet HTTP method\n\tMethodGet = \"GET\"\n\n\t// MethodPost HTTP method\n\tMethodPost = \"POST\"\n\n\t// MethodPut HTTP method\n\tMethodPut = \"PUT\"\n\n\t// MethodDelete HTTP method\n\tMethodDelete = \"DELETE\"\n\n\t// MethodPatch HTTP method\n\tMethodPatch = \"PATCH\"\n\n\t// MethodHead HTTP method\n\tMethodHead = \"HEAD\"\n\n\t// MethodOptions HTTP method\n\tMethodOptions = \"OPTIONS\"\n\n\t// MethodTrace HTTP method\n\tMethodTrace = \"TRACE\"\n)\n\nconst (\n\tdefaultWatcherPoolingInterval = 24 * time.Hour\n)\n\nvar (\n\tErrNotHttpTransportType       = errors.New(\"resty: not a http.Transport type\")\n\tErrUnsupportedRequestBodyKind = errors.New(\"resty: unsupported request body kind\")\n\n\thdrUserAgentKey       = http.CanonicalHeaderKey(\"User-Agent\")\n\thdrAcceptKey          = http.CanonicalHeaderKey(\"Accept\")\n\thdrAcceptEncodingKey  = http.CanonicalHeaderKey(\"Accept-Encoding\")\n\thdrContentTypeKey     = http.CanonicalHeaderKey(\"Content-Type\")\n\thdrContentLengthKey   = http.CanonicalHeaderKey(\"Content-Length\")\n\thdrContentEncodingKey = http.CanonicalHeaderKey(\"Content-Encoding\")\n\thdrContentDisposition = http.CanonicalHeaderKey(\"Content-Disposition\")\n\thdrAuthorizationKey   = http.CanonicalHeaderKey(\"Authorization\")\n\thdrWwwAuthenticateKey = http.CanonicalHeaderKey(\"WWW-Authenticate\")\n\thdrRetryAfterKey      = http.CanonicalHeaderKey(\"Retry-After\")\n\thdrCookieKey          = http.CanonicalHeaderKey(\"Cookie\")\n\n\tplainTextType   = \"text/plain; charset=utf-8\"\n\tjsonContentType = \"application/json\"\n\tformContentType = \"application/x-www-form-urlencoded\"\n\n\tjsonKey = \"json\"\n\txmlKey  = \"xml\"\n\n\tdefaultAuthScheme = \"Bearer\"\n\n\thdrUserAgentValue = \"go-resty/\" + Version + \" (https://resty.dev)\"\n\tbufPool           = &sync.Pool{New: func() any { return &bytes.Buffer{} }}\n)\n\ntype (\n\t// RequestMiddleware type is for request middleware, called before a request is sent\n\tRequestMiddleware func(*Client, *Request) error\n\n\t// ResponseMiddleware type is for response middleware, called after a response has been received\n\tResponseMiddleware func(*Client, *Response) error\n\n\t// ErrorHook type is for reacting to request errors, called after all retries were attempted\n\tErrorHook func(*Request, error)\n\n\t// SuccessHook type is for reacting to request success\n\tSuccessHook func(*Client, *Response)\n\n\t// CloseHook type is for reacting to client closing\n\tCloseHook func()\n\n\t// RequestFunc type is for extended manipulation of the Request instance\n\tRequestFunc func(*Request) *Request\n\n\t// TLSClientConfiger interface is to configure TLS Client configuration on custom transport\n\t// implemented using [http.RoundTripper]\n\tTLSClientConfiger interface {\n\t\tTLSClientConfig() *tls.Config\n\t\tSetTLSClientConfig(*tls.Config) error\n\t}\n)\n\n// TransportSettings struct is used to define custom dialer and transport\n// values for the Resty client. Please refer to individual\n// struct fields to know the default values.\n//\n// Also, refer to https://pkg.go.dev/net/http#Transport for more details.\ntype TransportSettings struct {\n\t// DialerTimeout, default value is `30` seconds.\n\tDialerTimeout time.Duration\n\n\t// DialerKeepAlive, default value is `30` seconds.\n\tDialerKeepAlive time.Duration\n\n\t// IdleConnTimeout, default value is `90` seconds.\n\tIdleConnTimeout time.Duration\n\n\t// TLSHandshakeTimeout, default value is `10` seconds.\n\tTLSHandshakeTimeout time.Duration\n\n\t// ExpectContinueTimeout, default value is `1` seconds.\n\tExpectContinueTimeout time.Duration\n\n\t// ResponseHeaderTimeout, added to provide ability to\n\t// set value. No default value in Resty, the Go\n\t// HTTP client default value applies.\n\tResponseHeaderTimeout time.Duration\n\n\t// MaxIdleConns, default value is `100`.\n\tMaxIdleConns int\n\n\t// MaxIdleConnsPerHost, default value is `runtime.GOMAXPROCS(0) + 1`.\n\tMaxIdleConnsPerHost int\n\n\t// MaxConnsPerHost, default value is no limit.\n\tMaxConnsPerHost int\n\n\t// DisableKeepAlives, default value is `false`.\n\tDisableKeepAlives bool\n\n\t// MaxResponseHeaderBytes, added to provide ability to\n\t// set value. No default value in Resty, the Go\n\t// HTTP client default value applies.\n\tMaxResponseHeaderBytes int64\n\n\t// WriteBufferSize, added to provide ability to\n\t// set value. No default value in Resty, the Go\n\t// HTTP client default value applies.\n\tWriteBufferSize int\n\n\t// ReadBufferSize, added to provide ability to\n\t// set value. No default value in Resty, the Go\n\t// HTTP client default value applies.\n\tReadBufferSize int\n}\n\n// Client struct is used to create a Resty client with client-level settings,\n// these settings apply to all the requests raised from the client.\n//\n// Resty also provides an option to override most of the client settings\n// at [Request] level.\ntype Client struct {\n\tlock                       *sync.RWMutex\n\tbaseURL                    string\n\tqueryParams                url.Values\n\tformData                   url.Values\n\tpathParams                 map[string]string\n\theader                     http.Header\n\tcredentials                *credentials\n\tauthToken                  string\n\tauthScheme                 string\n\tcookies                    []*http.Cookie\n\terrorType                  reflect.Type\n\tdebug                      bool\n\tdisableWarn                bool\n\tisMethodGetAllowPayload    bool\n\tisMethodDeleteAllowPayload bool\n\ttimeout                    time.Duration\n\tretryCount                 int\n\tretryWaitTime              time.Duration\n\tretryMaxWaitTime           time.Duration\n\tretryConditions            []RetryConditionFunc\n\tretryHooks                 []RetryHookFunc\n\tretryDelayStrategy         RetryDelayStrategyFunc\n\tisRetryDefaultConditions   bool\n\tisRetryAllowNonIdempotent  bool\n\theaderAuthorizationKey     string\n\tresponseBodyLimit          int64\n\tresBodyUnlimitedReads      bool\n\tjsonEscapeHTML             bool\n\tcloseConnection            bool\n\tisResponseDoNotParse       bool\n\tisTrace                    bool\n\tdebugBodyLimit             int\n\tresponseSaveDirectory      string\n\tisResponseSaveToFile       bool\n\tscheme                     string\n\tlog                        Logger\n\tctx                        context.Context\n\thttpClient                 *http.Client\n\tproxyURL                   *url.URL\n\tdebugLogFormatter          DebugLogFormatterFunc\n\tdebugLogCallback           DebugLogCallbackFunc\n\tisCurlCmdGenerate          bool\n\tisCurlCmdDebugLog          bool\n\tunescapeQueryParams        bool\n\tloadBalancer               LoadBalancer\n\tbeforeRequest              []RequestMiddleware\n\tafterResponse              []ResponseMiddleware\n\terrorHooks                 []ErrorHook\n\tinvalidHooks               []ErrorHook\n\tpanicHooks                 []ErrorHook\n\tsuccessHooks               []SuccessHook\n\tcloseHooks                 []CloseHook\n\tcontentTypeEncoders        map[string]ContentTypeEncoder\n\tcontentTypeDecoders        map[string]ContentTypeDecoder\n\tcontentDecompresserKeys    []string\n\tcontentDecompressers       map[string]ContentDecompresser\n\tcertWatcherStopChan        chan bool\n\tcircuitBreaker             *CircuitBreaker\n\thedging                    *Hedging\n}\n\n// CertWatcherOptions allows configuring a watcher that reloads dynamically TLS certs.\ntype CertWatcherOptions struct {\n\t// PoolInterval is the frequency at which resty will check if the PEM file needs to be reloaded.\n\t// Default is 24 hours.\n\tPoolInterval time.Duration\n}\n\n//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\n// Client methods\n//___________________________________\n\n// BaseURL method returns the Base URL value from the client instance.\nfunc (c *Client) BaseURL() string {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.baseURL\n}\n\n// SetBaseURL method sets the Base URL in the client instance. It will be used with a request\n// raised from this client with a relative URL\n//\n//\t// Setting HTTP address\n//\tclient.SetBaseURL(\"http://myjeeva.com\")\n//\n//\t// Setting HTTPS address\n//\tclient.SetBaseURL(\"https://myjeeva.com\")\nfunc (c *Client) SetBaseURL(url string) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.baseURL = strings.TrimRight(url, \"/\")\n\treturn c\n}\n\n// LoadBalancer method returns the request load balancer instance from the client\n// instance. Otherwise returns nil.\nfunc (c *Client) LoadBalancer() LoadBalancer {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.loadBalancer\n}\n\n// SetLoadBalancer method is used to set the new request load balancer into the client.\nfunc (c *Client) SetLoadBalancer(b LoadBalancer) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.loadBalancer = b\n\treturn c\n}\n\n// Header method returns the headers from the client instance.\nfunc (c *Client) Header() http.Header {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.header\n}\n\n// SetHeader method sets a single header and its value in the client instance.\n// These headers will be applied to all requests raised from the client instance.\n// Also, it can be overridden by request-level header options.\n//\n// For Example: To set `Content-Type` and `Accept` as `application/json`\n//\n//\tclient.\n//\t\tSetHeader(\"Content-Type\", \"application/json\").\n//\t\tSetHeader(\"Accept\", \"application/json\")\n//\n// See [Request.SetHeader] or [Request.SetHeaders].\nfunc (c *Client) SetHeader(header, value string) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.header.Set(header, value)\n\treturn c\n}\n\n// SetHeaderAny method sets a single header field and its value in the client instance\n// for all requests raised from the client.\n//\n// It is similar to [Client.SetHeader] but accepts any type as the value and converts\n// it to a string using predefined formatting rules (integers, bools, time.Time, etc.).\n//\n// For Example: To set `X-Request-Id` with an integer value\n//\n//\tclient.SetHeaderAny(\"X-Request-Id\", 12345)\n//\n// See [Request.SetHeaderAny] or [Client.SetHeader].\nfunc (c *Client) SetHeaderAny(header string, value any) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tstrVal := formatAnyToString(value)\n\tc.header.Set(header, strVal)\n\treturn c\n}\n\n// SetHeaders method sets multiple headers and their values at one go, and\n// these headers will be applied to all requests raised from the client instance.\n// Also, it can be overridden at request-level headers options.\n//\n// For Example: To set `Content-Type` and `Accept` as `application/json`\n//\n//\tclient.SetHeaders(map[string]string{\n//\t\t\"Content-Type\": \"application/json\",\n//\t\t\"Accept\": \"application/json\",\n//\t})\n//\n// See [Request.SetHeaders] or [Request.SetHeader].\nfunc (c *Client) SetHeaders(headers map[string]string) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tfor h, v := range headers {\n\t\tc.header.Set(h, v)\n\t}\n\treturn c\n}\n\n// SetHeaderVerbatim method is used to set the HTTP header key and value verbatim in the current request.\n// It is typically helpful for legacy applications or servers that require HTTP headers in a certain way\n//\n// For Example: To set header key as `all_lowercase`, `UPPERCASE`, and `x-cloud-trace-id`\n//\n//\tclient.\n//\t\tSetHeaderVerbatim(\"all_lowercase\", \"available\").\n//\t\tSetHeaderVerbatim(\"UPPERCASE\", \"available\").\n//\t\tSetHeaderVerbatim(\"x-cloud-trace-id\", \"798e94019e5fc4d57fbb8901eb4c6cae\")\n//\n// See [Request.SetHeaderVerbatim].\nfunc (c *Client) SetHeaderVerbatim(header, value string) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.header[header] = []string{value}\n\treturn c\n}\n\n// SetHeaderVerbatimAny method sets the HTTP header key and value verbatim in the client instance\n// for all requests raised from the client.\n//\n// It is similar to [Client.SetHeaderVerbatim] but accepts any type as the value and converts\n// it to a string using predefined formatting rules (integers, bools, time.Time, etc.).\n//\n// For Example: To set header key as `x-trace-id` with an integer value\n//\n//\tclient.SetHeaderVerbatimAny(\"x-trace-id\", 798940)\n//\n// See [Request.SetHeaderVerbatimAny] or [Client.SetHeaderVerbatim].\nfunc (c *Client) SetHeaderVerbatimAny(header string, value any) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tstrVal := formatAnyToString(value)\n\tc.header[header] = []string{strVal}\n\treturn c\n}\n\n// Context method returns the [context.Context] from the client instance.\nfunc (c *Client) Context() context.Context {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.ctx\n}\n\n// SetContext method sets the given [context.Context] in the client instance and\n// it gets added to [Request] raised from this instance.\nfunc (c *Client) SetContext(ctx context.Context) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.ctx = ctx\n\treturn c\n}\n\n// CookieJar method returns the HTTP cookie jar instance from the underlying Go HTTP Client.\nfunc (c *Client) CookieJar() http.CookieJar {\n\treturn c.Client().Jar\n}\n\n// SetCookieJar method sets custom [http.CookieJar] in the resty client. It's a way to override the default.\n//\n// For Example, sometimes we don't want to save cookies in API mode so that we can remove the default\n// CookieJar in resty client.\n//\n//\tclient.SetCookieJar(nil)\nfunc (c *Client) SetCookieJar(jar http.CookieJar) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.httpClient.Jar = jar\n\treturn c\n}\n\n// Cookies method returns all cookies registered in the client instance.\nfunc (c *Client) Cookies() []*http.Cookie {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.cookies\n}\n\n// SetCookie method appends a single cookie to the client instance.\n// These cookies will be added to all the requests from this client instance.\n//\n//\tclient.SetCookie(&http.Cookie{\n//\t\tName:\"go-resty\",\n//\t\tValue:\"This is cookie value\",\n//\t})\nfunc (c *Client) SetCookie(hc *http.Cookie) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.cookies = append(c.cookies, hc)\n\treturn c\n}\n\n// SetCookies method sets an array of cookies in the client instance.\n// These cookies will be added to all the requests from this client instance.\n//\n//\tcookies := []*http.Cookie{\n//\t\t&http.Cookie{\n//\t\t\tName:\"go-resty-1\",\n//\t\t\tValue:\"This is cookie 1 value\",\n//\t\t},\n//\t\t&http.Cookie{\n//\t\t\tName:\"go-resty-2\",\n//\t\t\tValue:\"This is cookie 2 value\",\n//\t\t},\n//\t}\n//\n//\t// Setting a cookies into resty\n//\tclient.SetCookies(cookies)\nfunc (c *Client) SetCookies(cs []*http.Cookie) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.cookies = append(c.cookies, cs...)\n\treturn c\n}\n\n// QueryParams method returns all query parameters and their values from the client instance.\nfunc (c *Client) QueryParams() url.Values {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.queryParams\n}\n\n// SetQueryParam method sets a single parameter and its value in the client instance.\n// It will be formed as a query string for the request.\n//\n//\tFor Example: `search=kitchen%20papers&size=large`\n//\n// In the URL after the `?` mark. These query params will be added to all the requests raised from\n// this client instance. Also, it can be overridden at the request level.\n//\n// See [Request.SetQueryParam] or [Request.SetQueryParams].\n//\n//\tclient.\n//\t\tSetQueryParam(\"search\", \"kitchen papers\").\n//\t\tSetQueryParam(\"size\", \"large\")\nfunc (c *Client) SetQueryParam(param, value string) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.queryParams.Set(param, value)\n\treturn c\n}\n\n// SetQueryParamAny method sets a single query parameter and its value in the client instance.\n// It will be formed as a query string for the request.\n//\n// It is similar to [Client.SetQueryParam] but accepts any type as the value and converts\n// it to a string using predefined formatting rules (integers, bools, time.Time, etc.).\n//\n// For Example: To set `page` and `active` query parameters\n//\n//\tclient.\n//\t\tSetQueryParamAny(\"page\", 5).\n//\t\tSetQueryParamAny(\"active\", true)\n//\n// See [Request.SetQueryParamAny] or [Client.SetQueryParam].\nfunc (c *Client) SetQueryParamAny(param string, value any) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tstrVal := formatAnyToString(value)\n\tc.queryParams.Set(param, strVal)\n\treturn c\n}\n\n// SetQueryParams method sets multiple parameters and their values at one go in the client instance.\n// It will be formed as a query string for the request.\n//\n//\tFor Example: `search=kitchen%20papers&size=large`\n//\n// In the URL after the `?` mark. These query params will be added to all the requests raised from this\n// client instance. Also, it can be overridden at the request level.\n//\n// See [Request.SetQueryParams] or [Request.SetQueryParam].\n//\n//\tclient.SetQueryParams(map[string]string{\n//\t\t\"search\": \"kitchen papers\",\n//\t\t\"size\": \"large\",\n//\t})\nfunc (c *Client) SetQueryParams(params map[string]string) *Client {\n\t// Do not lock here since there is potential deadlock.\n\tfor p, v := range params {\n\t\tc.SetQueryParam(p, v)\n\t}\n\treturn c\n}\n\n// FormData method returns the form parameters and their values from the client instance.\nfunc (c *Client) FormData() url.Values {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.formData\n}\n\n// SetFormData method sets Form parameters and their values in the client instance.\n// The request content type would be set as `application/x-www-form-urlencoded`.\n// The client-level form data gets added to all the requests. Also, it can be\n// overridden at the request level.\n//\n// See [Request.SetFormData].\n//\n//\tclient.SetFormData(map[string]string{\n//\t\t\"access_token\": \"BC594900-518B-4F7E-AC75-BD37F019E08F\",\n//\t\t\"user_id\": \"3455454545\",\n//\t})\nfunc (c *Client) SetFormData(data map[string]string) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tfor k, v := range data {\n\t\tc.formData.Set(k, v)\n\t}\n\treturn c\n}\n\n// SetBasicAuth method sets the basic authentication header in the HTTP request. For Example:\n//\n//\tAuthorization: Basic <base64-encoded-value>\n//\n// For Example: To set the header for username \"go-resty\" and password \"welcome\"\n//\n//\tclient.SetBasicAuth(\"go-resty\", \"welcome\")\n//\n// This basic auth information is added to all requests from this client instance.\n// It can also be overridden at the request level.\n//\n// See [Request.SetBasicAuth].\nfunc (c *Client) SetBasicAuth(username, password string) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.credentials = &credentials{Username: username, Password: password}\n\treturn c\n}\n\n// AuthToken method returns the auth token value registered in the client instance.\nfunc (c *Client) AuthToken() string {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.authToken\n}\n\n// HeaderAuthorizationKey method returns the HTTP header name for Authorization from the client instance.\nfunc (c *Client) HeaderAuthorizationKey() string {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.headerAuthorizationKey\n}\n\n// SetHeaderAuthorizationKey method sets the given HTTP header name for Authorization in the client instance.\n//\n// It can be overridden at the request level; see [Request.SetHeaderAuthorizationKey].\n//\n//\tclient.SetHeaderAuthorizationKey(\"X-Custom-Authorization\")\nfunc (c *Client) SetHeaderAuthorizationKey(k string) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.headerAuthorizationKey = k\n\treturn c\n}\n\n// SetAuthToken method sets the auth token of the `Authorization` header for all HTTP requests.\n// The default auth scheme is `Bearer`; it can be customized with the method [Client.SetAuthScheme]. For Example:\n//\n//\tAuthorization: <auth-scheme> <auth-token-value>\n//\n// For Example: To set auth token BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F\n//\n//\tclient.SetAuthToken(\"BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F\")\n//\n// This auth token gets added to all the requests raised from this client instance.\n// Also, it can be overridden at the request level.\n//\n// See [Request.SetAuthToken].\nfunc (c *Client) SetAuthToken(token string) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.authToken = token\n\treturn c\n}\n\n// AuthScheme method returns the auth scheme name set in the client instance.\n//\n// See [Client.SetAuthScheme], [Request.SetAuthScheme].\nfunc (c *Client) AuthScheme() string {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.authScheme\n}\n\n// SetAuthScheme method sets the auth scheme type in the HTTP request. For Example:\n//\n//\tAuthorization: <auth-scheme-value> <auth-token-value>\n//\n// For Example: To set the scheme to use OAuth\n//\n//\tclient.SetAuthScheme(\"OAuth\")\n//\n// This auth scheme gets added to all the requests raised from this client instance.\n// Also, it can be overridden at the request level.\n//\n// Information about auth schemes can be found in [RFC 7235], IANA [HTTP Auth schemes].\n//\n// See [Request.SetAuthScheme].\n//\n// [RFC 7235]: https://tools.ietf.org/html/rfc7235\n// [HTTP Auth schemes]: https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes\nfunc (c *Client) SetAuthScheme(scheme string) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.authScheme = scheme\n\treturn c\n}\n\n// SetDigestAuth method sets the Digest Auth transport with provided credentials in the client.\n// If a server responds with 401 and sends a Digest challenge in the header `WWW-Authenticate`,\n// the request will be resent with the appropriate digest `Authorization` header.\n//\n// For Example: To set the Digest scheme with user \"Mufasa\" and password \"Circle Of Life\"\n//\n//\tclient.SetDigestAuth(\"Mufasa\", \"Circle Of Life\")\n//\n// Information about Digest Access Authentication can be found in [RFC 7616].\n//\n// NOTE:\n//   - On the QOP `auth-int` scenario, the request body is read into memory to\n//     compute the body hash that increases memory usage.\n//   - Create a dedicated client instance to use digest auth,\n//     as it does digest auth for all the requests raised by the client.\n//\n// [RFC 7616]: https://datatracker.ietf.org/doc/html/rfc7616\nfunc (c *Client) SetDigestAuth(username, password string) *Client {\n\tdt := &digestTransport{\n\t\tcredentials: &credentials{username, password},\n\t\ttransport:   c.Transport(),\n\t}\n\tc.SetTransport(dt)\n\treturn c\n}\n\n// R method creates a new request instance; it's used for Get, Post, Put, Delete, Patch, Head, Options, etc.\nfunc (c *Client) R() *Request {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\tr := &Request{\n\t\tQueryParams:                  url.Values{},\n\t\tFormData:                     url.Values{},\n\t\tHeader:                       http.Header{},\n\t\tCookies:                      make([]*http.Cookie, 0),\n\t\tPathParams:                   make(map[string]string),\n\t\tTimeout:                      c.timeout,\n\t\tIsDebug:                      c.debug,\n\t\tIsTrace:                      c.isTrace,\n\t\tIsResponseSaveToFile:         c.isResponseSaveToFile,\n\t\tAuthScheme:                   c.authScheme,\n\t\tAuthToken:                    c.authToken,\n\t\tRetryCount:                   c.retryCount,\n\t\tRetryWaitTime:                c.retryWaitTime,\n\t\tRetryMaxWaitTime:             c.retryMaxWaitTime,\n\t\tRetryDelayStrategy:           c.retryDelayStrategy,\n\t\tIsRetryDefaultConditions:     c.isRetryDefaultConditions,\n\t\tIsCloseConnection:            c.closeConnection,\n\t\tIsResponseDoNotParse:         c.isResponseDoNotParse,\n\t\tDebugBodyLimit:               c.debugBodyLimit,\n\t\tResponseBodyLimit:            c.responseBodyLimit,\n\t\tIsResponseBodyUnlimitedReads: c.resBodyUnlimitedReads,\n\t\tIsMethodGetAllowPayload:      c.isMethodGetAllowPayload,\n\t\tIsMethodDeleteAllowPayload:   c.isMethodDeleteAllowPayload,\n\t\tIsRetryAllowNonIdempotent:    c.isRetryAllowNonIdempotent,\n\t\tHeaderAuthorizationKey:       c.headerAuthorizationKey,\n\n\t\tmu:                  new(sync.Mutex),\n\t\tclient:              c,\n\t\tbaseURL:             c.baseURL,\n\t\tmultipartFields:     make([]*MultipartField, 0),\n\t\tjsonEscapeHTML:      c.jsonEscapeHTML,\n\t\tlog:                 c.log,\n\t\tisCurlCmdGenerate:   c.isCurlCmdGenerate,\n\t\tisCurlCmdDebugLog:   c.isCurlCmdDebugLog,\n\t\tunescapeQueryParams: c.unescapeQueryParams,\n\t\tcredentials:         c.credentials,\n\t}\n\n\tif c.ctx != nil {\n\t\tr.ctx = context.WithoutCancel(c.ctx) // refer to godoc for more info about this function\n\t}\n\n\treturn r\n}\n\n// NewRequest method is an alias for method `R()`.\nfunc (c *Client) NewRequest() *Request {\n\treturn c.R()\n}\n\n// SetRequestMiddlewares method allows Resty users to override the default request\n// middlewares sequence\n//\n//\tclient.SetRequestMiddlewares(\n//\t\tCustom1RequestMiddleware,\n//\t\tCustom2RequestMiddleware,\n//\t\tresty.PrepareRequestMiddleware, // after this, `Request.RawRequest` instance is available\n//\t\tCustom3RequestMiddleware,\n//\t\tCustom4RequestMiddleware,\n//\t)\n//\n// See, [Client.AddRequestMiddleware]\n//\n// NOTE:\n//   - It overwrites the existing request middleware list.\n//   - Be sure to include Resty request middlewares in the request chain at the appropriate spot.\nfunc (c *Client) SetRequestMiddlewares(middlewares ...RequestMiddleware) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.beforeRequest = middlewares\n\treturn c\n}\n\n// SetResponseMiddlewares method allows Resty users to override the default response\n// middlewares sequence\n//\n//\tclient.SetResponseMiddlewares(\n//\t\tCustom1ResponseMiddleware,\n//\t\tCustom2ResponseMiddleware,\n//\t\tresty.AutoParseResponseMiddleware, // before this, the body is not read except on the debug flow\n//\t\tCustom3ResponseMiddleware,\n//\t\tresty.SaveToFileResponseMiddleware, // See, Request.SetOutputFileName, Request.SetSaveResponse\n//\t\tCustom4ResponseMiddleware,\n//\t\tCustom5ResponseMiddleware,\n//\t)\n//\n// See, [Client.AddResponseMiddleware]\n//\n// NOTE:\n//   - It overwrites the existing response middleware list.\n//   - Be sure to include Resty response middlewares in the response chain at the appropriate spot.\nfunc (c *Client) SetResponseMiddlewares(middlewares ...ResponseMiddleware) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.afterResponse = middlewares\n\treturn c\n}\n\nfunc (c *Client) requestMiddlewares() []RequestMiddleware {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.beforeRequest\n}\n\n// AddRequestMiddleware method appends a request middleware to the before request chain.\n// After all requests, middlewares are applied, and the request is sent to the host server.\n//\n//\tclient.AddRequestMiddleware(func(c *resty.Client, r *resty.Request) error {\n//\t\t// Now you have access to the Client and Request instance\n//\t\t// manipulate it as per your need\n//\n//\t\treturn nil \t// if its successful otherwise return error\n//\t})\nfunc (c *Client) AddRequestMiddleware(m RequestMiddleware) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tidx := len(c.beforeRequest) - 1\n\tc.beforeRequest = slices.Insert(c.beforeRequest, idx, m)\n\treturn c\n}\n\nfunc (c *Client) responseMiddlewares() []ResponseMiddleware {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.afterResponse\n}\n\n// AddResponseMiddleware method appends response middleware to the after-response chain.\n// All the response middlewares are applied; once we receive a response\n// from the host server.\n//\n//\tclient.AddResponseMiddleware(func(c *resty.Client, r *resty.Response) error {\n//\t\t// Now you have access to the Client and Response instance\n//\t\t// Also, you could access request via Response.Request i.e., r.Request\n//\t\t// manipulate it as per your need\n//\n//\t\treturn nil \t// if its successful otherwise return error\n//\t})\nfunc (c *Client) AddResponseMiddleware(m ResponseMiddleware) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.afterResponse = append(c.afterResponse, m)\n\treturn c\n}\n\n// OnError method adds a callback that will be run whenever a request execution fails.\n// This is called after all retries have been attempted (if any).\n// If there was a response from the server, the error will be wrapped in [ResponseError]\n// which has the last response received from the server.\n//\n//\tclient.OnError(func(req *resty.Request, err error) {\n//\t\tif v, ok := err.(*resty.ResponseError); ok {\n//\t\t\t// Do something with v.Response\n//\t\t}\n//\t\t// Log the error, increment a metric, etc...\n//\t})\n//\n// Out of the [Client.OnSuccess], [Client.OnError], [Client.OnInvalid], [Client.OnPanic]\n// callbacks, exactly one set will be invoked for each call to [Request.Execute] that completes.\n//\n// NOTE:\n//   - Do not use [Client] setter methods within OnError hooks; deadlock will happen.\nfunc (c *Client) OnError(hooks ...ErrorHook) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.errorHooks = append(c.errorHooks, hooks...)\n\treturn c\n}\n\n// OnSuccess method adds a callback that will be run whenever a request execution\n// succeeds.  This is called after all retries have been attempted (if any).\n//\n// Out of the [Client.OnSuccess], [Client.OnError], [Client.OnInvalid], [Client.OnPanic]\n// callbacks, exactly one set will be invoked for each call to [Request.Execute] that completes.\n//\n// NOTE:\n//   - Do not use [Client] setter methods within OnSuccess hooks; deadlock will happen.\nfunc (c *Client) OnSuccess(hooks ...SuccessHook) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.successHooks = append(c.successHooks, hooks...)\n\treturn c\n}\n\n// OnInvalid method adds a callback that will be run whenever a request execution\n// fails before it starts because the request is invalid.\n//\n// Out of the [Client.OnSuccess], [Client.OnError], [Client.OnInvalid], [Client.OnPanic]\n// callbacks, exactly one set will be invoked for each call to [Request.Execute] that completes.\n//\n// NOTE:\n//   - Do not use [Client] setter methods within OnInvalid hooks; deadlock will happen.\nfunc (c *Client) OnInvalid(hooks ...ErrorHook) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.invalidHooks = append(c.invalidHooks, hooks...)\n\treturn c\n}\n\n// OnPanic method adds a callback that will be run whenever a request execution\n// panics.\n//\n// Out of the [Client.OnSuccess], [Client.OnError], [Client.OnInvalid], [Client.OnPanic]\n// callbacks, exactly one set will be invoked for each call to [Request.Execute] that completes.\n//\n// If an [Client.OnSuccess], [Client.OnError], or [Client.OnInvalid] callback panics,\n// then exactly one rule can be violated.\n//\n// NOTE:\n//   - Do not use [Client] setter methods within OnPanic hooks; deadlock will happen.\nfunc (c *Client) OnPanic(hooks ...ErrorHook) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.panicHooks = append(c.panicHooks, hooks...)\n\treturn c\n}\n\n// OnClose method adds a callback that will be run whenever the client is closed.\n// The hooks are executed in the order they were registered.\nfunc (c *Client) OnClose(hooks ...CloseHook) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.closeHooks = append(c.closeHooks, hooks...)\n\treturn c\n}\n\n// ContentTypeEncoders method returns all the registered content type encoders.\nfunc (c *Client) ContentTypeEncoders() map[string]ContentTypeEncoder {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.contentTypeEncoders\n}\n\n// AddContentTypeEncoder method adds the user-provided Content-Type encoder into a client.\n//\n// NOTE: It overwrites the encoder function if the given Content-Type key already exists.\nfunc (c *Client) AddContentTypeEncoder(ct string, e ContentTypeEncoder) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.contentTypeEncoders[strings.ToLower(ct)] = e\n\treturn c\n}\n\nfunc (c *Client) inferContentTypeEncoder(ct ...string) (ContentTypeEncoder, bool) {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\tfor _, v := range ct {\n\t\tif d, f := c.contentTypeEncoders[v]; f {\n\t\t\treturn d, f\n\t\t}\n\t}\n\treturn nil, false\n}\n\n// ContentTypeDecoders method returns all the registered content type decoders.\nfunc (c *Client) ContentTypeDecoders() map[string]ContentTypeDecoder {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.contentTypeDecoders\n}\n\n// AddContentTypeDecoder method adds the user-provided Content-Type decoder into a client.\n//\n// NOTE: It overwrites the decoder function if the given Content-Type key already exists.\nfunc (c *Client) AddContentTypeDecoder(ct string, d ContentTypeDecoder) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.contentTypeDecoders[strings.ToLower(ct)] = d\n\treturn c\n}\n\nfunc (c *Client) inferContentTypeDecoder(ct ...string) (ContentTypeDecoder, bool) {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\tfor _, v := range ct {\n\t\tif d, f := c.contentTypeDecoders[v]; f {\n\t\t\treturn d, f\n\t\t}\n\t}\n\treturn nil, false\n}\n\n// ContentDecompressers method returns all the registered content-encoding Decompressers.\nfunc (c *Client) ContentDecompressers() map[string]ContentDecompresser {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.contentDecompressers\n}\n\n// AddContentDecompresser method adds the user-provided Content-Encoding ([RFC 9110]) Decompresser\n// and directive into a client.\n//\n// NOTE: It overwrites the Decompresser function if the given Content-Encoding directive already exists.\n//\n// [RFC 9110]: https://datatracker.ietf.org/doc/html/rfc9110\nfunc (c *Client) AddContentDecompresser(k string, d ContentDecompresser) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tlk := strings.ToLower(k)\n\tif !slices.Contains(c.contentDecompresserKeys, lk) {\n\t\tc.contentDecompresserKeys = slices.Insert(c.contentDecompresserKeys, 0, lk)\n\t}\n\tc.contentDecompressers[lk] = d\n\treturn c\n}\n\n// ContentDecompresserKeys method returns all the registered content-encoding Decompressers\n// keys as comma-separated string.\nfunc (c *Client) ContentDecompresserKeys() string {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn strings.Join(c.contentDecompresserKeys, \", \")\n}\n\n// SetContentDecompresserKeys method sets given Content-Encoding ([RFC 9110]) directives into the client instance.\n//\n// It checks the given Content-Encoding exists in the [ContentDecompresser] list before assigning it,\n// if it does not exist, it will skip that directive.\n//\n// Use this method to overwrite the default order. If a new content Decompresser is added,\n// that directive will be the first.\n//\n// [RFC 9110]: https://datatracker.ietf.org/doc/html/rfc9110\nfunc (c *Client) SetContentDecompresserKeys(keys []string) *Client {\n\tresult := make([]string, 0)\n\tdecoders := c.ContentDecompressers()\n\tfor _, k := range keys {\n\t\tk = strings.ToLower(k)\n\t\tif _, f := decoders[k]; f {\n\t\t\tresult = append(result, k)\n\t\t}\n\t}\n\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.contentDecompresserKeys = result\n\treturn c\n}\n\n// SetCircuitBreaker method sets the Circuit Breaker instance into the client.\n// It is used to prevent the client from sending requests that are likely to fail.\n// For Example: To use the default Circuit Breaker:\n//\n//\tclient.SetCircuitBreaker(NewCircuitBreaker())\nfunc (c *Client) SetCircuitBreaker(b *CircuitBreaker) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.circuitBreaker = b\n\treturn c\n}\n\n// IsDebug method returns `true` if the client is in debug mode; otherwise, it is `false`.\nfunc (c *Client) IsDebug() bool {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.debug\n}\n\n// SetDebug method is used to turn on/off the debug mode on the Resty client instance. It logs details\n// of every request and response when enabled.\n//\n//\tclient.SetDebug(true)\n//\n// Also, it can be enabled at the request level for a particular request; see [Request.SetDebug].\n//   - For [Request], it logs information such as HTTP verb, Relative URL path,\n//     Host, Headers, and Body if it has one.\n//   - For [Response], it logs information such as Status, Response Time, Headers,\n//     and Body if it has one.\nfunc (c *Client) SetDebug(d bool) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.debug = d\n\treturn c\n}\n\n// DebugBodyLimit method returns the debug body limit value set on the client instance\nfunc (c *Client) DebugBodyLimit() int {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.debugBodyLimit\n}\n\n// SetDebugBodyLimit sets the maximum size in bytes for which the response and\n// request body will be logged in debug mode.\n//\n//\tclient.SetDebugBodyLimit(1000000)\nfunc (c *Client) SetDebugBodyLimit(sl int) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.debugBodyLimit = sl\n\treturn c\n}\n\nfunc (c *Client) debugLogCallbackFunc() DebugLogCallbackFunc {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.debugLogCallback\n}\n\n// OnDebugLog method sets the debug log callback function to the client instance.\n// Registered callback gets called before the Resty logs the information.\nfunc (c *Client) OnDebugLog(dlc DebugLogCallbackFunc) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tif c.debugLogCallback != nil {\n\t\tc.log.Warnf(\"Overwriting an existing on-debug-log callback from=%s to=%s\",\n\t\t\tfunctionName(c.debugLogCallback), functionName(dlc))\n\t}\n\tc.debugLogCallback = dlc\n\treturn c\n}\n\nfunc (c *Client) debugLogFormatterFunc() DebugLogFormatterFunc {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.debugLogFormatter\n}\n\n// SetDebugLogFormatter method sets the Resty debug log formatter to the client instance.\nfunc (c *Client) SetDebugLogFormatter(df DebugLogFormatterFunc) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.debugLogFormatter = df\n\treturn c\n}\n\n// IsDisableWarn method returns `true` if the warning message is disabled; otherwise, it is `false`.\nfunc (c *Client) IsDisableWarn() bool {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.disableWarn\n}\n\n// SetLoggerWarnLevel method disables the warning log message on the Resty client.\n//\n// For example, Resty warns users when BasicAuth is used in non-TLS mode.\n//\n//\tclient.SetLoggerWarnLevel(true)\nfunc (c *Client) SetLoggerWarnLevel(d bool) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.disableWarn = d\n\treturn c\n}\n\n// IsMethodGetAllowPayload method returns `true` if the client is enabled to allow\n// payload with GET method; otherwise, it is `false`.\nfunc (c *Client) IsMethodGetAllowPayload() bool {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.isMethodGetAllowPayload\n}\n\n// SetMethodGetAllowPayload method allows the GET method with payload on the Resty client.\n// By default, Resty does not allow.\n//\n//\tclient.SetMethodGetAllowPayload(true)\n//\n// It can be overridden at the request level. See [Request.SetMethodGetAllowPayload]\nfunc (c *Client) SetMethodGetAllowPayload(allow bool) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.isMethodGetAllowPayload = allow\n\treturn c\n}\n\n// IsMethodDeleteAllowPayload method returns `true` if the client is enabled to allow\n// payload with DELETE method; otherwise, it is `false`.\n//\n// More info, refer to GH#881\nfunc (c *Client) IsMethodDeleteAllowPayload() bool {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.isMethodDeleteAllowPayload\n}\n\n// SetMethodDeleteAllowPayload method allows the DELETE method with payload on the Resty client.\n// By default, Resty does not allow.\n//\n//\tclient.SetMethodDeleteAllowPayload(true)\n//\n// More info, refer to GH#881\n//\n// It can be overridden at the request level. See [Request.SetMethodDeleteAllowPayload]\nfunc (c *Client) SetMethodDeleteAllowPayload(allow bool) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.isMethodDeleteAllowPayload = allow\n\treturn c\n}\n\n// Logger method returns the logger instance used by the client instance.\nfunc (c *Client) Logger() Logger {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.log\n}\n\n// SetLogger method sets given writer for logging Resty request and response details.\n//\n// Compliant to interface [resty.Logger]\nfunc (c *Client) SetLogger(l Logger) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.log = l\n\treturn c\n}\n\n// Timeout method returns the timeout duration value from the client\nfunc (c *Client) Timeout() time.Duration {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.timeout\n}\n\n// SetTimeout method is used to set a timeout for a request raised by the client.\n//\n//\tclient.SetTimeout(1 * time.Minute)\n//\n// It can be overridden at the request level. See [Request.SetTimeout]\n//\n// NOTE: Resty uses [context.WithTimeout] on the request, it does not use [http.Client].Timeout\nfunc (c *Client) SetTimeout(timeout time.Duration) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.timeout = timeout\n\treturn c\n}\n\n// ResultError method returns the global or client common `ResultError` object\n// type registered in the client instance.\nfunc (c *Client) ResultError() reflect.Type {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.errorType\n}\n\n// SetResultError method registers the global or client common `ResultError`\n// object type into the client instance. It is used for automatic unmarshalling if\n// the response status code is greater than 399 and the content type is JSON or XML.\n// It can be a pointer or a non-pointer.\n//\n//\tclient.SetResultError(&LoginErrorResponse{})\n//\t// OR\n//\tclient.SetResultError(LoginErrorResponse{})\nfunc (c *Client) SetResultError(v any) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.errorType = inferType(v)\n\treturn c\n}\n\nfunc (c *Client) newErrorInterface() any {\n\te := c.ResultError()\n\tif e == nil {\n\t\treturn e\n\t}\n\treturn reflect.New(e).Interface()\n}\n\n// SetRedirectPolicy method sets the redirect policy for the client. Resty provides ready-to-use\n// redirect policies. Wanna create one for yourself, refer to `redirect.go`.\n//\n//\tclient.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20))\n//\n//\t// Need multiple redirect policies together\n//\tclient.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20), resty.DomainCheckRedirectPolicy(\"host1.com\", \"host2.net\"))\n//\n// NOTE: It overwrites the previous redirect policies in the client instance.\nfunc (c *Client) SetRedirectPolicy(policies ...RedirectPolicy) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error {\n\t\tfor _, p := range policies {\n\t\t\tif err := p.Apply(req, via); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil // looks good, go ahead\n\t}\n\treturn c\n}\n\n// RetryCount method returns the retry count value from the client instance.\nfunc (c *Client) RetryCount() int {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.retryCount\n}\n\n// SetRetryCount method enables retry on Resty client and allows you\n// to set no. of retry count.\n//\n//\tfirst attempt + retry count = total attempts\n//\n// See [Request.SetRetryDelayStrategy]\n//\n// NOTE:\n//   - By default, Resty only does retry on idempotent HTTP verb, [RFC 9110 Section 9.2.2], [RFC 9110 Section 18.2]\n//\n// [RFC 9110 Section 9.2.2]: https://datatracker.ietf.org/doc/html/rfc9110.html#name-idempotent-methods\n// [RFC 9110 Section 18.2]: https://datatracker.ietf.org/doc/html/rfc9110.html#name-method-registration\nfunc (c *Client) SetRetryCount(count int) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.retryCount = count\n\treturn c\n}\n\n// RetryWaitTime method returns the retry wait time that is used to sleep before\n// retrying the request.\nfunc (c *Client) RetryWaitTime() time.Duration {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.retryWaitTime\n}\n\n// SetRetryWaitTime method sets the default wait time for sleep before retrying\n//\n// Default is 100 milliseconds.\nfunc (c *Client) SetRetryWaitTime(waitTime time.Duration) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.retryWaitTime = waitTime\n\treturn c\n}\n\n// RetryMaxWaitTime method returns the retry max wait time that is used to sleep\n// before retrying the request.\nfunc (c *Client) RetryMaxWaitTime() time.Duration {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.retryMaxWaitTime\n}\n\n// SetRetryMaxWaitTime method sets the max wait time for sleep before retrying\n//\n// Default is 2 seconds.\nfunc (c *Client) SetRetryMaxWaitTime(maxWaitTime time.Duration) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.retryMaxWaitTime = maxWaitTime\n\treturn c\n}\n\n// RetryDelayStrategy method returns the retry delay strategy function;\n// otherwise, it is nil.\n//\n// See [Client.SetRetryDelayStrategy]\nfunc (c *Client) RetryDelayStrategy() RetryDelayStrategyFunc {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.retryDelayStrategy\n}\n\n// SetRetryDelayStrategy method used to set the custom Retry delay strategy\n// into Resty client, it is used to get wait time before each retry.\n// It can be overridden at request level, see [Request.SetRetryDelayStrategy]\n//\n// By default, Resty employs the capped exponential backoff with a jitter delay strategy.\nfunc (c *Client) SetRetryDelayStrategy(rs RetryDelayStrategyFunc) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.retryDelayStrategy = rs\n\treturn c\n}\n\n// IsRetryDefaultConditions method returns true if Resty's default retry conditions\n// are enabled otherwise false\n//\n// Default value is `true`\nfunc (c *Client) IsRetryDefaultConditions() bool {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.isRetryDefaultConditions\n}\n\n// SetRetryDefaultConditions method is used to enable/disable the Resty's default\n// retry conditions\n//\n// It can be overridden at request level, see [Request.SetRetryDefaultConditions]\nfunc (c *Client) SetRetryDefaultConditions(b bool) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.isRetryDefaultConditions = b\n\treturn c\n}\n\n// IsRetryAllowNonIdempotent method returns true if the client is enabled to allow\n// non-idempotent HTTP methods retry; otherwise, it is `false`\n//\n// Default value is `false`\nfunc (c *Client) IsRetryAllowNonIdempotent() bool {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.isRetryAllowNonIdempotent\n}\n\n// SetRetryAllowNonIdempotent method is used to enable/disable non-idempotent HTTP\n// methods retry. By default, Resty only allows idempotent HTTP methods, see\n// [RFC 9110 Section 9.2.2], [RFC 9110 Section 18.2]\n//\n// It can be overridden at request level, see [Request.SetRetryAllowNonIdempotent]\n//\n// [RFC 9110 Section 9.2.2]: https://datatracker.ietf.org/doc/html/rfc9110.html#name-idempotent-methods\n// [RFC 9110 Section 18.2]: https://datatracker.ietf.org/doc/html/rfc9110.html#name-method-registration\nfunc (c *Client) SetRetryAllowNonIdempotent(b bool) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.isRetryAllowNonIdempotent = b\n\treturn c\n}\n\n// RetryConditions method returns all the retry condition functions.\nfunc (c *Client) RetryConditions() []RetryConditionFunc {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.retryConditions\n}\n\n// AddRetryConditions method adds one or more retry condition functions into the request.\n// These retry conditions are executed to determine if the request can be retried.\n// The request will retry if any functions return `true`, otherwise return `false`.\n//\n// NOTE:\n//   - Retry conditions are executed on each retry attempt.\n//   - Default retry conditions are executed first.\n//   - Client-level retry conditions are applied to all requests.\n//   - Request-level retry conditions are executed before client-level retry conditions.\n//     See [Request.AddRetryConditions], [Request.SetRetryConditions]\n//   - Once a retry condition returns true, the remaining retry conditions are not executed.\n//   - Retry conditions are executed in the order in which they are added.\nfunc (c *Client) AddRetryConditions(conditions ...RetryConditionFunc) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.retryConditions = append(c.retryConditions, conditions...)\n\treturn c\n}\n\n// RetryHooks method returns all the retry hook functions.\nfunc (c *Client) RetryHooks() []RetryHookFunc {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.retryHooks\n}\n\n// AddRetryHooks method adds one or more side-effecting retry hooks to an array\n// of hooks that will be executed on each retry.\n//\n// NOTE:\n//   - Retry hooks are executed on each retry attempt.\n//   - The request-level retry hooks are executed first before client-level hooks.\n//     See [Request.AddRetryHooks], [Request.SetRetryHooks]\n//   - Retry hooks are executed in the order in which they are added.\nfunc (c *Client) AddRetryHooks(hooks ...RetryHookFunc) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.retryHooks = append(c.retryHooks, hooks...)\n\treturn c\n}\n\n// isHedgingEnabled method returns true if hedging is enabled.\nfunc (c *Client) isHedgingEnabled() bool {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.hedging != nil\n}\n\n// Hedging method returns the hedging configuration of the client.\n// If nil is returned, it means hedging is disabled.\nfunc (c *Client) Hedging() *Hedging {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.hedging\n}\n\n// SetHedging method sets the hedging instance into client. If nil is passed, it disables hedging.\n//\n// See [NewHedging] for more details about the Hedging configuration.\nfunc (c *Client) SetHedging(h *Hedging) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\n\t// if nil is passed, we disable hedging\n\t// by reverting the transport instance\n\tif h == nil {\n\t\tif ht, ok := c.httpClient.Transport.(*Hedging); ok {\n\t\t\tc.httpClient.Transport = ht.transport\n\t\t\tc.hedging = h\n\t\t}\n\t\treturn c\n\t}\n\n\t// enable hedging if its not already enabled\n\n\tcurrentTransport := c.httpClient.Transport\n\tif currentTransport == nil {\n\t\tcurrentTransport = createTransport(nil, nil)\n\t}\n\n\t// If current transport is already a Hedging instance, unwrap it\n\t// to avoid double-wrapping (e.g., when SetHedging is called multiple times)\n\tif hedging, ok := currentTransport.(*Hedging); ok {\n\t\tcurrentTransport = hedging.transport\n\t}\n\n\t// Disable retry by default when hedging is enabled.\n\t// Users can re-enable retry if they want it as a fallback mechanism.\n\tif c.retryCount > 0 {\n\t\tc.log.Warnf(\"Disabling retry (count: %d) as hedging is now enabled.\"+\n\t\t\t\" You can re-enable retry with SetRetryCount() if you really want it as a fallback.\"+\n\t\t\t\" otherwise, hedging and retry requests can overwhelm the server.\", c.retryCount)\n\t\tc.retryCount = 0\n\t}\n\n\th.transport = currentTransport\n\tc.httpClient.Transport = h\n\tc.hedging = h\n\n\treturn c\n}\n\n// TLSClientConfig method returns the [tls.Config] from underlying client transport\n// otherwise returns nil\nfunc (c *Client) TLSClientConfig() *tls.Config {\n\tcfg, err := c.tlsConfig()\n\tif err != nil {\n\t\tc.Logger().Errorf(\"%v\", err)\n\t}\n\treturn cfg\n}\n\n// SetTLSClientConfig method sets TLSClientConfig for underlying client Transport.\n//\n// Values supported by https://pkg.go.dev/crypto/tls#Config can be configured.\n//\n//\t// Disable SSL cert verification for local development\n//\tclient.SetTLSClientConfig(&tls.Config{\n//\t\tInsecureSkipVerify: true\n//\t})\n//\n// NOTE: This method overwrites existing [http.Transport.TLSClientConfig]\nfunc (c *Client) SetTLSClientConfig(tlsConfig *tls.Config) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\n\t// TLSClientConfiger interface handling\n\tif tc, ok := c.httpClient.Transport.(TLSClientConfiger); ok {\n\t\tif err := tc.SetTLSClientConfig(tlsConfig); err != nil {\n\t\t\tc.log.Errorf(\"%v\", err)\n\t\t}\n\t\treturn c\n\t}\n\n\t// default standard transport handling\n\ttransport, ok := c.httpClient.Transport.(*http.Transport)\n\tif !ok {\n\t\tc.log.Errorf(\"SetTLSClientConfig: %v\", ErrNotHttpTransportType)\n\t\treturn c\n\t}\n\ttransport.TLSClientConfig = tlsConfig\n\n\treturn c\n}\n\n// ProxyURL method returns the proxy URL if set otherwise nil.\nfunc (c *Client) ProxyURL() *url.URL {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.proxyURL\n}\n\n// SetProxy method sets the Proxy URL and Port for the Resty client.\n//\n//\t// HTTP/HTTPS proxy\n//\tclient.SetProxy(\"http://proxyserver:8888\")\n//\n//\t// SOCKS5 Proxy\n//\tclient.SetProxy(\"socks5://127.0.0.1:1080\")\n//\n// OR you could also set Proxy via environment variable, refer to [http.ProxyFromEnvironment]\nfunc (c *Client) SetProxy(proxyURL string) *Client {\n\ttransport, err := c.HTTPTransport()\n\tif err != nil {\n\t\tc.Logger().Errorf(\"%v\", err)\n\t\treturn c\n\t}\n\n\tpURL, err := url.Parse(proxyURL)\n\tif err != nil {\n\t\tc.Logger().Errorf(\"%v\", err)\n\t\treturn c\n\t}\n\n\tc.lock.Lock()\n\tc.proxyURL = pURL\n\ttransport.Proxy = http.ProxyURL(c.proxyURL)\n\tc.lock.Unlock()\n\treturn c\n}\n\n// RemoveProxy method removes the proxy configuration from the Resty client\n//\n//\tclient.RemoveProxy()\nfunc (c *Client) RemoveProxy() *Client {\n\ttransport, err := c.HTTPTransport()\n\tif err != nil {\n\t\tc.Logger().Errorf(\"%v\", err)\n\t\treturn c\n\t}\n\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.proxyURL = nil\n\ttransport.Proxy = nil\n\treturn c\n}\n\n// SetCertificateFromFile method helps to set client certificates into Resty\n// from cert and key files to perform SSL client authentication\n//\n//\tclient.SetCertificateFromFile(\"certs/client.pem\", \"certs/client.key\")\nfunc (c *Client) SetCertificateFromFile(certFilePath, certKeyFilePath string) *Client {\n\tcert, err := tls.LoadX509KeyPair(certFilePath, certKeyFilePath)\n\tif err != nil {\n\t\tc.Logger().Errorf(\"client certificate/key parsing error: %v\", err)\n\t\treturn c\n\t}\n\tc.SetCertificates(cert)\n\treturn c\n}\n\n// SetCertificateFromString method helps to set client certificates into Resty\n// from string to perform SSL client authentication\n//\n//\tmyClientCertStr := `-----BEGIN CERTIFICATE-----\n//\t... cert content ...\n//\t-----END CERTIFICATE-----`\n//\n//\tmyClientCertKeyStr := `-----BEGIN PRIVATE KEY-----\n//\t... cert key content ...\n//\t-----END PRIVATE KEY-----`\n//\n//\tclient.SetCertificateFromString(myClientCertStr, myClientCertKeyStr)\nfunc (c *Client) SetCertificateFromString(certStr, certKeyStr string) *Client {\n\tcert, err := tls.X509KeyPair([]byte(certStr), []byte(certKeyStr))\n\tif err != nil {\n\t\tc.Logger().Errorf(\"client certificate/key parsing error: %v\", err)\n\t\treturn c\n\t}\n\tc.SetCertificates(cert)\n\treturn c\n}\n\n// SetCertificates method helps to conveniently set a slice of client certificates\n// into Resty to perform SSL client authentication\n//\n//\tcert, err := tls.LoadX509KeyPair(\"certs/client.pem\", \"certs/client.key\")\n//\tif err != nil {\n//\t\tlog.Printf(\"ERROR client certificate/key parsing error: %v\", err)\n//\t\treturn\n//\t}\n//\n//\tclient.SetCertificates(cert)\nfunc (c *Client) SetCertificates(certs ...tls.Certificate) *Client {\n\tconfig, err := c.tlsConfig()\n\tif err != nil {\n\t\tc.Logger().Errorf(\"%v\", err)\n\t\treturn c\n\t}\n\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tconfig.Certificates = append(config.Certificates, certs...)\n\treturn c\n}\n\n// SetRootCertificates method helps to add one or more root certificate files\n// into the Resty client\n//\n//\t// one pem file path\n//\tclient.SetRootCertificates(\"/path/to/root/pemFile.pem\")\n//\n//\t// one or more pem file path(s)\n//\tclient.SetRootCertificates(\n//\t    \"/path/to/root/pemFile1.pem\",\n//\t    \"/path/to/root/pemFile2.pem\"\n//\t    \"/path/to/root/pemFile3.pem\"\n//\t)\n//\n//\t// if you happen to have string slices\n//\tclient.SetRootCertificates(certs...)\nfunc (c *Client) SetRootCertificates(pemFilePaths ...string) *Client {\n\tfor _, fp := range pemFilePaths {\n\t\trootPemData, err := os.ReadFile(fp)\n\t\tif err != nil {\n\t\t\tc.Logger().Errorf(\"%v\", err)\n\t\t\treturn c\n\t\t}\n\t\tc.handleCAs(\"root\", rootPemData)\n\t}\n\treturn c\n}\n\n// SetRootCertificatesWatcher method enables dynamic reloading of one or more root certificate files.\n// It is designed for scenarios involving long-running Resty clients where certificates may be renewed.\n//\n//\tclient.SetRootCertificatesWatcher(\n//\t\t&resty.CertWatcherOptions{\n//\t\t\tPoolInterval: 24 * time.Hour,\n//\t\t},\n//\t\t\"root-ca.pem\",\n//\t)\nfunc (c *Client) SetRootCertificatesWatcher(options *CertWatcherOptions, pemFilePaths ...string) *Client {\n\tc.SetRootCertificates(pemFilePaths...)\n\tfor _, fp := range pemFilePaths {\n\t\tc.initCertWatcher(fp, \"root\", options)\n\t}\n\treturn c\n}\n\n// SetRootCertificateFromString method helps to add root certificate from the string\n// into the Resty client\n//\n//\tmyRootCertStr := `-----BEGIN CERTIFICATE-----\n//\t... cert content ...\n//\t-----END CERTIFICATE-----`\n//\n//\tclient.SetRootCertificateFromString(myRootCertStr)\nfunc (c *Client) SetRootCertificateFromString(pemCerts string) *Client {\n\tc.handleCAs(\"root\", []byte(pemCerts))\n\treturn c\n}\n\n// SetClientRootCertificates method helps to add one or more client root\n// certificate files into the Resty client\n//\n//\t// one pem file path\n//\tclient.SetClientRootCertificates(\"/path/to/client-root/pemFile.pem\")\n//\n//\t// one or more pem file path(s)\n//\tclient.SetClientRootCertificates(\n//\t    \"/path/to/client-root/pemFile1.pem\",\n//\t    \"/path/to/client-root/pemFile2.pem\"\n//\t    \"/path/to/client-root/pemFile3.pem\"\n//\t)\n//\n//\t// if you happen to have string slices\n//\tclient.SetClientRootCertificates(certs...)\nfunc (c *Client) SetClientRootCertificates(pemFilePaths ...string) *Client {\n\tfor _, fp := range pemFilePaths {\n\t\tpemData, err := os.ReadFile(fp)\n\t\tif err != nil {\n\t\t\tc.Logger().Errorf(\"%v\", err)\n\t\t\treturn c\n\t\t}\n\t\tc.handleCAs(\"client-root\", pemData)\n\t}\n\treturn c\n}\n\n// SetClientRootCertificatesWatcher method enables dynamic reloading of one or more client root certificate files.\n// It is designed for scenarios involving long-running Resty clients where certificates may be renewed.\n//\n//\tclient.SetClientRootCertificatesWatcher(\n//\t\t&resty.CertWatcherOptions{\n//\t\t\tPoolInterval: 24 * time.Hour,\n//\t\t},\n//\t\t\"client-root-ca.pem\",\n//\t)\nfunc (c *Client) SetClientRootCertificatesWatcher(options *CertWatcherOptions, pemFilePaths ...string) *Client {\n\tc.SetClientRootCertificates(pemFilePaths...)\n\tfor _, fp := range pemFilePaths {\n\t\tc.initCertWatcher(fp, \"client-root\", options)\n\t}\n\treturn c\n}\n\n// SetClientRootCertificateFromString method helps to add a client root certificate\n// from the string into the Resty client\n//\n//\tmyClientRootCertStr := `-----BEGIN CERTIFICATE-----\n//\t... cert content ...\n//\t-----END CERTIFICATE-----`\n//\n//\tclient.SetClientRootCertificateFromString(myClientRootCertStr)\nfunc (c *Client) SetClientRootCertificateFromString(pemCerts string) *Client {\n\tc.handleCAs(\"client-root\", []byte(pemCerts))\n\treturn c\n}\n\nfunc (c *Client) handleCAs(scope string, permCerts []byte) {\n\tconfig, err := c.tlsConfig()\n\tif err != nil {\n\t\tc.Logger().Errorf(\"%v\", err)\n\t\treturn\n\t}\n\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tswitch scope {\n\tcase \"root\":\n\t\tif config.RootCAs == nil {\n\t\t\tconfig.RootCAs = x509.NewCertPool()\n\t\t}\n\t\tconfig.RootCAs.AppendCertsFromPEM(permCerts)\n\tcase \"client-root\":\n\t\tif config.ClientCAs == nil {\n\t\t\tconfig.ClientCAs = x509.NewCertPool()\n\t\t}\n\t\tconfig.ClientCAs.AppendCertsFromPEM(permCerts)\n\t}\n}\n\nfunc (c *Client) initCertWatcher(pemFilePath, scope string, options *CertWatcherOptions) {\n\ttickerDuration := defaultWatcherPoolingInterval\n\tif options != nil && options.PoolInterval > 0 {\n\t\ttickerDuration = options.PoolInterval\n\t}\n\n\tgo func() {\n\t\tticker := time.NewTicker(tickerDuration)\n\t\tst, err := os.Stat(pemFilePath)\n\t\tif err != nil {\n\t\t\tc.Logger().Errorf(\"%v\", err)\n\t\t\treturn\n\t\t}\n\n\t\tmodTime := st.ModTime().UTC()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-c.certWatcherStopChan:\n\t\t\t\tticker.Stop()\n\t\t\t\treturn\n\t\t\tcase <-ticker.C:\n\n\t\t\t\tc.debugf(\"Checking if cert %s has changed...\", pemFilePath)\n\n\t\t\t\tst, err = os.Stat(pemFilePath)\n\t\t\t\tif err != nil {\n\t\t\t\t\tc.Logger().Errorf(\"%v\", err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tnewModTime := st.ModTime().UTC()\n\n\t\t\t\tif modTime.Equal(newModTime) {\n\t\t\t\t\tc.debugf(\"Cert %s hasn't changed.\", pemFilePath)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tmodTime = newModTime\n\n\t\t\t\tc.debugf(\"Reloading cert %s ...\", pemFilePath)\n\n\t\t\t\tswitch scope {\n\t\t\t\tcase \"root\":\n\t\t\t\t\tc.SetRootCertificates(pemFilePath)\n\t\t\t\tcase \"client-root\":\n\t\t\t\t\tc.SetClientRootCertificates(pemFilePath)\n\t\t\t\t}\n\n\t\t\t\tc.debugf(\"Cert %s reloaded.\", pemFilePath)\n\t\t\t}\n\t\t}\n\t}()\n}\n\n// ResponseSaveDirectory method returns the output directory value from the client.\nfunc (c *Client) ResponseSaveDirectory() string {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.responseSaveDirectory\n}\n\n// SetResponseSaveDirectory method sets the output directory for saving HTTP responses in a file.\n// Resty creates one if the output directory does not exist. This setting is optional,\n// if you plan to use the absolute path in [Request.SetResponseSaveFileName] and can used together.\n//\n//\tclient.SetResponseSaveDirectory(\"/save/http/response/here\")\nfunc (c *Client) SetResponseSaveDirectory(dirPath string) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.responseSaveDirectory = dirPath\n\treturn c\n}\n\n// IsResponseSaveToFile method returns true if the save response is set to true; otherwise, false\nfunc (c *Client) IsResponseSaveToFile() bool {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.isResponseSaveToFile\n}\n\n// SetResponseSaveToFile method used to enable the save response option at the client level for\n// all requests\n//\n//\tclient.SetResponseSaveToFile(true)\n//\n// Resty determines the save filename in the following order -\n//   - [Request.SetResponseSaveFileName]\n//   - Content-Disposition header\n//   - Request URL using [path.Base]\n//   - Request URL hostname if path is empty or \"/\"\n//\n// It can be overridden at request level, see [Request.SetResponseSaveToFile]\nfunc (c *Client) SetResponseSaveToFile(save bool) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.isResponseSaveToFile = save\n\treturn c\n}\n\n// HTTPTransport method does type assertion and returns [http.Transport]\n// from the client instance, if type assertion fails it returns an error\nfunc (c *Client) HTTPTransport() (*http.Transport, error) {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\tif transport, ok := c.httpClient.Transport.(*http.Transport); ok {\n\t\treturn transport, nil\n\t}\n\treturn nil, ErrNotHttpTransportType\n}\n\n// Transport method returns underlying client transport referance as-is\n// i.e., [http.RoundTripper]\nfunc (c *Client) Transport() http.RoundTripper {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.httpClient.Transport\n}\n\n// SetTransport method sets custom [http.Transport] or any [http.RoundTripper]\n// compatible interface implementation in the Resty client.\n//\n//\ttransport := &http.Transport{\n//\t\t// something like Proxying to httptest.Server, etc...\n//\t\tProxy: func(req *http.Request) (*url.URL, error) {\n//\t\t\treturn url.Parse(server.URL)\n//\t\t},\n//\t}\n//\tclient.SetTransport(transport)\n//\n// NOTE:\n//   - If transport is not the type of [http.Transport], you may lose the\n//     ability to set a few Resty client settings. However, if you implement\n//     [TLSClientConfiger] interface, then TLS client config is possible to set.\n//   - It overwrites the Resty client transport instance and its configurations.\nfunc (c *Client) SetTransport(transport http.RoundTripper) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tif transport != nil {\n\t\tc.httpClient.Transport = transport\n\t}\n\treturn c\n}\n\n// Scheme method returns custom scheme value from the client.\n//\n//\tscheme := client.Scheme()\nfunc (c *Client) Scheme() string {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.scheme\n}\n\n// SetScheme method sets a custom scheme for the Resty client. It's a way to override the default.\n//\n//\tclient.SetScheme(\"http\")\nfunc (c *Client) SetScheme(scheme string) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tif !isStringEmpty(scheme) {\n\t\tc.scheme = strings.TrimSpace(scheme)\n\t}\n\treturn c\n}\n\n// SetCloseConnection method sets variable `Close` in HTTP request struct with the given\n// value. More info: https://golang.org/src/net/http/request.go\n//\n// It can be overridden at the request level, see [Request.SetCloseConnection]\nfunc (c *Client) SetCloseConnection(close bool) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.closeConnection = close\n\treturn c\n}\n\n// SetResponseDoNotParse method instructs Resty not to parse the response body automatically.\n//\n// Resty exposes the raw response body as [io.ReadCloser]. If you use it, do not\n// forget to close the body, otherwise, you might get into connection leaks, and connection\n// reuse may not happen.\n//\n// NOTE: The default [Response] middlewares are not executed when using this option. User\n// takes over the control of handling response body from Resty.\nfunc (c *Client) SetResponseDoNotParse(notParse bool) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.isResponseDoNotParse = notParse\n\treturn c\n}\n\n// PathParams method returns the path parameters from the client.\n//\n//\tpathParams := client.PathParams()\nfunc (c *Client) PathParams() map[string]string {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.pathParams\n}\n\n// SetPathParam method sets a single URL path key-value pair in the\n// Resty client instance.\n//\n//\tclient.SetPathParam(\"userId\", \"sample@sample.com\")\n//\n//\tResult:\n//\t   URL - /v1/users/{userId}/details\n//\t   Composed URL - /v1/users/sample@sample.com/details\n//\n// It replaces the value of the key while composing the request URL.\n// The value will be escaped using [url.PathEscape] function.\n//\n// It can be overridden at the request level,\n// see [Request.SetPathParam] or [Request.SetPathParams]\nfunc (c *Client) SetPathParam(param, value string) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.pathParams[param] = url.PathEscape(value)\n\treturn c\n}\n\n// SetPathParamAny method sets a single URL path key-value pair in the\n// Resty client instance.\n//\n// It is similar to [Client.SetPathParam] but accepts any type as the value and converts\n// it to a string using predefined formatting rules (integers, bools, time.Time, etc.).\n//\n//\tclient.SetPathParamAny(\"userId\", 12345)\n//\n//\tResult:\n//\t   URL - /v1/users/{userId}/details\n//\t   Composed URL - /v1/users/12345/details\n//\n// It replaces the value of the key while composing the request URL.\n// The value will be escaped using [url.PathEscape] function.\n//\n// It can be overridden at the request level,\n// see [Request.SetPathParamAny] or [Request.SetPathParams]\nfunc (c *Client) SetPathParamAny(param string, value any) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tstrVal := formatAnyToString(value)\n\tc.pathParams[param] = url.PathEscape(strVal)\n\treturn c\n}\n\n// SetPathParams method sets multiple URL path key-value pairs at one go in the\n// Resty client instance.\n//\n//\tclient.SetPathParams(map[string]string{\n//\t\t\"userId\":       \"sample@sample.com\",\n//\t\t\"subAccountId\": \"100002\",\n//\t\t\"path\":         \"groups/developers\",\n//\t})\n//\n//\tResult:\n//\t   URL - /v1/users/{userId}/{subAccountId}/{path}/details\n//\t   Composed URL - /v1/users/sample@sample.com/100002/groups%2Fdevelopers/details\n//\n// It replaces the value of the key while composing the request URL.\n// The values will be escaped using [url.PathEscape] function.\n//\n// It can be overridden at the request level,\n// see [Request.SetPathParam] or [Request.SetPathParams]\nfunc (c *Client) SetPathParams(params map[string]string) *Client {\n\tfor p, v := range params {\n\t\tc.SetPathParam(p, v)\n\t}\n\treturn c\n}\n\n// SetPathRawParam method sets a single URL path key-value pair in the\n// Resty client instance without path escape.\n//\n//\tclient.SetPathRawParam(\"path\", \"groups/developers\")\n//\n//\tResult:\n//\t\tURL - /v1/users/{path}/details\n//\t\tComposed URL - /v1/users/groups/developers/details\n//\n// It replaces the value of the key while composing the request URL.\n// The value will be used as-is, no path escape applied.\n//\n// It can be overridden at the request level,\n// see [Request.SetPathRawParam] or [Request.SetPathRawParams]\nfunc (c *Client) SetPathRawParam(param, value string) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.pathParams[param] = value\n\treturn c\n}\n\n// SetPathRawParamAny method sets a single URL path key-value pair in the\n// Resty client instance without path escape.\n//\n// It is similar to [Client.SetPathRawParam] but accepts any type as the value and converts\n// it to a string using predefined formatting rules (integers, bools, time.Time, etc.).\n//\n//\tclient.SetPathRawParamAny(\"userId\", 12345)\n//\n//\tResult:\n//\t   URL - /v1/users/{userId}/details\n//\t   Composed URL - /v1/users/12345/details\n//\n// It replaces the value of the key while composing the request URL.\n// The value will be used as-is, no path escape applied.\n//\n// It can be overridden at the request level,\n// see [Request.SetPathRawParamAny] or [Request.SetPathRawParams]\nfunc (c *Client) SetPathRawParamAny(param string, value any) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tstrVal := formatAnyToString(value)\n\tc.pathParams[param] = strVal\n\treturn c\n}\n\n// SetPathRawParams method sets multiple URL path key-value pairs at one go in the\n// Resty client instance without path escape.\n//\n//\tclient.SetPathRawParams(map[string]string{\n//\t\t\"userId\":       \"sample@sample.com\",\n//\t\t\"subAccountId\": \"100002\",\n//\t\t\"path\":         \"groups/developers\",\n//\t})\n//\n//\tResult:\n//\t   URL - /v1/users/{userId}/{subAccountId}/{path}/details\n//\t   Composed URL - /v1/users/sample@sample.com/100002/groups/developers/details\n//\n// It replaces the value of the key while composing the request URL.\n// The value will be used as-is, no path escape applied.\n//\n// It can be overridden at the request level,\n// see [Request.SetPathRawParam] or [Request.SetPathRawParams]\nfunc (c *Client) SetPathRawParams(params map[string]string) *Client {\n\tfor p, v := range params {\n\t\tc.SetPathRawParam(p, v)\n\t}\n\treturn c\n}\n\n// SetJSONEscapeHTML method enables or disables the HTML escape on JSON marshal.\n// By default, escape HTML is `true`.\n//\n// NOTE: This option only applies to the standard JSON Marshaller used by Resty.\n//\n// It can be overridden at the request level, see [Request.SetJSONEscapeHTML]\nfunc (c *Client) SetJSONEscapeHTML(b bool) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.jsonEscapeHTML = b\n\treturn c\n}\n\n// ResponseBodyLimit method returns the value max body size limit in bytes from\n// the client instance.\nfunc (c *Client) ResponseBodyLimit() int64 {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.responseBodyLimit\n}\n\n// SetResponseBodyLimit method sets a maximum body size limit in bytes on response,\n// avoid reading too much data to memory.\n//\n// Client will return [resty.ErrResponseBodyTooLarge] if the body size of the body\n// in the uncompressed response is larger than the limit.\n// Body size limit will not be enforced in the following cases:\n//   - ResponseBodyLimit <= 0, which is the default behavior.\n//   - [Request.SetResponseSaveFileName] is called to save response data to the file.\n//   - \"DoNotParseResponse\" is set for client or request.\n//\n// It can be overridden at the request level; see [Request.SetResponseBodyLimit]\nfunc (c *Client) SetResponseBodyLimit(v int64) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.responseBodyLimit = v\n\treturn c\n}\n\n// IsTrace method returns true if the trace is enabled on the client instance; otherwise, it returns false.\nfunc (c *Client) IsTrace() bool {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.isTrace\n}\n\n// SetTrace method is used to turn on/off the trace capability in the Resty client instance.\n// It provides an insight into the request lifecycle using [httptrace.ClientTrace].\n//\n//\tclient := resty.New().SetTrace(true)\n//\n//\tresp, err := client.R().Get(\"https://httpbin.org/get\")\n//\tfmt.Println(\"error:\", err)\n//\tfmt.Println(\"Trace Info:\", resp.Request.TraceInfo())\n//\n// The method [Request.SetTrace] is also available to get trace info for a single request.\nfunc (c *Client) SetTrace(t bool) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.isTrace = t\n\treturn c\n}\n\n// SetCurlCmdGenerate method is used to turn on/off the generate curl command at the\n// client instance level.\n//\n// By default, Resty does not log the curl command in the debug log since it has the potential\n// to leak sensitive data unless explicitly enabled via [Client.SetCurlCmdDebugLog] or\n// [Request.SetCurlCmdDebugLog].\n//\n// NOTE: Use with care.\n//   - Potential to leak sensitive data from [Request] and [Response] in the debug log\n//     when the debug log option is enabled.\n//   - Additional memory usage since the request body was reread.\n//   - curl body is not generated for [io.Reader] and multipart request flow.\n//\n// It can be overridden at the request level; see [Request.SetCurlCmdGenerate]\nfunc (c *Client) SetCurlCmdGenerate(b bool) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.isCurlCmdGenerate = b\n\treturn c\n}\n\n// SetCurlCmdDebugLog method enables the curl command to be logged in the debug log.\n//\n// It can be overridden at the request level; see [Request.SetCurlCmdDebugLog]\nfunc (c *Client) SetCurlCmdDebugLog(b bool) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.isCurlCmdDebugLog = b\n\treturn c\n}\n\n// SetQueryParamsUnescape method sets the choice of unescape query parameters for the request URL.\n// To prevent broken URL, Resty replaces space (\" \") with \"+\" in the query parameters.\n//\n// See [Request.SetQueryParamsUnescape]\n//\n// NOTE: Request failure is possible due to non-standard usage of Unescaped Query Parameters.\nfunc (c *Client) SetQueryParamsUnescape(unescape bool) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.unescapeQueryParams = unescape\n\treturn c\n}\n\n// ResponseBodyUnlimitedReads method returns true if enabled. Otherwise, it returns false\nfunc (c *Client) ResponseBodyUnlimitedReads() bool {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.resBodyUnlimitedReads\n}\n\n// SetResponseBodyUnlimitedReads method is to turn on/off the response body in memory\n// that provides an ability to do unlimited reads.\n//\n// It can be overridden at the request level; see [Request.SetResponseBodyUnlimitedReads]\n//\n// Unlimited reads are possible in a few scenarios, even without enabling it.\n//   - When debug mode is enabled\n//\n// NOTE: Use with care\n//   - Turning on this feature keeps the response body in memory, which might cause additional memory usage.\nfunc (c *Client) SetResponseBodyUnlimitedReads(b bool) *Client {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\tc.resBodyUnlimitedReads = b\n\treturn c\n}\n\n// IsProxySet method returns the true is proxy is set from the Resty client; otherwise\n// false. By default, the proxy is set from the environment variable; refer to [http.ProxyFromEnvironment].\nfunc (c *Client) IsProxySet() bool {\n\treturn c.ProxyURL() != nil\n}\n\n// Client method returns the underlying Go [http.Client] used by the Resty.\nfunc (c *Client) Client() *http.Client {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\treturn c.httpClient\n}\n\n// Clone method returns a clone of the original client.\n//\n// NOTE: Use with care:\n//   - Interface values are not deeply cloned. Thus, both the original and the\n//     clone will use the same value.\n//   - It is not safe for concurrent use. You should only use this method\n//     when you are sure that any other concurrent process is not using the client\n//     or client instance is protected by a mutex.\nfunc (c *Client) Clone(ctx context.Context) *Client {\n\tcc := new(Client)\n\t// dereference the pointer and copy the value\n\t*cc = *c\n\n\tcc.ctx = ctx\n\tcc.queryParams = cloneURLValues(c.queryParams)\n\tcc.formData = cloneURLValues(c.formData)\n\tcc.header = c.header.Clone()\n\tcc.pathParams = maps.Clone(c.pathParams)\n\n\tif c.credentials != nil {\n\t\tcc.credentials = c.credentials.Clone()\n\t}\n\n\tcc.contentTypeEncoders = maps.Clone(c.contentTypeEncoders)\n\tcc.contentTypeDecoders = maps.Clone(c.contentTypeDecoders)\n\tcc.contentDecompressers = maps.Clone(c.contentDecompressers)\n\tcopy(cc.contentDecompresserKeys, c.contentDecompresserKeys)\n\n\tif c.proxyURL != nil {\n\t\tcc.proxyURL, _ = url.Parse(c.proxyURL.String())\n\t}\n\t// clone cookies\n\tif l := len(c.cookies); l > 0 {\n\t\tcc.cookies = make([]*http.Cookie, 0, l)\n\t\tfor _, cookie := range c.cookies {\n\t\t\tcc.cookies = append(cc.cookies, cloneCookie(cookie))\n\t\t}\n\t}\n\n\t// certain values need to be reset\n\tcc.lock = &sync.RWMutex{}\n\treturn cc\n}\n\n// Close method performs cleanup and closure activities on the client instance\nfunc (c *Client) Close() error {\n\t// Execute close hooks first\n\tc.onCloseHooks()\n\n\tif c.LoadBalancer() != nil {\n\t\tsilently(c.LoadBalancer().Close())\n\t}\n\tclose(c.certWatcherStopChan)\n\n\treturn nil\n}\n\nfunc (c *Client) executeRequestMiddlewares(req *Request) (err error) {\n\tfor _, f := range c.requestMiddlewares() {\n\t\tif err = f(c, req); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// Executes method executes the given `Request` object and returns\n// response or error.\nfunc (c *Client) execute(req *Request) (*Response, error) {\n\tif c.circuitBreaker != nil {\n\t\tif err := c.circuitBreaker.allow(); err != nil {\n\t\t\tc.circuitBreaker.onTriggerHooks(req, err)\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tif err := c.executeRequestMiddlewares(req); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif hostHeader := req.Header.Get(\"Host\"); hostHeader != \"\" {\n\t\treq.RawRequest.Host = hostHeader\n\t}\n\n\tprepareRequestDebugInfo(c, req)\n\n\treq.StartTime = time.Now()\n\tresp, err := c.Client().Do(req.withTimeout())\n\t// Cancel multipart context for io.Copy to stop reading/writing further\n\tif req.isMultiPart && req.multipartCancelFunc != nil {\n\t\treq.multipartCancelFunc()\n\t}\n\n\tresponse := &Response{Request: req, RawResponse: resp}\n\tresponse.setReceivedAt()\n\tif err != nil {\n\t\treturn response, err\n\t}\n\tif req.isMultiPart && req.multipartErrChan != nil {\n\t\t// read all multipart errors from channel\n\t\tfor err = range req.multipartErrChan {\n\t\t\tresponse.CascadeError = wrapErrors(err, response.CascadeError)\n\t\t}\n\t}\n\n\tif resp != nil {\n\t\tif c.circuitBreaker != nil {\n\t\t\tc.circuitBreaker.applyPolicies(resp)\n\t\t}\n\n\t\tresponse.Body = resp.Body\n\t\tif err = response.wrapContentDecompresser(); err != nil {\n\t\t\treturn response, response.wrapError(err, false)\n\t\t}\n\n\t\tresponse.wrapLimitReadCloser()\n\n\t\tif !req.IsResponseDoNotParse {\n\t\t\tif req.IsResponseBodyUnlimitedReads || req.IsDebug {\n\t\t\t\tresponse.wrapCopyReadCloser()\n\n\t\t\t\tif err = response.readAll(); err != nil {\n\t\t\t\t\treturn response, response.wrapError(err, false)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tdebugLogger(c, response)\n\n\t// Apply Response middleware\n\tfor _, f := range c.responseMiddlewares() {\n\t\tif err = f(c, response); err != nil {\n\t\t\tresponse.CascadeError = wrapErrors(err, response.CascadeError)\n\t\t}\n\t}\n\n\treturn response, response.wrapError(nil, false)\n}\n\n// getting TLS client config if not exists then create one\nfunc (c *Client) tlsConfig() (*tls.Config, error) {\n\tc.lock.Lock()\n\tdefer c.lock.Unlock()\n\n\tif tc, ok := c.httpClient.Transport.(TLSClientConfiger); ok {\n\t\treturn tc.TLSClientConfig(), nil\n\t}\n\n\ttransport, ok := c.httpClient.Transport.(*http.Transport)\n\tif !ok {\n\t\treturn nil, ErrNotHttpTransportType\n\t}\n\n\tif transport.TLSClientConfig == nil {\n\t\ttransport.TLSClientConfig = &tls.Config{}\n\t}\n\treturn transport.TLSClientConfig, nil\n}\n\n// just an internal helper method\nfunc (c *Client) outputLogTo(w io.Writer) *Client {\n\tc.Logger().(*logger).l.SetOutput(w)\n\treturn c\n}\n\n// ResponseError is a wrapper that includes the server response with an error.\n// Neither the err nor the response should be nil.\ntype ResponseError struct {\n\tResponse *Response\n\tErr      error\n}\n\nfunc (e *ResponseError) Error() string {\n\treturn e.Err.Error()\n}\n\nfunc (e *ResponseError) Unwrap() error {\n\treturn e.Err\n}\n\n// Helper to run errorHooks hooks.\n// It wraps the error in a [ResponseError] if the resp is not nil\n// so hooks can access it.\nfunc (c *Client) onErrorHooks(req *Request, res *Response, err error) {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\tif err != nil {\n\t\tif res != nil { // wrap with ResponseError\n\t\t\terr = &ResponseError{Response: res, Err: err}\n\t\t}\n\t\tfor _, h := range c.errorHooks {\n\t\t\th(req, err)\n\t\t}\n\t} else {\n\t\tfor _, h := range c.successHooks {\n\t\t\th(c, res)\n\t\t}\n\t}\n}\n\n// Helper to run panicHooks hooks.\nfunc (c *Client) onPanicHooks(req *Request, err error) {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\tfor _, h := range c.panicHooks {\n\t\th(req, err)\n\t}\n}\n\n// Helper to run invalidHooks hooks.\nfunc (c *Client) onInvalidHooks(req *Request, err error) {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\tfor _, h := range c.invalidHooks {\n\t\th(req, err)\n\t}\n}\n\n// Helper to run closeHooks hooks.\nfunc (c *Client) onCloseHooks() {\n\tc.lock.RLock()\n\tdefer c.lock.RUnlock()\n\tfor _, h := range c.closeHooks {\n\t\th()\n\t}\n}\n\nfunc (c *Client) debugf(format string, v ...any) {\n\tif c.IsDebug() {\n\t\tc.Logger().Debugf(format, v...)\n\t}\n}\n"
  },
  {
    "path": "client_test.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"compress/lzw\"\n\t\"context\"\n\tcryprand \"crypto/rand\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"math\"\n\t\"math/rand\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestClientBasicAuth(t *testing.T) {\n\tts := createAuthServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetBasicAuth(\"myuser\", \"basicauth\").\n\t\tSetBaseURL(ts.URL).\n\t\tSetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})\n\n\tresp, err := c.R().\n\t\tSetResult(&AuthSuccess{}).\n\t\tPost(\"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\tt.Logf(\"Result Success: %q\", resp.Result().(*AuthSuccess))\n\tlogResponse(t, resp)\n}\n\nfunc TestClientAuthToken(t *testing.T) {\n\tts := createAuthServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).\n\t\tSetAuthToken(\"004DDB79-6801-4587-B976-F093E6AC44FF\").\n\t\tSetBaseURL(ts.URL + \"/\")\n\n\tresp, err := c.R().Get(\"/profile\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n}\n\nfunc TestClientAuthScheme(t *testing.T) {\n\tts := createAuthServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\t// Ensure default Bearer\n\tc.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).\n\t\tSetAuthToken(\"004DDB79-6801-4587-B976-F093E6AC44FF\").\n\t\tSetBaseURL(ts.URL + \"/\")\n\n\tresp, err := c.R().Get(\"/profile\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\t// Ensure setting the scheme works as well\n\tc.SetAuthScheme(\"Bearer\")\n\tassertEqual(t, \"Bearer\", c.AuthScheme())\n\n\tresp2, err2 := c.R().Get(\"/profile\")\n\tassertError(t, err2)\n\tassertEqual(t, http.StatusOK, resp2.StatusCode())\n\n}\n\nfunc TestClientResponseMiddleware(t *testing.T) {\n\tts := createGenericServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.AddResponseMiddleware(func(c *Client, res *Response) error {\n\t\tt.Logf(\"Request sent at: %v\", res.Request.StartTime)\n\t\tt.Logf(\"Response Received at: %v\", res.ReceivedAt())\n\n\t\treturn nil\n\t})\n\n\tresp, err := c.R().\n\t\tSetBody(\"ResponseMiddleware: This is plain text body to server\").\n\t\tPut(ts.URL + \"/plaintext\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"TestPut: plain text response\", resp.String())\n}\n\nfunc TestClientRedirectPolicy(t *testing.T) {\n\tts := createRedirectServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl().SetRedirectPolicy(RedirectFlexiblePolicy(20), RedirectDomainCheckPolicy(\"127.0.0.1\"))\n\tres, err := c.R().\n\t\tSetHeader(\"Name1\", \"Value1\").\n\t\tSetHeader(\"Name2\", \"Value2\").\n\t\tSetHeader(\"Name3\", \"Value3\").\n\t\tGet(ts.URL + \"/redirect-1\")\n\n\tassertTrue(t, err.Error() == \"Get \\\"/redirect-21\\\": resty: stopped after 20 redirects\")\n\n\tredirects := res.RedirectHistory()\n\tassertEqual(t, 20, len(redirects))\n\n\tfinalReq := redirects[0]\n\tassertEqual(t, 307, finalReq.StatusCode)\n\tassertEqual(t, ts.URL+\"/redirect-20\", finalReq.URL)\n\n\tc.SetRedirectPolicy(RedirectNoPolicy())\n\tres, err = c.R().Get(ts.URL + \"/redirect-1\")\n\tassertNil(t, err)\n\tassertEqual(t, http.StatusTemporaryRedirect, res.StatusCode())\n\tassertEqual(t, `<a href=\"/redirect-2\">Temporary Redirect</a>.`, res.String())\n}\n\nfunc TestClientTimeout(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl().SetTimeout(200 * time.Millisecond)\n\t_, err := c.R().Get(ts.URL + \"/set-timeout-test\")\n\tassertErrorIs(t, context.DeadlineExceeded, err)\n}\n\nfunc TestClientTimeoutWithinThreshold(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl().SetTimeout(200 * time.Millisecond)\n\n\tresp, err := c.R().Get(ts.URL + \"/set-timeout-test-with-sequence\")\n\tassertError(t, err)\n\n\tseq1, _ := strconv.ParseInt(resp.String(), 10, 32)\n\n\tresp, err = c.R().Get(ts.URL + \"/set-timeout-test-with-sequence\")\n\tassertError(t, err)\n\n\tseq2, _ := strconv.ParseInt(resp.String(), 10, 32)\n\n\tassertEqual(t, seq1+1, seq2)\n}\n\nfunc TestClientTimeoutInternalError(t *testing.T) {\n\tc := dcnl().SetTimeout(time.Second * 1)\n\t_, _ = c.R().Get(\"http://localhost:9000/set-timeout-test\")\n}\n\nfunc TestClientProxy(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetTimeout(1 * time.Second)\n\tc.SetProxy(\"http://sampleproxy:8888\")\n\n\tresp, err := c.R().Get(ts.URL)\n\tassertNotNil(t, resp)\n\tassertNotNil(t, err)\n\n\t// error\n\tc.SetProxy(\"//not.a.user@%66%6f%6f.com:8888\")\n\n\tresp, err = c.R().\n\t\tGet(ts.URL)\n\tassertNotNil(t, err)\n\tassertNotNil(t, resp)\n}\n\nfunc TestClientSetCertificates(t *testing.T) {\n\tcertFile := filepath.Join(getTestDataPath(), \"cert.pem\")\n\tkeyFile := filepath.Join(getTestDataPath(), \"key.pem\")\n\n\tt.Run(\"client cert from file\", func(t *testing.T) {\n\t\tc := dcnl()\n\t\tc.SetCertificateFromFile(certFile, keyFile)\n\t\tassertEqual(t, 1, len(c.TLSClientConfig().Certificates))\n\t})\n\n\tt.Run(\"error-client cert from file\", func(t *testing.T) {\n\t\tc := dcnl()\n\t\tc.SetCertificateFromFile(certFile+\"no\", keyFile+\"no\")\n\t\tassertEqual(t, 0, len(c.TLSClientConfig().Certificates))\n\t})\n\n\tt.Run(\"client cert from string\", func(t *testing.T) {\n\t\tcertPemData, _ := os.ReadFile(certFile)\n\t\tkeyPemData, _ := os.ReadFile(keyFile)\n\t\tc := dcnl()\n\t\tc.SetCertificateFromString(string(certPemData), string(keyPemData))\n\t\tassertEqual(t, 1, len(c.TLSClientConfig().Certificates))\n\t})\n\n\tt.Run(\"error-client cert from string\", func(t *testing.T) {\n\t\tc := dcnl()\n\t\tc.SetCertificateFromString(string(\"empty\"), string(\"empty\"))\n\t\tassertEqual(t, 0, len(c.TLSClientConfig().Certificates))\n\t})\n}\n\nfunc TestClientSetRootCertificate(t *testing.T) {\n\tt.Run(\"root cert\", func(t *testing.T) {\n\t\tclient := dcnl()\n\t\tclient.SetRootCertificates(filepath.Join(getTestDataPath(), \"sample-root.pem\"))\n\n\t\ttransport, err := client.HTTPTransport()\n\n\t\tassertNil(t, err)\n\t\tassertNotNil(t, transport.TLSClientConfig.RootCAs)\n\t})\n\n\tt.Run(\"root cert not exists\", func(t *testing.T) {\n\t\tclient := dcnl()\n\t\tclient.SetRootCertificates(filepath.Join(getTestDataPath(), \"not-exists-sample-root.pem\"))\n\n\t\ttransport, err := client.HTTPTransport()\n\n\t\tassertNil(t, err)\n\t\tassertNil(t, transport.TLSClientConfig)\n\t})\n\n\tt.Run(\"root cert from string\", func(t *testing.T) {\n\t\tclient := dcnl()\n\t\trootPemData, err := os.ReadFile(filepath.Join(getTestDataPath(), \"sample-root.pem\"))\n\t\tassertNil(t, err)\n\n\t\tclient.SetRootCertificateFromString(string(rootPemData))\n\n\t\ttransport, err := client.HTTPTransport()\n\n\t\tassertNil(t, err)\n\t\tassertNotNil(t, transport.TLSClientConfig.RootCAs)\n\t})\n}\n\ntype CustomRoundTripper1 struct{}\n\n// RoundTrip just for test\nfunc (rt *CustomRoundTripper1) RoundTrip(_ *http.Request) (*http.Response, error) {\n\treturn &http.Response{}, nil\n}\n\nfunc TestClientCACertificateFromStringErrorTls(t *testing.T) {\n\tt.Run(\"root cert string\", func(t *testing.T) {\n\t\tclient := NewWithClient(&http.Client{})\n\t\tclient.outputLogTo(io.Discard)\n\n\t\trootPemData, err := os.ReadFile(filepath.Join(getTestDataPath(), \"sample-root.pem\"))\n\t\tassertNil(t, err)\n\t\trt := &CustomRoundTripper1{}\n\t\tclient.SetTransport(rt)\n\t\ttransport, err := client.HTTPTransport()\n\n\t\tclient.SetRootCertificateFromString(string(rootPemData))\n\n\t\tassertNotNil(t, rt)\n\t\tassertNotNil(t, err)\n\t\tassertNil(t, transport)\n\t})\n\n\tt.Run(\"client cert string\", func(t *testing.T) {\n\t\tclient := NewWithClient(&http.Client{})\n\t\tclient.outputLogTo(io.Discard)\n\n\t\trootPemData, err := os.ReadFile(filepath.Join(getTestDataPath(), \"sample-root.pem\"))\n\t\tassertNil(t, err)\n\t\trt := &CustomRoundTripper1{}\n\t\tclient.SetTransport(rt)\n\t\ttransport, err := client.HTTPTransport()\n\n\t\tclient.SetClientRootCertificateFromString(string(rootPemData))\n\n\t\tassertNotNil(t, rt)\n\t\tassertNotNil(t, err)\n\t\tassertNil(t, transport)\n\t})\n}\n\n// CustomRoundTripper2 just for test\ntype CustomRoundTripper2 struct {\n\thttp.RoundTripper\n\tTLSClientConfiger\n\ttlsConfig *tls.Config\n\treturnErr bool\n}\n\n// RoundTrip just for test\nfunc (rt *CustomRoundTripper2) RoundTrip(_ *http.Request) (*http.Response, error) {\n\tif rt.returnErr {\n\t\treturn nil, errors.New(\"test req mock error\")\n\t}\n\treturn &http.Response{}, nil\n}\n\nfunc (rt *CustomRoundTripper2) TLSClientConfig() *tls.Config {\n\treturn rt.tlsConfig\n}\nfunc (rt *CustomRoundTripper2) SetTLSClientConfig(tlsConfig *tls.Config) error {\n\tif rt.returnErr {\n\t\treturn errors.New(\"test mock error\")\n\t}\n\trt.tlsConfig = tlsConfig\n\treturn nil\n}\n\nfunc TestClientTLSConfigerInterface(t *testing.T) {\n\n\tt.Run(\"assert transport and custom roundtripper\", func(t *testing.T) {\n\t\tc := dcnl()\n\n\t\tassertNotNil(t, c.Transport())\n\t\tassertEqual(t, \"http.Transport\", inferType(c.Transport()).String())\n\n\t\tct := &CustomRoundTripper2{}\n\t\tc.SetTransport(ct)\n\t\tassertNotNil(t, c.Transport())\n\t\tassertEqual(t, \"resty.CustomRoundTripper2\", inferType(c.Transport()).String())\n\t})\n\n\tt.Run(\"get and set tls config\", func(t *testing.T) {\n\t\tc := dcnl()\n\n\t\tct := &CustomRoundTripper2{}\n\t\tc.SetTransport(ct)\n\n\t\ttlsConfig := &tls.Config{InsecureSkipVerify: true}\n\t\tc.SetTLSClientConfig(tlsConfig)\n\t\tassertEqual(t, tlsConfig, c.TLSClientConfig())\n\t})\n\n\tt.Run(\"get tls config error\", func(t *testing.T) {\n\t\tc := dcnl()\n\n\t\tct := &CustomRoundTripper1{}\n\t\tc.SetTransport(ct)\n\t\tassertNil(t, c.TLSClientConfig())\n\t})\n\n\tt.Run(\"set tls config error\", func(t *testing.T) {\n\t\tc := dcnl()\n\n\t\tct := &CustomRoundTripper2{returnErr: true}\n\t\tc.SetTransport(ct)\n\n\t\ttlsConfig := &tls.Config{InsecureSkipVerify: true}\n\t\tc.SetTLSClientConfig(tlsConfig)\n\t\tassertNil(t, c.TLSClientConfig())\n\t})\n}\n\nfunc TestClientSetClientRootCertificate(t *testing.T) {\n\tclient := dcnl()\n\tclient.SetClientRootCertificates(filepath.Join(getTestDataPath(), \"sample-root.pem\"))\n\n\ttransport, err := client.HTTPTransport()\n\n\tassertNil(t, err)\n\tassertNotNil(t, transport.TLSClientConfig.ClientCAs)\n}\n\nfunc TestClientSetClientRootCertificateNotExists(t *testing.T) {\n\tclient := dcnl()\n\tclient.SetClientRootCertificates(filepath.Join(getTestDataPath(), \"not-exists-sample-root.pem\"))\n\n\ttransport, err := client.HTTPTransport()\n\n\tassertNil(t, err)\n\tassertNil(t, transport.TLSClientConfig)\n}\n\nfunc TestClientSetClientRootCertificateWatcher(t *testing.T) {\n\tt.Run(\"Cert exists\", func(t *testing.T) {\n\t\tclient := dcnl()\n\t\tclient.SetClientRootCertificatesWatcher(\n\t\t\t&CertWatcherOptions{PoolInterval: time.Second * 1},\n\t\t\tfilepath.Join(getTestDataPath(), \"sample-root.pem\"),\n\t\t)\n\n\t\ttransport, err := client.HTTPTransport()\n\n\t\tassertNil(t, err)\n\t\tassertNotNil(t, transport.TLSClientConfig.ClientCAs)\n\t})\n\n\tt.Run(\"Cert does not exist\", func(t *testing.T) {\n\t\tclient := dcnl()\n\t\tclient.SetClientRootCertificatesWatcher(nil, filepath.Join(getTestDataPath(), \"not-exists-sample-root.pem\"))\n\n\t\ttransport, err := client.HTTPTransport()\n\n\t\tassertNil(t, err)\n\t\tassertNil(t, transport.TLSClientConfig)\n\t})\n}\n\nfunc TestClientSetClientRootCertificateFromString(t *testing.T) {\n\tclient := dcnl()\n\trootPemData, err := os.ReadFile(filepath.Join(getTestDataPath(), \"sample-root.pem\"))\n\tassertNil(t, err)\n\n\tclient.SetClientRootCertificateFromString(string(rootPemData))\n\n\ttransport, err := client.HTTPTransport()\n\n\tassertNil(t, err)\n\tassertNotNil(t, transport.TLSClientConfig.ClientCAs)\n}\n\nfunc TestClientRequestMiddlewareModification(t *testing.T) {\n\ttc := dcnl()\n\ttc.AddRequestMiddleware(func(c *Client, r *Request) error {\n\t\tr.SetAuthToken(\"This is test auth token\")\n\t\treturn nil\n\t})\n\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tresp, err := tc.R().Get(ts.URL + \"/\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"200 OK\", resp.Status())\n\tassertEqual(t, \"TestGet: text response\", resp.String())\n\n\tlogResponse(t, resp)\n}\n\nfunc TestClientSetHeaderVerbatim(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl().\n\t\tSetHeaderVerbatim(\"header-lowercase\", \"value_lowercase\").\n\t\tSetHeader(\"header-lowercase\", \"value_standard\")\n\n\t//lint:ignore SA1008 valid one, so ignore this!\n\tunConventionHdrValue := strings.Join(c.Header()[\"header-lowercase\"], \"\")\n\tassertEqual(t, \"value_lowercase\", unConventionHdrValue)\n\tassertEqual(t, \"value_standard\", c.Header().Get(\"Header-Lowercase\"))\n}\n\nfunc TestClientSetHeaderAny(t *testing.T) {\n\tc := dcnl().\n\t\tSetHeaderAny(\"X-Int-Value\", 42).\n\t\tSetHeaderAny(\"X-String-Value\", \"hello\")\n\n\tassertEqual(t, \"42\", c.Header().Get(\"X-Int-Value\"))\n\tassertEqual(t, \"hello\", c.Header().Get(\"X-String-Value\"))\n}\n\nfunc TestClientSetHeaderVerbatimAny(t *testing.T) {\n\tc := dcnl().\n\t\tSetHeaderVerbatimAny(\"header-lowercase\", 123)\n\n\t//lint:ignore SA1008 valid one, so ignore this!\n\tunConventionHdrValue := strings.Join(c.Header()[\"header-lowercase\"], \"\")\n\tassertEqual(t, \"123\", unConventionHdrValue)\n}\n\nfunc TestClientSetQueryParamAny(t *testing.T) {\n\tc := dcnl().\n\t\tSetQueryParamAny(\"page\", 5).\n\t\tSetQueryParamAny(\"active\", true)\n\n\tassertEqual(t, \"5\", c.QueryParams().Get(\"page\"))\n\tassertEqual(t, \"true\", c.QueryParams().Get(\"active\"))\n}\n\nfunc TestClientSetPathParamAny(t *testing.T) {\n\tc := dcnl().\n\t\tSetPathParamAny(\"userId\", 42).\n\t\tSetPathParamAny(\"name\", \"john doe\")\n\n\tassertEqual(t, \"42\", c.PathParams()[\"userId\"])\n\tassertEqual(t, \"john%20doe\", c.PathParams()[\"name\"])\n}\n\nfunc TestClientSetRawPathParamAny(t *testing.T) {\n\tc := dcnl().\n\t\tSetPathRawParamAny(\"userId\", 42).\n\t\tSetPathRawParamAny(\"name\", \"john doe\")\n\n\tassertEqual(t, \"42\", c.PathParams()[\"userId\"])\n\tassertEqual(t, \"john doe\", c.PathParams()[\"name\"])\n}\n\nfunc TestClientSetTransport(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\tclient := dcnl()\n\n\ttransport := &http.Transport{\n\t\t// something like Proxying to httptest.Server, etc...\n\t\tProxy: func(req *http.Request) (*url.URL, error) {\n\t\t\treturn url.Parse(ts.URL)\n\t\t},\n\t}\n\tclient.SetTransport(transport)\n\ttransportInUse, err := client.HTTPTransport()\n\n\tassertNil(t, err)\n\tassertTrue(t, transport == transportInUse, \"HTTP Transport should be of same type\")\n}\n\nfunc TestClientSetScheme(t *testing.T) {\n\tclient := dcnl()\n\n\tclient.SetScheme(\"http\")\n\n\tassertEqual(t, \"http\", client.scheme, \"Scheme should be 'http'\")\n}\n\nfunc TestClientSetCookieJar(t *testing.T) {\n\tclient := dcnl()\n\tbackupJar := client.httpClient.Jar\n\n\tclient.SetCookieJar(nil)\n\tassertNil(t, client.httpClient.Jar, \"CookieJar should be nil\")\n\n\tclient.SetCookieJar(backupJar)\n\tassertTrue(t, client.httpClient.Jar == backupJar, \"CookieJar should be set back to original jar\")\n}\n\n// This test methods exist for test coverage purpose\n// to validate the getter and setter\nfunc TestClientSettingsCoverage(t *testing.T) {\n\tc := dcnl()\n\n\tassertNotNil(t, c.CookieJar())\n\tassertNotNil(t, c.ContentTypeEncoders())\n\tassertNotNil(t, c.ContentTypeDecoders())\n\tassertFalse(t, c.IsDebug())\n\tassertEqual(t, math.MaxInt32, c.DebugBodyLimit())\n\tassertNotNil(t, c.Logger())\n\tassertEqual(t, 0, c.RetryCount())\n\tassertEqual(t, time.Millisecond*100, c.RetryWaitTime())\n\tassertEqual(t, time.Second*2, c.RetryMaxWaitTime())\n\tassertFalse(t, c.IsTrace())\n\tassertEqual(t, 0, len(c.RetryConditions()))\n\n\tauthToken := \"sample auth token value\"\n\tc.SetAuthToken(authToken)\n\tassertEqual(t, authToken, c.AuthToken())\n\n\tcustomAuthHeader := \"X-Custom-Authorization\"\n\tc.SetHeaderAuthorizationKey(customAuthHeader)\n\tassertEqual(t, customAuthHeader, c.HeaderAuthorizationKey())\n\n\tc.SetCloseConnection(true)\n\n\tc.SetDebug(false)\n\n\tassertTrue(t, c.IsRetryDefaultConditions())\n\tc.SetRetryDefaultConditions(false)\n\tassertFalse(t, c.IsRetryDefaultConditions())\n\tc.SetRetryDefaultConditions(true)\n\tassertTrue(t, c.IsRetryDefaultConditions())\n\n\tnr := nopReader{}\n\tn, err1 := nr.Read(nil)\n\tassertEqual(t, 0, n)\n\tassertEqual(t, io.EOF, err1)\n\tb, err1 := nr.ReadByte()\n\tassertEqual(t, byte(0), b)\n\tassertEqual(t, io.EOF, err1)\n\n\t// [Start] Custom Transport scenario\n\tct := dcnl()\n\tct.SetTransport(&CustomRoundTripper1{})\n\t_, err := ct.HTTPTransport()\n\tassertNotNil(t, err)\n\tassertEqual(t, ErrNotHttpTransportType, err)\n\n\tct.SetProxy(\"http://localhost:8080\")\n\tct.RemoveProxy()\n\n\tct.SetCertificates(tls.Certificate{})\n\tct.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})\n\tct.SetRootCertificateFromString(\"root cert\")\n\n\tct.outputLogTo(io.Discard)\n\t// [End] Custom Transport scenario\n\n\t// Response - for now stay here\n\tresp := &Response{Request: &Request{}}\n\ts := resp.fmtBodyString(0)\n\tassertEqual(t, \"***** NO CONTENT *****\", s)\n}\n\nfunc TestContentLengthWhenBodyIsNil(t *testing.T) {\n\tclient := dcnl()\n\n\tfnPreRequestMiddleware1 := func(c *Client, r *Request) error {\n\t\t// validate\n\t\tassertEqual(t, int64(0), r.contentLength)\n\t\tassertEqual(t, int64(0), r.RawRequest.ContentLength)\n\t\treturn nil\n\t}\n\tclient.SetRequestMiddlewares(\n\t\tMiddlewareRequestCreate,\n\t\tfnPreRequestMiddleware1,\n\t)\n\n\tclient.R().SetBody(nil).Get(\"http://localhost\")\n}\n\nfunc TestClientPreRequestMiddlewares(t *testing.T) {\n\tclient := dcnl()\n\n\tfnPreRequestMiddleware1 := func(c *Client, r *Request) error {\n\t\tc.Logger().Debugf(\"I'm in Pre-Request Hook\")\n\t\treturn nil\n\t}\n\n\tfnPreRequestMiddleware2 := func(c *Client, r *Request) error {\n\t\tc.Logger().Debugf(\"I'm Overwriting existing Pre-Request Hook\")\n\n\t\t// Reading Request `N` no of times\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tb, _ := r.RawRequest.GetBody()\n\t\t\trb, _ := io.ReadAll(b)\n\t\t\tc.Logger().Debugf(\"%s %v\", string(rb), len(rb))\n\t\t\tassertTrue(t, len(rb) >= 45)\n\t\t}\n\t\treturn nil\n\t}\n\n\tclient.SetRequestMiddlewares(\n\t\tMiddlewareRequestCreate,\n\t\tfnPreRequestMiddleware1,\n\t\tfnPreRequestMiddleware2,\n\t)\n\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\t// Regular bodybuf use case\n\tresp, _ := client.R().\n\t\tSetBody(map[string]any{\"username\": \"testuser\", \"password\": \"testpass\"}).\n\t\tPost(ts.URL + \"/login\")\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, `{ \"id\": \"success\", \"message\": \"login successful\" }`, resp.String())\n\n\t// io.Reader body use case\n\tresp, _ = client.R().\n\t\tSetHeader(hdrContentTypeKey, jsonContentType).\n\t\tSetBody(bytes.NewReader([]byte(`{\"username\":\"testuser\", \"password\":\"testpass\"}`))).\n\t\tPost(ts.URL + \"/login\")\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, `{ \"id\": \"success\", \"message\": \"login successful\" }`, resp.String())\n}\n\nfunc TestClientPreRequestMiddlewareError(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tfnPreRequestMiddleware1 := func(c *Client, r *Request) error {\n\t\treturn errors.New(\"error from PreRequestMiddleware\")\n\t}\n\tc.SetRequestMiddlewares(\n\t\tMiddlewareRequestCreate,\n\t\tfnPreRequestMiddleware1,\n\t)\n\n\tresp, err := c.R().Get(ts.URL)\n\tassertNotNil(t, err)\n\tassertEqual(t, \"error from PreRequestMiddleware\", err.Error())\n\tassertNil(t, resp)\n}\n\nfunc TestClientAllowMethodGetPayload(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tt.Run(\"method GET allow string payload at client level\", func(t *testing.T) {\n\t\tc := dcnl()\n\t\tc.SetMethodGetAllowPayload(true)\n\t\tassertTrue(t, c.IsMethodGetAllowPayload())\n\n\t\tpayload := \"test-payload\"\n\t\tresp, err := c.R().SetBody(payload).Get(ts.URL + \"/get-method-payload-test\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode(), \"Status code should be 200 OK\")\n\t\tassertEqual(t, payload, resp.String(), \"Response payload should be same as request payload\")\n\t})\n\n\tt.Run(\"method GET allow io.Reader payload at client level\", func(t *testing.T) {\n\t\tc := dcnl()\n\t\tc.SetMethodGetAllowPayload(true)\n\t\tassertTrue(t, c.IsMethodGetAllowPayload())\n\n\t\tpayload := \"test-payload\"\n\t\tbody := bytes.NewReader([]byte(payload))\n\t\tresp, err := c.R().SetBody(body).Get(ts.URL + \"/get-method-payload-test\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\t\tassertEqual(t, payload, resp.String(), \"Response payload should be same as request payload\")\n\t})\n\n\tt.Run(\"method GET disallow payload at client level\", func(t *testing.T) {\n\t\tc := dcnl()\n\t\tc.SetMethodGetAllowPayload(false)\n\t\tassertFalse(t, c.IsMethodGetAllowPayload())\n\n\t\tpayload := bytes.NewReader([]byte(\"test-payload\"))\n\t\tresp, err := c.R().SetBody(payload).Get(ts.URL + \"/get-method-payload-test\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\t\tassertEqual(t, \"\", resp.String())\n\t})\n}\n\nfunc TestClientAllowMethodDeletePayload(t *testing.T) {\n\tts := createGenericServer(t)\n\tdefer ts.Close()\n\n\tt.Run(\"method DELETE allow string payload at client level\", func(t *testing.T) {\n\t\tc := dcnl().SetBaseURL(ts.URL)\n\n\t\tc.SetMethodDeleteAllowPayload(true)\n\t\tassertTrue(t, c.IsMethodDeleteAllowPayload())\n\n\t\tpayload := \"test-payload\"\n\t\tresp, err := c.R().SetBody(payload).Delete(\"/delete\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\t\tassertEqual(t, payload, resp.String())\n\t})\n\n\tt.Run(\"method DELETE allow io.Reader payload at client level\", func(t *testing.T) {\n\t\tc := dcnl().SetBaseURL(ts.URL)\n\n\t\tc.SetMethodDeleteAllowPayload(true)\n\t\tassertTrue(t, c.IsMethodDeleteAllowPayload())\n\n\t\tpayload := \"test-payload\"\n\t\tbody := bytes.NewReader([]byte(payload))\n\t\tresp, err := c.R().SetBody(body).Delete(\"/delete\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\t\tassertEqual(t, payload, resp.String())\n\t})\n\n\tt.Run(\"method DELETE disallow payload at client level\", func(t *testing.T) {\n\t\tc := dcnl().SetBaseURL(ts.URL)\n\n\t\tc.SetMethodDeleteAllowPayload(false)\n\t\tassertFalse(t, c.IsMethodDeleteAllowPayload())\n\n\t\tpayload := bytes.NewReader([]byte(\"test-payload\"))\n\t\tresp, err := c.R().SetBody(payload).Delete(\"/delete\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\t\tassertEqual(t, \"\", resp.String(), \"Response payload should be empty\")\n\t})\n}\n\nfunc TestClientRoundTripper(t *testing.T) {\n\tc := NewWithClient(&http.Client{})\n\tc.outputLogTo(io.Discard)\n\n\trt := &CustomRoundTripper2{}\n\tc.SetTransport(rt)\n\n\tct, err := c.HTTPTransport()\n\tassertNotNil(t, err)\n\tassertNil(t, ct)\n\tassertEqual(t, ErrNotHttpTransportType, err)\n}\n\nfunc TestClientNewRequest(t *testing.T) {\n\tc := New()\n\trequest := c.NewRequest()\n\tassertNotNil(t, request)\n}\n\nfunc TestClientDebugBodySizeLimit(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc, lb := dcldb()\n\tc.SetDebugBodyLimit(30)\n\n\ttestcases := []struct{ url, want string }{\n\t\t// Text, does not exceed limit.\n\t\t{url: ts.URL, want: \"TestGet: text response\"},\n\t\t// Empty response.\n\t\t{url: ts.URL + \"/no-content\", want: \"***** NO CONTENT *****\"},\n\t\t// JSON, does not exceed limit.\n\t\t{url: ts.URL + \"/json\", want: \"{\\n   \\\"TestGet\\\": \\\"JSON response\\\"\\n}\"},\n\t\t// Invalid JSON, does not exceed limit.\n\t\t{url: ts.URL + \"/json-invalid\", want: \"DebugLog: Response.fmtBodyString: invalid character 'T' looking for beginning of value\"},\n\t\t// Text, exceeds limit.\n\t\t{url: ts.URL + \"/long-text\", want: \"RESPONSE TOO LARGE\"},\n\t\t// JSON, exceeds limit.\n\t\t{url: ts.URL + \"/long-json\", want: \"RESPONSE TOO LARGE\"},\n\t}\n\tfor _, tc := range testcases {\n\t\t_, err := c.R().Get(tc.url)\n\t\tif tc.want != \"\" {\n\t\t\tassertError(t, err)\n\t\t\tdebugLog := lb.String()\n\t\t\tif !strings.Contains(debugLog, tc.want) {\n\t\t\t\tt.Errorf(\"Expected logs to contain [%v], got [\\n%v]\", tc.want, debugLog)\n\t\t\t}\n\t\t\tlb.Reset()\n\t\t}\n\t}\n}\n\nfunc TestGzipCompress(t *testing.T) {\n\tts := createGenericServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\ttestcases := []struct{ url, want string }{\n\t\t{ts.URL + \"/gzip-test\", \"This is Gzip response testing\"},\n\t\t{ts.URL + \"/gzip-test-gziped-empty-body\", \"\"},\n\t\t{ts.URL + \"/gzip-test-no-gziped-body\", \"\"},\n\t}\n\tfor _, tc := range testcases {\n\t\tresp, err := c.R().Get(tc.url)\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\t\tassertEqual(t, \"200 OK\", resp.Status())\n\t\tassertEqual(t, tc.want, resp.String())\n\n\t\tlogResponse(t, resp)\n\t}\n}\n\nfunc TestDeflateCompress(t *testing.T) {\n\tts := createGenericServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\ttestcases := []struct{ url, want string }{\n\t\t{ts.URL + \"/deflate-test\", \"This is Deflate response testing\"},\n\t\t{ts.URL + \"/deflate-test-empty-body\", \"\"},\n\t\t{ts.URL + \"/deflate-test-no-body\", \"\"},\n\t}\n\tfor _, tc := range testcases {\n\t\tresp, err := c.R().Get(tc.url)\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\t\tassertEqual(t, \"200 OK\", resp.Status())\n\t\tassertEqual(t, tc.want, resp.String())\n\n\t\tlogResponse(t, resp)\n\t}\n}\n\ntype lzwReader struct {\n\ts io.ReadCloser\n\tr io.ReadCloser\n}\n\nfunc (l *lzwReader) Read(p []byte) (n int, err error) {\n\treturn l.r.Read(p)\n}\n\nfunc (l *lzwReader) Close() error {\n\tcloseq(l.r)\n\tcloseq(l.s)\n\treturn nil\n}\n\nfunc TestLzwCompress(t *testing.T) {\n\tts := createGenericServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\n\t// Not found scenario\n\t_, err := c.R().Get(ts.URL + \"/lzw-test\")\n\tassertNotNil(t, err)\n\tassertEqual(t, ErrContentDecompresserNotFound, err)\n\n\t// Register LZW content decoder\n\tc.AddContentDecompresser(\"ComPreSs\", func(r io.ReadCloser) (io.ReadCloser, error) {\n\t\tl := &lzwReader{\n\t\t\ts: r,\n\t\t\tr: lzw.NewReader(r, lzw.LSB, 8),\n\t\t}\n\t\treturn l, nil\n\t})\n\tc.SetContentDecompresserKeys([]string{\"compress\"})\n\n\ttestcases := []struct{ url, want string }{\n\t\t{ts.URL + \"/lzw-test\", \"This is LZW response testing\"},\n\t\t{ts.URL + \"/lzw-test-empty-body\", \"\"},\n\t\t{ts.URL + \"/lzw-test-no-body\", \"\"},\n\t}\n\tfor _, tc := range testcases {\n\t\tresp, err := c.R().Get(tc.url)\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\t\tassertEqual(t, \"200 OK\", resp.Status())\n\t\tassertEqual(t, tc.want, resp.String())\n\n\t\tlogResponse(t, resp)\n\t}\n}\n\nfunc TestClientLogCallbacks(t *testing.T) {\n\tts := createAuthServer(t)\n\tdefer ts.Close()\n\n\tc, lb := dcldb()\n\n\tc.OnDebugLog(func(dl *DebugLog) {\n\t\t// request\n\t\t// masking authorization header\n\t\tdl.Request.Header.Set(\"Authorization\", \"Bearer *******************************\")\n\n\t\t// response\n\t\tdl.Response.Header.Add(\"X-Debug-Response-Log\", \"Modified :)\")\n\t\tdl.Response.Body += \"\\nModified the response body content\"\n\t})\n\n\tc.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).\n\t\tSetAuthToken(\"004DDB79-6801-4587-B976-F093E6AC44FF\")\n\n\tresp, err := c.R().\n\t\tSetAuthToken(\"004DDB79-6801-4587-B976-F093E6AC44FF-Request\").\n\t\tGet(ts.URL + \"/profile\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\t// Validating debug log updates\n\tlogInfo := lb.String()\n\tassertTrue(t, strings.Contains(logInfo, \"Bearer *******************************\"))\n\tassertTrue(t, strings.Contains(logInfo, \"X-Debug-Response-Log\"))\n\tassertTrue(t, strings.Contains(logInfo, \"Modified the response body content\"))\n\n\t// overwrite scenario\n\tc.OnDebugLog(func(dl *DebugLog) {\n\t\t// overwrite debug log\n\t})\n\tresp, err = c.R().\n\t\tSetAuthToken(\"004DDB79-6801-4587-B976-F093E6AC44FF-Request\").\n\t\tGet(ts.URL + \"/profile\")\n\tassertNil(t, err)\n\tassertNotNil(t, resp)\n\tassertEqual(t, int64(66), resp.Size())\n\tassertTrue(t, strings.Contains(lb.String(), \"Overwriting an existing on-debug-log callback from=resty.dev/v3.TestClientLogCallbacks.func1 to=resty.dev/v3.TestClientLogCallbacks.func2\"))\n}\n\nfunc TestDebugLogSimultaneously(t *testing.T) {\n\tts := createGetServer(t)\n\n\tc := dcnl().\n\t\tSetDebug(true).\n\t\tSetBaseURL(ts.URL)\n\n\tt.Cleanup(ts.Close)\n\tfor i := 0; i < 50; i++ {\n\t\tt.Run(fmt.Sprint(i), func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tresp, err := c.R().\n\t\t\t\tSetBody([]int{1, 2, 3}).\n\t\t\t\tSetHeader(hdrContentTypeKey, \"application/json; charset=utf-8\").\n\t\t\t\tPost(\"/\")\n\n\t\t\tassertError(t, err)\n\t\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\t\t})\n\t}\n}\n\nfunc TestCustomTransportSettings(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tcustomTransportSettings := &TransportSettings{\n\t\tDialerTimeout:          30 * time.Second,\n\t\tDialerKeepAlive:        15 * time.Second,\n\t\tIdleConnTimeout:        120 * time.Second,\n\t\tTLSHandshakeTimeout:    20 * time.Second,\n\t\tExpectContinueTimeout:  1 * time.Second,\n\t\tMaxIdleConns:           50,\n\t\tMaxIdleConnsPerHost:    3,\n\t\tMaxConnsPerHost:        100,\n\t\tResponseHeaderTimeout:  10 * time.Second,\n\t\tMaxResponseHeaderBytes: 1 << 10,\n\t\tWriteBufferSize:        2 << 10,\n\t\tReadBufferSize:         2 << 10,\n\t}\n\tclient := NewWithTransportSettings(customTransportSettings)\n\tclient.SetBaseURL(ts.URL)\n\n\tresp, err := client.R().Get(\"/\")\n\tassertNil(t, err)\n\tassertEqual(t, \"TestGet: text response\", resp.String())\n}\n\nfunc TestDefaultDialerTransportSettings(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tt.Run(\"transport-default\", func(t *testing.T) {\n\t\tclient := NewWithTransportSettings(nil)\n\t\tclient.SetBaseURL(ts.URL)\n\n\t\tresp, err := client.R().Get(\"/\")\n\t\tassertNil(t, err)\n\t\tassertEqual(t, \"TestGet: text response\", resp.String())\n\t})\n\n\tt.Run(\"dialer-transport-default\", func(t *testing.T) {\n\t\tclient := NewWithDialerAndTransportSettings(nil, nil)\n\t\tclient.SetBaseURL(ts.URL)\n\n\t\tresp, err := client.R().Get(\"/\")\n\t\tassertNil(t, err)\n\t\tassertEqual(t, \"TestGet: text response\", resp.String())\n\t})\n}\n\nfunc TestNewWithDialer(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tdialer := &net.Dialer{\n\t\tTimeout:   15 * time.Second,\n\t\tKeepAlive: 15 * time.Second,\n\t}\n\tclient := NewWithDialer(dialer)\n\tclient.SetBaseURL(ts.URL)\n\n\tresp, err := client.R().Get(\"/\")\n\tassertNil(t, err)\n\tassertEqual(t, \"TestGet: text response\", resp.String())\n}\n\nfunc TestNewWithLocalAddr(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tlocalAddress, _ := net.ResolveTCPAddr(\"tcp\", \"127.0.0.1\")\n\tclient := NewWithLocalAddr(localAddress)\n\tclient.SetBaseURL(ts.URL)\n\n\tresp, err := client.R().Get(\"/\")\n\tassertNil(t, err)\n\tassertEqual(t, \"TestGet: text response\", resp.String())\n}\n\nfunc TestClientOnResponseFailure(t *testing.T) {\n\ttests := []struct {\n\t\tname        string\n\t\tsetup       func(*Client)\n\t\tisError     bool\n\t\thasResponse bool\n\t\tpanics      bool\n\t}{\n\t\t{\n\t\t\tname: \"successful_request\",\n\t\t},\n\t\t{\n\t\t\tname: \"http_status_failure\",\n\t\t\tsetup: func(client *Client) {\n\t\t\t\tclient.SetAuthToken(\"BAD\")\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"before_request_failure\",\n\t\t\tsetup: func(client *Client) {\n\t\t\t\tclient.AddRequestMiddleware(func(client *Client, request *Request) error {\n\t\t\t\t\treturn fmt.Errorf(\"before request\")\n\t\t\t\t})\n\t\t\t},\n\t\t\tisError: true,\n\t\t},\n\t\t{\n\t\t\tname: \"before_request_failure_retry\",\n\t\t\tsetup: func(client *Client) {\n\t\t\t\tclient.SetRetryCount(3).AddRequestMiddleware(func(client *Client, request *Request) error {\n\t\t\t\t\treturn fmt.Errorf(\"before request\")\n\t\t\t\t})\n\t\t\t},\n\t\t\tisError: true,\n\t\t},\n\t\t{\n\t\t\tname: \"after_response_failure\",\n\t\t\tsetup: func(client *Client) {\n\t\t\t\tclient.AddResponseMiddleware(func(client *Client, response *Response) error {\n\t\t\t\t\treturn fmt.Errorf(\"after response\")\n\t\t\t\t})\n\t\t\t},\n\t\t\tisError:     true,\n\t\t\thasResponse: true,\n\t\t},\n\t\t{\n\t\t\tname: \"after_response_failure_retry\",\n\t\t\tsetup: func(client *Client) {\n\t\t\t\tclient.SetRetryCount(3).AddResponseMiddleware(func(client *Client, response *Response) error {\n\t\t\t\t\treturn fmt.Errorf(\"after response\")\n\t\t\t\t})\n\t\t\t},\n\t\t\tisError:     true,\n\t\t\thasResponse: true,\n\t\t},\n\t\t{\n\t\t\tname: \"panic with error\",\n\t\t\tsetup: func(client *Client) {\n\t\t\t\tclient.AddRequestMiddleware(func(client *Client, request *Request) error {\n\t\t\t\t\tpanic(fmt.Errorf(\"before request\"))\n\t\t\t\t})\n\t\t\t},\n\t\t\tisError:     false,\n\t\t\thasResponse: false,\n\t\t\tpanics:      true,\n\t\t},\n\t\t{\n\t\t\tname: \"panic with string\",\n\t\t\tsetup: func(client *Client) {\n\t\t\t\tclient.AddRequestMiddleware(func(client *Client, request *Request) error {\n\t\t\t\t\tpanic(\"before request\")\n\t\t\t\t})\n\t\t\t},\n\t\t\tisError:     false,\n\t\t\thasResponse: false,\n\t\t\tpanics:      true,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\ttest := test\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tt.Parallel()\n\t\t\tts := createAuthServer(t)\n\t\t\tdefer ts.Close()\n\n\t\t\tvar assertErrorHook = func(r *Request, err error) {\n\t\t\t\tassertNotNil(t, r)\n\t\t\t\tv, ok := err.(*ResponseError)\n\t\t\t\tassertEqual(t, test.hasResponse, ok)\n\t\t\t\tif ok {\n\t\t\t\t\tassertNotNil(t, v.Response)\n\t\t\t\t\tassertNotNil(t, v.Err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tvar errorHook1, errorHook2, successHook1, successHook2, panicHook1, panicHook2 int\n\t\t\tdefer func() {\n\t\t\t\tif rec := recover(); rec != nil {\n\t\t\t\t\tassertTrue(t, test.panics, \"expected to panic\")\n\t\t\t\t\tassertEqual(t, 0, errorHook1)\n\t\t\t\t\tassertEqual(t, 0, successHook1)\n\t\t\t\t\tassertEqual(t, 1, panicHook1)\n\t\t\t\t\tassertEqual(t, 1, panicHook2)\n\t\t\t\t}\n\t\t\t}()\n\t\t\tc := dcnl().\n\t\t\t\tSetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).\n\t\t\t\tSetAuthToken(\"004DDB79-6801-4587-B976-F093E6AC44FF\").\n\t\t\t\tSetRetryCount(0).\n\t\t\t\tSetRetryMaxWaitTime(time.Microsecond).\n\t\t\t\tAddRetryConditions(func(response *Response, err error) bool {\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\t\t\t\t\treturn response.IsStatusFailure()\n\t\t\t\t}).\n\t\t\t\tOnError(func(r *Request, err error) {\n\t\t\t\t\tassertErrorHook(r, err)\n\t\t\t\t\terrorHook1++\n\t\t\t\t}).\n\t\t\t\tOnError(func(r *Request, err error) {\n\t\t\t\t\tassertErrorHook(r, err)\n\t\t\t\t\terrorHook2++\n\t\t\t\t}).\n\t\t\t\tOnPanic(func(r *Request, err error) {\n\t\t\t\t\tassertErrorHook(r, err)\n\t\t\t\t\tpanicHook1++\n\t\t\t\t}).\n\t\t\t\tOnPanic(func(r *Request, err error) {\n\t\t\t\t\tassertErrorHook(r, err)\n\t\t\t\t\tpanicHook2++\n\t\t\t\t}).\n\t\t\t\tOnSuccess(func(c *Client, resp *Response) {\n\t\t\t\t\tassertNotNil(t, c)\n\t\t\t\t\tassertNotNil(t, resp)\n\t\t\t\t\tsuccessHook1++\n\t\t\t\t}).\n\t\t\t\tOnSuccess(func(c *Client, resp *Response) {\n\t\t\t\t\tassertNotNil(t, c)\n\t\t\t\t\tassertNotNil(t, resp)\n\t\t\t\t\tsuccessHook2++\n\t\t\t\t})\n\t\t\tif test.setup != nil {\n\t\t\t\ttest.setup(c)\n\t\t\t}\n\t\t\t_, err := c.R().Get(ts.URL + \"/profile\")\n\t\t\tif test.isError {\n\t\t\t\tassertNotNil(t, err)\n\t\t\t\tassertEqual(t, 1, errorHook1)\n\t\t\t\tassertEqual(t, 1, errorHook2)\n\t\t\t\tassertEqual(t, 0, successHook1)\n\t\t\t\tassertEqual(t, 0, panicHook1)\n\t\t\t} else {\n\t\t\t\tassertError(t, err)\n\t\t\t\tassertEqual(t, 0, errorHook1)\n\t\t\t\tassertEqual(t, 1, successHook1)\n\t\t\t\tassertEqual(t, 1, successHook2)\n\t\t\t\tassertEqual(t, 0, panicHook1)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestResponseError(t *testing.T) {\n\terr := errors.New(\"error message\")\n\tre := &ResponseError{\n\t\tResponse: &Response{},\n\t\tErr:      err,\n\t}\n\tassertNotNil(t, re.Unwrap())\n\tassertEqual(t, err.Error(), re.Error())\n}\n\nfunc TestHostURLForGH318AndGH407(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\ttargetURL, _ := url.Parse(ts.URL)\n\tt.Log(\"ts.URL:\", ts.URL)\n\tt.Log(\"targetURL.Host:\", targetURL.Host)\n\t// Sample output\n\t// ts.URL: http://127.0.0.1:55967\n\t// targetURL.Host: 127.0.0.1:55967\n\n\t// Unable use the local http test server for this\n\t// use case testing\n\t//\n\t// using `targetURL.Host` value or test case yield to ERROR\n\t// \"parse \"127.0.0.1:55967\": first path segment in URL cannot contain colon\"\n\n\t// test the functionality with httpbin.org locally\n\t// will figure out later\n\n\tc := dcnl()\n\t// c.SetScheme(\"http\")\n\t// c.SetHostURL(targetURL.Host + \"/\")\n\n\t// t.Log(\"with leading `/`\")\n\t// resp, err := c.R().Post(\"/login\")\n\t// assertNil(t, err)\n\t// assertNotNil(t, resp)\n\n\t// t.Log(\"\\nwithout leading `/`\")\n\t// resp, err = c.R().Post(\"login\")\n\t// assertNil(t, err)\n\t// assertNotNil(t, resp)\n\n\tt.Log(\"with leading `/` on request & with trailing `/` on host url\")\n\tc.SetBaseURL(ts.URL + \"/\")\n\tresp, err := c.R().\n\t\tSetBody(map[string]any{\"username\": \"testuser\", \"password\": \"testpass\"}).\n\t\tPost(\"/login\")\n\tassertNil(t, err)\n\tassertNotNil(t, resp)\n}\n\nfunc TestPostRedirectWithBody(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tmu := sync.Mutex{}\n\trnd := rand.New(rand.NewSource(time.Now().UnixNano()))\n\n\tc := dcnl().SetBaseURL(ts.URL)\n\n\ttotalRequests := 4000\n\twg := sync.WaitGroup{}\n\twg.Add(totalRequests)\n\tfor i := 0; i < totalRequests; i++ {\n\t\tif i%50 == 0 {\n\t\t\ttime.Sleep(20 * time.Millisecond) // to prevent test server socket exhaustion\n\t\t}\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tmu.Lock()\n\t\t\trandNumber := rnd.Int()\n\t\t\tmu.Unlock()\n\t\t\tresp, err := c.R().\n\t\t\t\tSetBody([]byte(strconv.Itoa(randNumber))).\n\t\t\t\tPost(\"/redirect-with-body\")\n\t\t\tassertError(t, err)\n\t\t\tassertNotNil(t, resp)\n\t\t}()\n\t}\n\twg.Wait()\n}\n\nfunc TestUnixSocket(t *testing.T) {\n\tunixSocketAddr := createUnixSocketEchoServer(t)\n\tdefer os.Remove(unixSocketAddr)\n\n\t// Create a Go's http.Transport so we can set it in resty.\n\ttransport := http.Transport{\n\t\tDial: func(_, _ string) (net.Conn, error) {\n\t\t\treturn net.Dial(\"unix\", unixSocketAddr)\n\t\t},\n\t}\n\n\t// Create a Resty Client\n\tclient := New()\n\n\t// Set the previous transport that we created, set the scheme of the communication to the\n\t// socket and set the unixSocket as the HostURL.\n\tclient.SetTransport(&transport).SetScheme(\"http\").SetBaseURL(unixSocketAddr)\n\n\t// No need to write the host's URL on the request, just the path.\n\tres, err := client.R().Get(\"http://localhost/\")\n\tassertNil(t, err)\n\tassertEqual(t, \"Hi resty client from a server running on Unix domain socket!\", res.String())\n\n\tres, err = client.R().Get(\"http://localhost/hello\")\n\tassertNil(t, err)\n\tassertEqual(t, \"Hello resty client from a server running on endpoint /hello!\", res.String())\n}\n\nfunc TestClientClone(t *testing.T) {\n\tparent := New()\n\n\t// set a non-interface field\n\tparent.SetBaseURL(\"http://localhost\")\n\tparent.SetBasicAuth(\"parent\", \"\")\n\tparent.SetProxy(\"http://localhost:8080\")\n\n\tparent.SetCookie(&http.Cookie{\n\t\tName:  \"go-resty-1\",\n\t\tValue: \"This is cookie 1 value\",\n\t})\n\tparent.SetCookies([]*http.Cookie{\n\t\t{\n\t\t\tName:  \"go-resty-2\",\n\t\t\tValue: \"This is cookie 2 value\",\n\t\t},\n\t\t{\n\t\t\tName:  \"go-resty-3\",\n\t\t\tValue: \"This is cookie 3 value\",\n\t\t},\n\t})\n\n\tclone := parent.Clone(context.Background())\n\t// update value of non-interface type - change will only happen on clone\n\tclone.SetBaseURL(\"https://local.host\")\n\n\tclone.SetBasicAuth(\"clone\", \"clone\")\n\n\t// assert non-interface type\n\tassertEqual(t, \"http://localhost\", parent.BaseURL())\n\tassertEqual(t, \"https://local.host\", clone.BaseURL())\n\tassertEqual(t, \"parent\", parent.credentials.Username)\n\tassertEqual(t, \"clone\", clone.credentials.Username)\n\n\t// assert interface/pointer type\n\tassertEqual(t, parent.Client(), clone.Client())\n\n\t// assert cookies\n\tparentCookies := parent.Cookies()\n\tcloneCookies := clone.Cookies()\n\tassertEqual(t, len(parentCookies), len(cloneCookies))\n\tfor i := range parentCookies {\n\t\tassertEqual(t, parentCookies[i].Name, cloneCookies[i].Name)\n\t\tassertEqual(t, parentCookies[i].Value, cloneCookies[i].Value)\n\t}\n}\n\nfunc TestResponseBodyLimit(t *testing.T) {\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tio.CopyN(w, cryprand.Reader, 100*800)\n\t})\n\tdefer ts.Close()\n\n\tt.Run(\"client body limit\", func(t *testing.T) {\n\t\tresBodyLimit := int64(1024)\n\t\tc := dcnl().SetResponseBodyLimit(resBodyLimit)\n\t\tassertEqual(t, resBodyLimit, c.ResponseBodyLimit())\n\n\t\tresp, err := c.R().Get(ts.URL + \"/\")\n\t\tassertNotNil(t, err)\n\t\tassertErrorIs(t, ErrReadExceedsThresholdLimit, err)\n\t\tassertTrue(t, resp.Size() == resBodyLimit)\n\t})\n\n\tt.Run(\"request body limit\", func(t *testing.T) {\n\t\tresBodyLimit := int64(1024)\n\t\tc := dcnl()\n\n\t\tresp, err := c.R().SetResponseBodyLimit(resBodyLimit).Get(ts.URL + \"/\")\n\t\tassertNotNil(t, err)\n\t\tassertErrorIs(t, ErrReadExceedsThresholdLimit, err)\n\t\tassertTrue(t, resp.Size() == resBodyLimit)\n\t})\n\n\tt.Run(\"body less than limit\", func(t *testing.T) {\n\t\tc := dcnl()\n\n\t\tres, err := c.R().SetResponseBodyLimit(800*100 + 10).Get(ts.URL + \"/\")\n\t\tassertNil(t, err)\n\t\tassertEqual(t, 800*100, len(res.Bytes()))\n\t\tassertEqual(t, int64(800*100), res.Size())\n\t})\n\n\tt.Run(\"no body limit\", func(t *testing.T) {\n\t\tc := dcnl()\n\n\t\tres, err := c.R().Get(ts.URL + \"/\")\n\t\tassertNil(t, err)\n\t\tassertEqual(t, 800*100, len(res.Bytes()))\n\t\tassertEqual(t, int64(800*100), res.Size())\n\t})\n\n\tt.Run(\"read error\", func(t *testing.T) {\n\t\ttse := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.Header().Set(hdrContentEncodingKey, \"gzip\")\n\t\t\tvar buf [1024]byte\n\t\t\tw.Write(buf[:])\n\t\t})\n\t\tdefer tse.Close()\n\n\t\tc := dcnl()\n\n\t\t_, err := c.R().SetResponseBodyLimit(10240).Get(tse.URL + \"/\")\n\t\tassertErrorIs(t, gzip.ErrHeader, err)\n\t})\n}\n\nfunc TestClient_executeReadAllError(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tioReadAll = func(_ io.Reader) ([]byte, error) {\n\t\treturn nil, errors.New(\"test case error\")\n\t}\n\tt.Cleanup(func() {\n\t\tioReadAll = io.ReadAll\n\t})\n\n\tc := dcnld()\n\n\tresp, err := c.R().\n\t\tSetQueryParam(\"request_no\", strconv.FormatInt(time.Now().Unix(), 10)).\n\t\tGet(ts.URL + \"/json\")\n\n\tassertNotNil(t, err)\n\tassertEqual(t, \"test case error\", err.Error())\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"\", resp.String())\n}\n\nfunc TestClientDebugf(t *testing.T) {\n\tt.Run(\"Debug mode enabled\", func(t *testing.T) {\n\t\tvar b bytes.Buffer\n\t\tc := New().SetLogger(&logger{l: log.New(&b, \"\", 0)}).SetDebug(true)\n\t\tc.debugf(\"hello\")\n\t\tassertEqual(t, \"DEBUG RESTY hello\\n\", b.String())\n\t})\n\n\tt.Run(\"Debug mode disabled\", func(t *testing.T) {\n\t\tvar b bytes.Buffer\n\t\tc := New().SetLogger(&logger{l: log.New(&b, \"\", 0)})\n\t\tc.debugf(\"hello\")\n\t\tassertEqual(t, \"\", b.String())\n\t})\n}\n\nfunc TestClientOnClose(t *testing.T) {\n\tvar hookExecuted bool\n\n\tc := dcnl()\n\tc.OnClose(func() {\n\t\thookExecuted = true\n\t})\n\n\terr := c.Close()\n\tassertNil(t, err)\n\tassertTrue(t, hookExecuted, \"OnClose hook should be executed\")\n}\n\nfunc TestClientOnCloseMultipleHooks(t *testing.T) {\n\tvar executionOrder []string\n\n\tc := dcnl()\n\tc.OnClose(func() {\n\t\texecutionOrder = append(executionOrder, \"first\")\n\t})\n\tc.OnClose(func() {\n\t\texecutionOrder = append(executionOrder, \"second\")\n\t})\n\tc.OnClose(func() {\n\t\texecutionOrder = append(executionOrder, \"third\")\n\t})\n\n\terr := c.Close()\n\tassertNil(t, err)\n\tassertEqual(t, []string{\"first\", \"second\", \"third\"}, executionOrder)\n}\n\nfunc TestClientHedgingMutualExclusionWithRetry(t *testing.T) {\n\tc := dcnl()\n\n\t// Set retry first\n\tc.SetRetryCount(2)\n\tassertEqual(t, 2, c.RetryCount())\n\n\t// Enable hedging should disable retry by default\n\th := NewHedging().\n\t\tSetDelay(50 * time.Millisecond).\n\t\tSetMaxRequest(3).\n\t\tSetMaxRequestPerSecond(0)\n\tc.SetHedging(h)\n\tassertEqual(t, 0, c.RetryCount())\n\n\t// But user can re-enable retry as fallback\n\tc.SetRetryCount(1)\n\tassertEqual(t, 1, c.RetryCount())\n\tassertEqual(t, true, c.isHedgingEnabled())\n\n\t// Disable hedging\n\tc.SetHedging(nil)\n\tassertEqual(t, false, c.isHedgingEnabled())\n\tassertEqual(t, 1, c.RetryCount()) // Retry count should remain\n}\n"
  },
  {
    "path": "context_test.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// 2016 Andrew Grigorev (https://github.com/ei-grad)\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net/http\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestClientSetContext(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\n\tassertNil(t, c.Context())\n\n\tc.SetContext(context.Background())\n\n\tresp, err := c.R().Get(ts.URL + \"/\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"200 OK\", resp.Status())\n\tassertEqual(t, \"TestGet: text response\", resp.String())\n\n\tlogResponse(t, resp)\n}\n\nfunc TestRequestSetContext(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tresp, err := dcnl().R().\n\t\tSetContext(context.Background()).\n\t\tGet(ts.URL + \"/\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"200 OK\", resp.Status())\n\tassertEqual(t, \"TestGet: text response\", resp.String())\n\n\tlogResponse(t, resp)\n}\n\nfunc TestSetContextWithError(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tresp, err := dcnlr().\n\t\tSetContext(context.Background()).\n\t\tGet(ts.URL + \"/mypage\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusBadRequest, resp.StatusCode(), \"expected bad request status code\")\n\tassertEqual(t, \"\", resp.String(), \"expected empty response body on bad request\")\n\n\tlogResponse(t, resp)\n}\n\nfunc TestSetContextCancel(t *testing.T) {\n\tch := make(chan struct{})\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer func() {\n\t\t\tch <- struct{}{} // tell test request is finished\n\t\t}()\n\t\tt.Logf(\"Server: %v %v\", r.Method, r.URL.Path)\n\t\tch <- struct{}{}\n\t\t<-ch // wait for client to finish request\n\t\tn, err := w.Write([]byte(\"TestSetContextCancel: response\"))\n\t\t// FIXME? test server doesn't handle request cancellation\n\t\tt.Logf(\"Server: wrote %d bytes\", n)\n\t\tt.Logf(\"Server: err is %v \", err)\n\t})\n\tdefer ts.Close()\n\n\tctx, cancel := context.WithCancel(context.Background())\n\n\tgo func() {\n\t\t<-ch // wait for server to start request handling\n\t\tcancel()\n\t}()\n\n\t_, err := dcnl().R().\n\t\tSetContext(ctx).\n\t\tGet(ts.URL + \"/\")\n\n\tch <- struct{}{} // tell server to continue request handling\n\n\t<-ch // wait for server to finish request handling\n\n\tt.Logf(\"Error: %v\", err)\n\tif !errIsContextCanceled(err) {\n\t\tt.Errorf(\"Got unexpected error: %v\", err)\n\t}\n}\n\nfunc TestSetContextCancelRetry(t *testing.T) {\n\treqCount := 0\n\tch := make(chan struct{})\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\treqCount++\n\t\tdefer func() {\n\t\t\tch <- struct{}{} // tell test request is finished\n\t\t}()\n\t\tt.Logf(\"Server: %v %v\", r.Method, r.URL.Path)\n\t\tch <- struct{}{}\n\t\t<-ch // wait for client to finish request\n\t\tn, err := w.Write([]byte(\"TestSetContextCancel: response\"))\n\t\t// FIXME? test server doesn't handle request cancellation\n\t\tt.Logf(\"Server: wrote %d bytes\", n)\n\t\tt.Logf(\"Server: err is %v \", err)\n\t})\n\tdefer ts.Close()\n\n\tctx, cancel := context.WithCancel(context.Background())\n\n\tgo func() {\n\t\t<-ch // wait for server to start request handling\n\t\tcancel()\n\t}()\n\n\tc := dcnl().\n\t\tSetTimeout(time.Second * 3).\n\t\tSetRetryCount(3)\n\n\t_, err := c.R().\n\t\tSetContext(ctx).\n\t\tGet(ts.URL + \"/\")\n\n\tch <- struct{}{} // tell server to continue request handling\n\n\t<-ch // wait for server to finish request handling\n\n\tt.Logf(\"Error: %v\", err)\n\tif !errIsContextCanceled(err) {\n\t\tt.Errorf(\"Got unexpected error: %v\", err)\n\t}\n\n\tif reqCount != 1 {\n\t\tt.Errorf(\"Request was retried %d times instead of 1\", reqCount)\n\t}\n}\n\nfunc TestSetContextCancelWithError(t *testing.T) {\n\tch := make(chan struct{})\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tdefer func() {\n\t\t\tch <- struct{}{} // tell test request is finished\n\t\t}()\n\t\tt.Logf(\"Server: %v %v\", r.Method, r.URL.Path)\n\t\tt.Log(\"Server: sending StatusBadRequest response\")\n\t\tw.WriteHeader(http.StatusBadRequest)\n\t\tch <- struct{}{}\n\t\t<-ch // wait for client to finish request\n\t\tn, err := w.Write([]byte(\"TestSetContextCancelWithError: response\"))\n\t\t// FIXME? test server doesn't handle request cancellation\n\t\tt.Logf(\"Server: wrote %d bytes\", n)\n\t\tt.Logf(\"Server: err is %v \", err)\n\t})\n\tdefer ts.Close()\n\n\tctx, cancel := context.WithCancel(context.Background())\n\n\tgo func() {\n\t\t<-ch // wait for server to start request handling\n\t\tcancel()\n\t}()\n\n\t_, err := dcnl().R().\n\t\tSetContext(ctx).\n\t\tGet(ts.URL + \"/\")\n\n\tch <- struct{}{} // tell server to continue request handling\n\n\t<-ch // wait for server to finish request handling\n\n\tt.Logf(\"Error: %v\", err)\n\tif !errIsContextCanceled(err) {\n\t\tt.Errorf(\"Got unexpected error: %v\", err)\n\t}\n}\n\nfunc TestClientRetryWithSetContext(t *testing.T) {\n\tvar attempt int32\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tt.Logf(\"Method: %v\", r.Method)\n\t\tt.Logf(\"Path: %v\", r.URL.Path)\n\t\tif atomic.AddInt32(&attempt, 1) <= 4 {\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t}\n\t\t_, _ = w.Write([]byte(\"TestClientRetry page\"))\n\t})\n\tdefer ts.Close()\n\n\tc := dcnl().\n\t\tSetTimeout(50 * time.Millisecond).\n\t\tSetRetryCount(3)\n\n\t_, err := c.R().\n\t\tSetContext(context.Background()).\n\t\tGet(ts.URL + \"/\")\n\n\tassertNotNil(t, ts)\n\tassertNotNil(t, err)\n\tassertErrorIs(t, context.DeadlineExceeded, err, \"expected context deadline exceeded error\")\n}\n\nfunc TestRequestContext(t *testing.T) {\n\tclient := dcnl()\n\tr := client.NewRequest()\n\tassertNotNil(t, r.Context(), \"expected default context to be non-nil\")\n\n\tr.SetContext(context.Background())\n\tassertNotNil(t, r.Context(), \"expected context to be set\")\n}\n\nfunc errIsContextCanceled(err error) bool {\n\treturn errors.Is(err, context.Canceled)\n}\n"
  },
  {
    "path": "curl.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\n\t\"net/url\"\n\t\"strings\"\n)\n\nfunc buildCurlCmd(req *Request) string {\n\t// generate curl raw headers\n\tvar curl = \"curl -X \" + req.Method + \" \"\n\theaders := dumpCurlHeaders(req.RawRequest)\n\tfor _, kv := range *headers {\n\t\tcurl += \"-H \" + cmdQuote(kv[0]+\": \"+kv[1]) + \" \"\n\t}\n\n\t// generate curl cookies\n\tif cookieJar := req.client.CookieJar(); cookieJar != nil {\n\t\tif cookies := cookieJar.Cookies(req.RawRequest.URL); len(cookies) > 0 {\n\t\t\tcurl += \"-H \" + cmdQuote(dumpCurlCookies(cookies)) + \" \"\n\t\t}\n\t}\n\n\t// generate curl body except for io.Reader and multipart request flow\n\tif req.RawRequest.GetBody != nil {\n\t\tbody, err := req.RawRequest.GetBody()\n\t\tif err == nil {\n\t\t\tbuf, _ := io.ReadAll(body)\n\t\t\tcurl += \"-d \" + cmdQuote(string(bytes.TrimRight(buf, \"\\n\"))) + \" \"\n\t\t} else {\n\t\t\treq.log.Errorf(\"curl: %v\", err)\n\t\t\tcurl += \"-d ''\"\n\t\t}\n\t}\n\n\turlString := cmdQuote(req.RawRequest.URL.String())\n\tif urlString == \"''\" {\n\t\turlString = \"'http://unexecuted-request'\"\n\t}\n\tcurl += urlString\n\treturn curl\n}\n\n// dumpCurlCookies dumps cookies to curl format\nfunc dumpCurlCookies(cookies []*http.Cookie) string {\n\tsb := strings.Builder{}\n\tsb.WriteString(\"Cookie: \")\n\tfor _, cookie := range cookies {\n\t\tsb.WriteString(cookie.Name + \"=\" + url.QueryEscape(cookie.Value) + \"&\")\n\t}\n\treturn strings.TrimRight(sb.String(), \"&\")\n}\n\n// dumpCurlHeaders dumps headers to curl format\nfunc dumpCurlHeaders(req *http.Request) *[][2]string {\n\theaders := [][2]string{}\n\tfor k, vs := range req.Header {\n\t\tfor _, v := range vs {\n\t\t\theaders = append(headers, [2]string{k, v})\n\t\t}\n\t}\n\tn := len(headers)\n\tfor i := 0; i < n; i++ {\n\t\tfor j := n - 1; j > i; j-- {\n\t\t\tjj := j - 1\n\t\t\th1, h2 := headers[j], headers[jj]\n\t\t\tif h1[0] < h2[0] {\n\t\t\t\theaders[jj], headers[j] = headers[j], headers[jj]\n\t\t\t}\n\t\t}\n\t}\n\treturn &headers\n}\n\nvar regexCmdQuote = regexp.MustCompile(`[^\\w@%+=:,./-]`)\n\n// cmdQuote method to escape arbitrary strings for a safe use as\n// command line arguments in the most common POSIX shells.\n//\n// The original Python package which this work was inspired by can be found\n// at https://pypi.python.org/pypi/shellescape.\nfunc cmdQuote(s string) string {\n\tif len(s) == 0 {\n\t\treturn \"''\"\n\t}\n\n\tif regexCmdQuote.MatchString(s) {\n\t\treturn \"'\" + strings.ReplaceAll(s, \"'\", \"'\\\"'\\\"'\") + \"'\"\n\t}\n\n\treturn s\n}\n"
  },
  {
    "path": "curl_test.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\t\"strings\"\n\t\"testing\"\n)\n\nfunc TestCurlGenerateUnexecutedRequest(t *testing.T) {\n\treq := dcnldr().\n\t\tSetBody(map[string]string{\n\t\t\t\"name\": \"Resty\",\n\t\t}).\n\t\tSetCookies(\n\t\t\t[]*http.Cookie{\n\t\t\t\t{Name: \"count\", Value: \"1\"},\n\t\t\t},\n\t\t).\n\t\tSetMethod(MethodPost)\n\n\tassertEqual(t, \"\", req.CurlCmd())\n\n\tcurlCmdUnexecuted := req.SetCurlCmdGenerate(true).CurlCmd()\n\treq.SetCurlCmdGenerate(false)\n\n\tif !strings.Contains(curlCmdUnexecuted, \"Cookie: count=1\") ||\n\t\t!strings.Contains(curlCmdUnexecuted, \"curl -X POST\") ||\n\t\t!strings.Contains(curlCmdUnexecuted, `-d '{\"name\":\"Resty\"}'`) {\n\t\tt.Fatal(\"Incomplete curl:\", curlCmdUnexecuted)\n\t} else {\n\t\tt.Log(\"curlCmdUnexecuted: \\n\", curlCmdUnexecuted)\n\t}\n\n}\n\nfunc TestCurlGenerateExecutedRequest(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tdata := map[string]string{\n\t\t\"name\": \"Resty\",\n\t}\n\tc := dcnl().SetDebug(true)\n\treq := c.R().\n\t\tSetBody(data).\n\t\tSetCookies(\n\t\t\t[]*http.Cookie{\n\t\t\t\t{Name: \"count\", Value: \"1\"},\n\t\t\t},\n\t\t)\n\n\turl := ts.URL + \"/curl-cmd-post\"\n\tresp, err := req.\n\t\tSetCurlCmdGenerate(true).\n\t\tPost(url)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\tcurlCmdExecuted := resp.Request.CurlCmd()\n\n\tc.SetCurlCmdGenerate(false)\n\treq.SetCurlCmdGenerate(false)\n\tif !strings.Contains(curlCmdExecuted, \"Cookie: count=1\") ||\n\t\t!strings.Contains(curlCmdExecuted, \"curl -X POST\") ||\n\t\t!strings.Contains(curlCmdExecuted, `-d '{\"name\":\"Resty\"}'`) ||\n\t\t!strings.Contains(curlCmdExecuted, url) {\n\t\tt.Fatal(\"Incomplete curl:\", curlCmdExecuted)\n\t} else {\n\t\tt.Log(\"curlCmdExecuted: \\n\", curlCmdExecuted)\n\t}\n}\n\nfunc TestCurlCmdDebugMode(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tc, logBuf := dcldb()\n\tc.SetCurlCmdGenerate(true).\n\t\tSetCurlCmdDebugLog(true)\n\n\t// Build request\n\treq := c.R().\n\t\tSetBody(map[string]string{\n\t\t\t\"name\": \"Resty\",\n\t\t}).\n\t\tSetCookies(\n\t\t\t[]*http.Cookie{\n\t\t\t\t{Name: \"count\", Value: \"1\"},\n\t\t\t},\n\t\t).\n\t\tSetCurlCmdDebugLog(true)\n\n\t// Execute request: set debug mode\n\turl := ts.URL + \"/curl-cmd-post\"\n\t_, err := req.SetDebug(true).Post(url)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tc.SetCurlCmdGenerate(false)\n\treq.SetCurlCmdGenerate(false)\n\n\t// test logContent curl cmd\n\tlogContent := logBuf.String()\n\tif !strings.Contains(logContent, \"Cookie: count=1\") ||\n\t\t!strings.Contains(logContent, `-d '{\"name\":\"Resty\"}'`) {\n\t\tt.Fatal(\"Incomplete debug curl info:\", logContent)\n\t}\n}\n\nfunc TestCurl_buildCurlCmd(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\tmethod   string\n\t\turl      string\n\t\theaders  map[string]string\n\t\tbody     string\n\t\tcookies  []*http.Cookie\n\t\texpected string\n\t}{\n\t\t{\n\t\t\tname:     \"With Headers\",\n\t\t\tmethod:   \"GET\",\n\t\t\turl:      \"http://example.com\",\n\t\t\theaders:  map[string]string{\"Content-Type\": \"application/json\", \"Authorization\": \"Bearer token\"},\n\t\t\texpected: \"curl -X GET -H 'Authorization: Bearer token' -H 'Content-Type: application/json' http://example.com\",\n\t\t},\n\t\t{\n\t\t\tname:     \"With Body\",\n\t\t\tmethod:   \"POST\",\n\t\t\turl:      \"http://example.com\",\n\t\t\theaders:  map[string]string{\"Content-Type\": \"application/json\"},\n\t\t\tbody:     `{\"key\":\"value\"}`,\n\t\t\texpected: \"curl -X POST -H 'Content-Type: application/json' -d '{\\\"key\\\":\\\"value\\\"}' http://example.com\",\n\t\t},\n\t\t{\n\t\t\tname:     \"With Empty Body\",\n\t\t\tmethod:   \"POST\",\n\t\t\turl:      \"http://example.com\",\n\t\t\theaders:  map[string]string{\"Content-Type\": \"application/json\"},\n\t\t\texpected: \"curl -X POST -H 'Content-Type: application/json' http://example.com\",\n\t\t},\n\t\t{\n\t\t\tname:     \"With Query Params\",\n\t\t\tmethod:   \"GET\",\n\t\t\turl:      \"http://example.com?param1=value1&param2=value2\",\n\t\t\texpected: \"curl -X GET 'http://example.com?param1=value1&param2=value2'\",\n\t\t},\n\t\t{\n\t\t\tname:     \"With Special Characters in URL\",\n\t\t\tmethod:   \"GET\",\n\t\t\turl:      \"http://example.com/path with spaces\",\n\t\t\texpected: \"curl -X GET http://example.com/path%20with%20spaces\",\n\t\t},\n\t\t{\n\t\t\tname:     \"With Cookies\",\n\t\t\tmethod:   \"GET\",\n\t\t\turl:      \"http://example.com\",\n\t\t\tcookies:  []*http.Cookie{{Name: \"session_id\", Value: \"abc123\"}},\n\t\t\texpected: \"curl -X GET -H 'Cookie: session_id=abc123' http://example.com\",\n\t\t},\n\t\t{\n\t\t\tname:     \"Without Cookies\",\n\t\t\tmethod:   \"GET\",\n\t\t\turl:      \"http://example.com\",\n\t\t\texpected: \"curl -X GET http://example.com\",\n\t\t},\n\t\t{\n\t\t\tname:     \"With Multiple Cookies\",\n\t\t\tmethod:   \"GET\",\n\t\t\turl:      \"http://example.com\",\n\t\t\tcookies:  []*http.Cookie{{Name: \"session_id\", Value: \"abc123\"}, {Name: \"user_id\", Value: \"user456\"}},\n\t\t\texpected: \"curl -X GET -H 'Cookie: session_id=abc123&user_id=user456' http://example.com\",\n\t\t},\n\t\t{\n\t\t\tname:     \"With Empty Cookie Jar\",\n\t\t\tmethod:   \"GET\",\n\t\t\turl:      \"http://example.com\",\n\t\t\texpected: \"curl -X GET http://example.com\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := dcnl()\n\t\t\treq := c.R().SetMethod(tt.method).SetURL(tt.url)\n\n\t\t\tif !isStringEmpty(tt.body) {\n\t\t\t\treq.SetBody(bytes.NewBufferString(tt.body))\n\t\t\t}\n\n\t\t\tfor k, v := range tt.headers {\n\t\t\t\treq.SetHeader(k, v)\n\t\t\t}\n\n\t\t\terr := createRawRequest(c, req)\n\t\t\tassertNil(t, err)\n\n\t\t\tif len(tt.cookies) > 0 {\n\t\t\t\tcookieJar, _ := cookiejar.New(nil)\n\t\t\t\tcookieJar.SetCookies(req.RawRequest.URL, tt.cookies)\n\t\t\t\tc.SetCookieJar(cookieJar)\n\t\t\t}\n\n\t\t\tcurlCmd := buildCurlCmd(req)\n\t\t\tassertEqual(t, tt.expected, curlCmd)\n\t\t})\n\t}\n}\n\nfunc TestCurlRequestGetBodyError(t *testing.T) {\n\tc := dcnl().\n\t\tSetDebug(true).\n\t\tSetRequestMiddlewares(\n\t\t\tMiddlewareRequestCreate,\n\t\t\tfunc(_ *Client, r *Request) error {\n\t\t\t\tr.RawRequest.GetBody = func() (io.ReadCloser, error) {\n\t\t\t\t\treturn nil, errors.New(\"test case error\")\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t)\n\n\treq := c.R().\n\t\tSetBody(map[string]string{\n\t\t\t\"name\": \"Resty\",\n\t\t}).\n\t\tSetCookies(\n\t\t\t[]*http.Cookie{\n\t\t\t\t{Name: \"count\", Value: \"1\"},\n\t\t\t},\n\t\t).\n\t\tSetMethod(MethodPost)\n\n\tassertEqual(t, \"\", req.CurlCmd())\n\n\tcurlCmdUnexecuted := req.SetCurlCmdGenerate(true).CurlCmd()\n\treq.SetCurlCmdGenerate(false)\n\n\tif !strings.Contains(curlCmdUnexecuted, \"Cookie: count=1\") ||\n\t\t!strings.Contains(curlCmdUnexecuted, \"curl -X POST\") ||\n\t\t!strings.Contains(curlCmdUnexecuted, `-d ''`) {\n\t\tt.Fatal(\"Incomplete curl:\", curlCmdUnexecuted)\n\t} else {\n\t\tt.Log(\"curlCmdUnexecuted: \\n\", curlCmdUnexecuted)\n\t}\n}\n\nfunc TestCurlRequestMiddlewaresError(t *testing.T) {\n\terrMsg := \"middleware error\"\n\tc := dcnl().SetDebug(true).\n\t\tSetRequestMiddlewares(\n\t\t\tfunc(c *Client, r *Request) error {\n\t\t\t\treturn errors.New(errMsg)\n\t\t\t},\n\t\t\tMiddlewareRequestCreate,\n\t\t)\n\n\tcurlCmdUnexecuted := c.R().SetCurlCmdGenerate(true).CurlCmd()\n\tassertEqual(t, \"\", curlCmdUnexecuted)\n}\n\nfunc TestCurlMiscTestCoverage(t *testing.T) {\n\tcookieStr := dumpCurlCookies([]*http.Cookie{\n\t\t{Name: \"count\", Value: \"1\"},\n\t})\n\tassertEqual(t, \"Cookie: count=1\", cookieStr)\n}\n"
  },
  {
    "path": "debug.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"time\"\n)\n\ntype (\n\t// DebugLogCallbackFunc function type is for request and response debug log callback purposes.\n\t// It gets called before Resty logs it\n\tDebugLogCallbackFunc func(*DebugLog)\n\n\t// DebugLogFormatterFunc function type is used to implement debug log formatting.\n\t// See out of the box [DebugLogStringFormatter], [DebugLogJSONFormatter]\n\tDebugLogFormatterFunc func(*DebugLog) string\n\n\t// DebugLog struct is used to collect details from Resty request and response\n\t// for debug logging callback purposes.\n\tDebugLog struct {\n\t\tRequest   *DebugLogRequest  `json:\"request\"`\n\t\tResponse  *DebugLogResponse `json:\"response\"`\n\t\tTraceInfo *TraceInfo        `json:\"trace_info\"`\n\t}\n\n\t// DebugLogRequest type used to capture debug info about the [Request].\n\tDebugLogRequest struct {\n\t\tCorrelationID string      `json:\"correlation_id\"`\n\t\tHost          string      `json:\"host\"`\n\t\tURI           string      `json:\"uri\"`\n\t\tMethod        string      `json:\"method\"`\n\t\tProto         string      `json:\"proto\"`\n\t\tHeader        http.Header `json:\"header\"`\n\t\tCurlCmd       string      `json:\"curl_cmd\"`\n\t\tAttempt       int         `json:\"attempt\"`\n\t\tBody          string      `json:\"body\"`\n\t}\n\n\t// DebugLogResponse type used to capture debug info about the [Response].\n\tDebugLogResponse struct {\n\t\tStatusCode int           `json:\"status_code\"`\n\t\tStatus     string        `json:\"status\"`\n\t\tProto      string        `json:\"proto\"`\n\t\tReceivedAt time.Time     `json:\"received_at\"`\n\t\tDuration   time.Duration `json:\"duration\"`\n\t\tSize       int64         `json:\"size\"`\n\t\tHeader     http.Header   `json:\"header\"`\n\t\tBody       string        `json:\"body\"`\n\t}\n)\n\n// DebugLogFormatter function formats the given debug log info in human readable\n// format.\n//\n// This is the default debug log formatter in the Resty.\nfunc DebugLogFormatter(dl *DebugLog) string {\n\tdebugLog := \"\\n==============================================================================\\n\"\n\n\treq := dl.Request\n\tif len(req.CurlCmd) > 0 {\n\t\tdebugLog += \"~~~ REQUEST(CURL) ~~~\\n\" +\n\t\t\tfmt.Sprintf(\"\t%v\\n\", req.CurlCmd)\n\t}\n\tdebugLog += \"~~~ REQUEST ~~~\\n\" +\n\t\tfmt.Sprintf(\"CORRELATION ID: %s\\n\", req.CorrelationID) +\n\t\tfmt.Sprintf(\"%s  %s  %s\\n\", req.Method, req.URI, req.Proto) +\n\t\tfmt.Sprintf(\"HOST   : %s\\n\", req.Host) +\n\t\tfmt.Sprintf(\"HEADERS:\\n%s\\n\", composeHeaders(req.Header)) +\n\t\tfmt.Sprintf(\"BODY   :\\n%v\\n\", req.Body) +\n\t\tfmt.Sprintf(\"ATTEMPT       : %d\\n\", req.Attempt) +\n\t\t\"------------------------------------------------------------------------------\\n\"\n\n\tres := dl.Response\n\tdebugLog += \"~~~ RESPONSE ~~~\\n\" +\n\t\tfmt.Sprintf(\"STATUS       : %s\\n\", res.Status) +\n\t\tfmt.Sprintf(\"PROTO        : %s\\n\", res.Proto) +\n\t\tfmt.Sprintf(\"RECEIVED AT  : %v\\n\", res.ReceivedAt.Format(time.RFC3339Nano)) +\n\t\tfmt.Sprintf(\"DURATION     : %v\\n\", res.Duration) +\n\t\t\"HEADERS      :\\n\" +\n\t\tcomposeHeaders(res.Header) + \"\\n\" +\n\t\tfmt.Sprintf(\"BODY         :\\n%v\\n\", res.Body)\n\tif dl.TraceInfo != nil {\n\t\tdebugLog += \"------------------------------------------------------------------------------\\n\"\n\t\tdebugLog += fmt.Sprintf(\"%v\\n\", dl.TraceInfo)\n\t}\n\tdebugLog += \"==============================================================================\\n\"\n\n\treturn debugLog\n}\n\n// DebugLogJSONFormatter function formats the given debug log info in JSON format.\nfunc DebugLogJSONFormatter(dl *DebugLog) string {\n\treturn toJSON(dl)\n}\n\nfunc debugLogger(c *Client, res *Response) {\n\treq := res.Request\n\tif !req.IsDebug {\n\t\treturn\n\t}\n\n\trdl := &DebugLogResponse{\n\t\tStatusCode: res.StatusCode(),\n\t\tStatus:     res.Status(),\n\t\tProto:      res.Proto(),\n\t\tReceivedAt: res.ReceivedAt(),\n\t\tDuration:   res.Duration(),\n\t\tSize:       res.Size(),\n\t\tHeader:     sanitizeHeaders(res.Header().Clone()),\n\t\tBody:       res.fmtBodyString(res.Request.DebugBodyLimit),\n\t}\n\n\tdl := &DebugLog{\n\t\tRequest:  req.values[debugRequestLogKey].(*DebugLogRequest),\n\t\tResponse: rdl,\n\t}\n\n\tif res.Request.IsTrace {\n\t\tti := req.TraceInfo()\n\t\tdl.TraceInfo = &ti\n\t}\n\n\tdblCallback := c.debugLogCallbackFunc()\n\tif dblCallback != nil {\n\t\tdblCallback(dl)\n\t}\n\n\tformatterFunc := c.debugLogFormatterFunc()\n\tif formatterFunc != nil {\n\t\tdebugLog := formatterFunc(dl)\n\t\treq.log.Debugf(\"%s\", debugLog)\n\t}\n}\n\nconst debugRequestLogKey = \"__restyDebugRequestLog\"\n\nfunc prepareRequestDebugInfo(c *Client, r *Request) {\n\tif !r.IsDebug {\n\t\treturn\n\t}\n\n\trr := r.RawRequest\n\trh := rr.Header.Clone()\n\tif c.Client().Jar != nil {\n\t\tfor _, cookie := range c.Client().Jar.Cookies(r.RawRequest.URL) {\n\t\t\ts := fmt.Sprintf(\"%s=%s\", cookie.Name, cookie.Value)\n\t\t\tif c := rh.Get(hdrCookieKey); isStringEmpty(c) {\n\t\t\t\trh.Set(hdrCookieKey, s)\n\t\t\t} else {\n\t\t\t\trh.Set(hdrCookieKey, c+\"; \"+s)\n\t\t\t}\n\t\t}\n\t}\n\n\trdl := &DebugLogRequest{\n\t\tCorrelationID: r.CorrelationID,\n\t\tHost:          rr.URL.Host,\n\t\tURI:           rr.URL.RequestURI(),\n\t\tMethod:        r.Method,\n\t\tProto:         rr.Proto,\n\t\tHeader:        sanitizeHeaders(rh),\n\t\tAttempt:       r.Attempt,\n\t\tBody:          r.fmtBodyString(r.DebugBodyLimit),\n\t}\n\tif r.isCurlCmdGenerate && r.isCurlCmdDebugLog {\n\t\trdl.CurlCmd = r.curlCmdString\n\t}\n\n\tr.initValuesMap()\n\tr.values[debugRequestLogKey] = rdl\n}\n"
  },
  {
    "path": "digest.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// 2023 Segev Dagan (https://github.com/segevda)\n// 2024 Philipp Wolfer (https://github.com/phw)\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bytes\"\n\t\"crypto/md5\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"crypto/sha512\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nvar (\n\tErrDigestBadChallenge    = errors.New(\"resty: digest: challenge is bad\")\n\tErrDigestInvalidCharset  = errors.New(\"resty: digest: invalid charset\")\n\tErrDigestAlgNotSupported = errors.New(\"resty: digest: algorithm is not supported\")\n\tErrDigestQopNotSupported = errors.New(\"resty: digest: qop is not supported\")\n)\n\n// Reference: https://datatracker.ietf.org/doc/html/rfc7616#section-6.1\nvar digestHashFuncs = map[string]func() hash.Hash{\n\t\"\":                 md5.New,\n\t\"MD5\":              md5.New,\n\t\"MD5-sess\":         md5.New,\n\t\"SHA-256\":          sha256.New,\n\t\"SHA-256-sess\":     sha256.New,\n\t\"SHA-512\":          sha512.New,\n\t\"SHA-512-sess\":     sha512.New,\n\t\"SHA-512-256\":      sha512.New512_256,\n\t\"SHA-512-256-sess\": sha512.New512_256,\n}\n\nconst (\n\tqopAuth    = \"auth\"\n\tqopAuthInt = \"auth-int\"\n)\n\ntype digestTransport struct {\n\t*credentials\n\ttransport http.RoundTripper\n}\n\nfunc (dt *digestTransport) RoundTrip(req *http.Request) (*http.Response, error) {\n\t// first request without body for all HTTP verbs\n\treq1 := dt.cloneReq(req, true)\n\n\t// make a request to get the 401 that contains the challenge.\n\tres, err := dt.transport.RoundTrip(req1)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif res.StatusCode != http.StatusUnauthorized {\n\t\treturn res, nil\n\t}\n\t_, _ = ioCopy(io.Discard, res.Body)\n\tcloseq(res.Body)\n\n\tchaHdrValue := strings.TrimSpace(res.Header.Get(hdrWwwAuthenticateKey))\n\tif chaHdrValue == \"\" {\n\t\treturn nil, ErrDigestBadChallenge\n\t}\n\n\tcha, err := dt.parseChallenge(chaHdrValue)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// prepare second request\n\treq2 := dt.cloneReq(req, false)\n\tcred, err := dt.createCredentials(cha, req2)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tauth, err := cred.digest(cha)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq2.Header.Set(hdrAuthorizationKey, auth)\n\treturn dt.transport.RoundTrip(req2)\n}\n\nfunc (dt *digestTransport) cloneReq(r *http.Request, first bool) *http.Request {\n\tr1 := r.Clone(r.Context())\n\tif first {\n\t\tr1.Body = http.NoBody\n\t\tr1.ContentLength = 0\n\t\tr1.GetBody = nil\n\t}\n\treturn r1\n}\n\nfunc (dt *digestTransport) parseChallenge(input string) (*digestChallenge, error) {\n\tconst ws = \" \\n\\r\\t\"\n\ts := strings.Trim(input, ws)\n\tif !strings.HasPrefix(s, \"Digest \") {\n\t\treturn nil, ErrDigestBadChallenge\n\t}\n\n\ts = strings.Trim(s[7:], ws)\n\tc := &digestChallenge{}\n\tb := strings.Builder{}\n\tkey := \"\"\n\tquoted := false\n\tfor _, r := range s {\n\t\tswitch r {\n\t\tcase '\"':\n\t\t\tquoted = !quoted\n\t\tcase ',':\n\t\t\tif quoted {\n\t\t\t\tb.WriteRune(r)\n\t\t\t} else {\n\t\t\t\tval := strings.Trim(b.String(), ws)\n\t\t\t\tb.Reset()\n\t\t\t\tif err := c.setValue(key, val); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tkey = \"\"\n\t\t\t}\n\t\tcase '=':\n\t\t\tif quoted {\n\t\t\t\tb.WriteRune(r)\n\t\t\t} else {\n\t\t\t\tkey = strings.Trim(b.String(), ws)\n\t\t\t\tb.Reset()\n\t\t\t}\n\t\tdefault:\n\t\t\tb.WriteRune(r)\n\t\t}\n\t}\n\n\tkey = strings.TrimSpace(key)\n\tif quoted || (key == \"\" && b.Len() > 0) {\n\t\treturn nil, ErrDigestBadChallenge\n\t}\n\n\tif key != \"\" {\n\t\tval := strings.Trim(b.String(), ws)\n\t\tif err := c.setValue(key, val); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn c, nil\n}\n\nfunc (dt *digestTransport) createCredentials(cha *digestChallenge, req *http.Request) (*digestCredentials, error) {\n\tcred := &digestCredentials{\n\t\tusername:      dt.Username,\n\t\tpassword:      dt.Password,\n\t\turi:           req.URL.RequestURI(),\n\t\tmethod:        req.Method,\n\t\trealm:         cha.realm,\n\t\tnonce:         cha.nonce,\n\t\tnc:            cha.nc,\n\t\talgorithm:     cha.algorithm,\n\t\tsessAlgorithm: strings.HasSuffix(cha.algorithm, \"-sess\"),\n\t\topaque:        cha.opaque,\n\t\tuserHash:      cha.userHash,\n\t}\n\n\tif cha.isQopSupported(qopAuthInt) {\n\t\tif err := dt.prepareBody(req); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"resty: digest: failed to prepare body for auth-int: %w\", err)\n\t\t}\n\t\tbody, err := req.GetBody()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"resty: digest: failed to get body for auth-int: %w\", err)\n\t\t}\n\t\tif body != http.NoBody {\n\t\t\tdefer closeq(body)\n\t\t\th := newHashFunc(cha.algorithm)\n\t\t\tif _, err := ioCopy(h, body); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tcred.bodyHash = hex.EncodeToString(h.Sum(nil))\n\t\t}\n\t}\n\n\treturn cred, nil\n}\n\nfunc (dt *digestTransport) prepareBody(req *http.Request) error {\n\tif req.GetBody != nil {\n\t\treturn nil\n\t}\n\n\tif req.Body == nil || req.Body == http.NoBody {\n\t\treq.GetBody = func() (io.ReadCloser, error) {\n\t\t\treturn http.NoBody, nil\n\t\t}\n\t\treturn nil\n\t}\n\n\tb, err := ioReadAll(req.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\tcloseq(req.Body)\n\treq.Body = io.NopCloser(bytes.NewReader(b))\n\treq.GetBody = func() (io.ReadCloser, error) {\n\t\treturn io.NopCloser(bytes.NewReader(b)), nil\n\t}\n\n\treturn nil\n}\n\ntype digestChallenge struct {\n\trealm     string\n\tdomain    string\n\tnonce     string\n\topaque    string\n\tstale     string\n\talgorithm string\n\tqop       []string\n\tnc        int\n\tuserHash  string\n}\n\nfunc (dc *digestChallenge) isQopSupported(qop string) bool {\n\tfor _, v := range dc.qop {\n\t\tif v == qop {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (dc *digestChallenge) setValue(k, v string) error {\n\tswitch k {\n\tcase \"realm\":\n\t\tdc.realm = v\n\tcase \"domain\":\n\t\tdc.domain = v\n\tcase \"nonce\":\n\t\tdc.nonce = v\n\tcase \"opaque\":\n\t\tdc.opaque = v\n\tcase \"stale\":\n\t\tdc.stale = v\n\tcase \"algorithm\":\n\t\tdc.algorithm = v\n\tcase \"qop\":\n\t\tif !isStringEmpty(v) {\n\t\t\tdc.qop = strings.Split(v, \",\")\n\t\t}\n\tcase \"charset\":\n\t\tif strings.ToUpper(v) != \"UTF-8\" {\n\t\t\treturn ErrDigestInvalidCharset\n\t\t}\n\tcase \"nc\":\n\t\tnc, err := strconv.ParseInt(v, 16, 32)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"resty: digest: invalid nc: %w\", err)\n\t\t}\n\t\tdc.nc = int(nc)\n\tcase \"userhash\":\n\t\tdc.userHash = v\n\tdefault:\n\t\treturn ErrDigestBadChallenge\n\t}\n\treturn nil\n}\n\ntype digestCredentials struct {\n\tusername      string\n\tpassword      string\n\tuserHash      string\n\tmethod        string\n\turi           string\n\trealm         string\n\tnonce         string\n\talgorithm     string\n\tsessAlgorithm bool\n\tcnonce        string\n\topaque        string\n\tqop           string\n\tnc            int\n\tresponse      string\n\tbodyHash      string\n}\n\nfunc (dc *digestCredentials) parseQop(cha *digestChallenge) error {\n\tif len(cha.qop) == 0 {\n\t\treturn nil\n\t}\n\n\tif cha.isQopSupported(qopAuth) {\n\t\tdc.qop = qopAuth\n\t\treturn nil\n\t}\n\n\tif cha.isQopSupported(qopAuthInt) {\n\t\tdc.qop = qopAuthInt\n\t\treturn nil\n\t}\n\n\treturn ErrDigestQopNotSupported\n}\n\nfunc (dc *digestCredentials) h(data string) string {\n\th := newHashFunc(dc.algorithm)\n\t_, _ = h.Write([]byte(data))\n\treturn hex.EncodeToString(h.Sum(nil))\n}\n\nfunc (dc *digestCredentials) digest(cha *digestChallenge) (string, error) {\n\tif _, ok := digestHashFuncs[dc.algorithm]; !ok {\n\t\treturn \"\", ErrDigestAlgNotSupported\n\t}\n\n\tif err := dc.parseQop(cha); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tdc.nc++\n\n\tb := make([]byte, 16)\n\t_, _ = io.ReadFull(rand.Reader, b)\n\tdc.cnonce = hex.EncodeToString(b)\n\n\tha1 := dc.ha1()\n\tha2 := dc.ha2()\n\n\tvar resp string\n\tswitch dc.qop {\n\tcase \"\":\n\t\tresp = fmt.Sprintf(\"%s:%s:%s\", ha1, dc.nonce, ha2)\n\tcase qopAuth, qopAuthInt:\n\t\tresp = fmt.Sprintf(\"%s:%s:%08x:%s:%s:%s\",\n\t\t\tha1, dc.nonce, dc.nc, dc.cnonce, dc.qop, ha2)\n\t}\n\tdc.response = dc.h(resp)\n\n\treturn \"Digest \" + dc.String(), nil\n}\n\n// https://datatracker.ietf.org/doc/html/rfc7616#section-3.4.2\nfunc (dc *digestCredentials) ha1() string {\n\ta1 := dc.h(fmt.Sprintf(\"%s:%s:%s\", dc.username, dc.realm, dc.password))\n\tif dc.sessAlgorithm {\n\t\treturn dc.h(fmt.Sprintf(\"%s:%s:%s\", a1, dc.nonce, dc.cnonce))\n\t}\n\treturn a1\n}\n\n// https://datatracker.ietf.org/doc/html/rfc7616#section-3.4.3\nfunc (dc *digestCredentials) ha2() string {\n\tif dc.qop == qopAuthInt {\n\t\treturn dc.h(fmt.Sprintf(\"%s:%s:%s\", dc.method, dc.uri, dc.bodyHash))\n\t}\n\treturn dc.h(fmt.Sprintf(\"%s:%s\", dc.method, dc.uri))\n}\n\nfunc (dc *digestCredentials) String() string {\n\tsl := make([]string, 0, 10)\n\t// https://datatracker.ietf.org/doc/html/rfc7616#section-3.4.4\n\tif dc.userHash == \"true\" {\n\t\tdc.username = dc.h(fmt.Sprintf(\"%s:%s\", dc.username, dc.realm))\n\t}\n\tsl = append(sl, fmt.Sprintf(`username=\"%s\"`, dc.username))\n\tsl = append(sl, fmt.Sprintf(`realm=\"%s\"`, dc.realm))\n\tsl = append(sl, fmt.Sprintf(`nonce=\"%s\"`, dc.nonce))\n\tsl = append(sl, fmt.Sprintf(`uri=\"%s\"`, dc.uri))\n\tif dc.algorithm != \"\" {\n\t\tsl = append(sl, fmt.Sprintf(`algorithm=%s`, dc.algorithm))\n\t}\n\tif dc.opaque != \"\" {\n\t\tsl = append(sl, fmt.Sprintf(`opaque=\"%s\"`, dc.opaque))\n\t}\n\tif dc.qop != \"\" {\n\t\tsl = append(sl, fmt.Sprintf(\"qop=%s\", dc.qop))\n\t\tsl = append(sl, fmt.Sprintf(\"nc=%08x\", dc.nc))\n\t\tsl = append(sl, fmt.Sprintf(`cnonce=\"%s\"`, dc.cnonce))\n\t}\n\tsl = append(sl, fmt.Sprintf(`userhash=%s`, dc.userHash))\n\tsl = append(sl, fmt.Sprintf(`response=\"%s\"`, dc.response))\n\n\treturn strings.Join(sl, \", \")\n}\n\nfunc newHashFunc(algorithm string) hash.Hash {\n\thf := digestHashFuncs[algorithm]\n\th := hf()\n\th.Reset()\n\treturn h\n}\n"
  },
  {
    "path": "digest_test.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n)\n\ntype digestServerConfig struct {\n\trealm, qop, nonce, opaque, algo, uri, charset, username, password, nc string\n}\n\nfunc defaultDigestServerConf() *digestServerConfig {\n\treturn &digestServerConfig{\n\t\trealm:    \"testrealm@host.com\",\n\t\tqop:      \"auth\",\n\t\tnonce:    \"dcd98b7102dd2f0e8b11d0f600bfb0c093\",\n\t\topaque:   \"5ccc069c403ebaf9f0171e9517f40e41\",\n\t\talgo:     \"MD5\",\n\t\turi:      \"/dir/index.html\",\n\t\tcharset:  \"utf-8\",\n\t\tusername: \"Mufasa\",\n\t\tpassword: \"Circle Of Life\",\n\t\tnc:       \"00000001\",\n\t}\n}\n\nfunc TestClientDigestAuth(t *testing.T) {\n\tconf := *defaultDigestServerConf()\n\tts := createDigestServer(t, &conf)\n\tdefer ts.Close()\n\n\tc := dcnl().\n\t\tSetBaseURL(ts.URL+\"/\").\n\t\tSetDigestAuth(conf.username, conf.password)\n\n\tresp, err := c.R().\n\t\tSetResult(&AuthSuccess{}).\n\t\tGet(conf.uri)\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n}\n\nfunc TestClientDigestAuthSession(t *testing.T) {\n\tconf := *defaultDigestServerConf()\n\tconf.algo = \"MD5-sess\"\n\tconf.qop = \"auth, auth-int\"\n\tts := createDigestServer(t, &conf)\n\tdefer ts.Close()\n\n\tc := dcnl().\n\t\tSetBaseURL(ts.URL+\"/\").\n\t\tSetDigestAuth(conf.username, conf.password)\n\n\tresp, err := c.R().\n\t\tSetResult(&AuthSuccess{}).\n\t\tGet(conf.uri)\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n}\n\nfunc TestClientDigestAuthErrors(t *testing.T) {\n\ttype test struct {\n\t\tmutateConf func(*digestServerConfig)\n\t\texpect     error\n\t}\n\ttests := []test{\n\t\t{mutateConf: func(c *digestServerConfig) { c.algo = \"BAD_ALGO\" }, expect: ErrDigestAlgNotSupported},\n\t\t{mutateConf: func(c *digestServerConfig) { c.qop = \"bad-qop\" }, expect: ErrDigestQopNotSupported},\n\t\t{mutateConf: func(c *digestServerConfig) { c.charset = \"utf-16\" }, expect: ErrDigestInvalidCharset},\n\t\t{mutateConf: func(c *digestServerConfig) { c.uri = \"/bad\" }, expect: ErrDigestBadChallenge},\n\t\t{mutateConf: func(c *digestServerConfig) { c.uri = \"/unknown_param\" }, expect: ErrDigestBadChallenge},\n\t\t{mutateConf: func(c *digestServerConfig) { c.uri = \"/missing_value\" }, expect: ErrDigestBadChallenge},\n\t\t{mutateConf: func(c *digestServerConfig) { c.uri = \"/unclosed_quote\" }, expect: ErrDigestBadChallenge},\n\t\t{mutateConf: func(c *digestServerConfig) { c.uri = \"/no_challenge\" }, expect: ErrDigestBadChallenge},\n\t\t{mutateConf: func(c *digestServerConfig) { c.uri = \"/status_500\" }, expect: nil},\n\t}\n\n\tfor _, tc := range tests {\n\t\tconf := *defaultDigestServerConf()\n\t\ttc.mutateConf(&conf)\n\t\tts := createDigestServer(t, &conf)\n\n\t\tc := dcnl().\n\t\t\tSetBaseURL(ts.URL+\"/\").\n\t\t\tSetDigestAuth(conf.username, conf.password)\n\n\t\t_, err := c.R().Get(conf.uri)\n\t\tassertErrorIs(t, tc.expect, err)\n\t\tts.Close()\n\t}\n}\n\nfunc TestClientDigestAuthWithBody(t *testing.T) {\n\tconf := *defaultDigestServerConf()\n\tts := createDigestServer(t, &conf)\n\tdefer ts.Close()\n\n\tc := dcnl().SetDigestAuth(conf.username, conf.password)\n\n\tresp, err := c.R().\n\t\tSetResult(&AuthSuccess{}).\n\t\tSetHeader(hdrContentTypeKey, \"application/json\").\n\t\tSetBody(map[string]any{\"zip_code\": \"00000\", \"city\": \"Los Angeles\"}).\n\t\tPost(ts.URL + conf.uri)\n\n\tresObj := resp.Result().(*AuthSuccess)\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, resObj.ID, \"success\")\n\tassertEqual(t, resObj.Message, \"login successful\")\n}\n\nfunc TestClientDigestAuthWithBodyQopAuthInt(t *testing.T) {\n\tconf := *defaultDigestServerConf()\n\tconf.qop = \"auth-int\"\n\tts := createDigestServer(t, &conf)\n\tdefer ts.Close()\n\n\tc := dcnl().SetDigestAuth(conf.username, conf.password)\n\n\tresp, err := c.R().\n\t\tSetResult(&AuthSuccess{}).\n\t\tSetHeader(hdrContentTypeKey, \"application/json\").\n\t\tSetBody(map[string]any{\"zip_code\": \"00000\", \"city\": \"Los Angeles\"}).\n\t\tPost(ts.URL + conf.uri)\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n}\n\nfunc TestClientDigestAuthWithBodyQopAuthIntIoCopyError(t *testing.T) {\n\tconf := *defaultDigestServerConf()\n\tconf.qop = \"auth-int\"\n\tts := createDigestServer(t, &conf)\n\tdefer ts.Close()\n\n\tc := dcnl().SetDigestAuth(conf.username, conf.password)\n\n\terrCopyMsg := \"test copy error\"\n\tioCopy = func(dst io.Writer, src io.Reader) (written int64, err error) {\n\t\treturn 0, errors.New(errCopyMsg)\n\t}\n\tt.Cleanup(func() {\n\t\tioCopy = io.Copy\n\t})\n\n\tresp, err := c.R().\n\t\tSetResult(&AuthSuccess{}).\n\t\tSetHeader(hdrContentTypeKey, \"application/json\").\n\t\tSetBody(map[string]any{\"zip_code\": \"00000\", \"city\": \"Los Angeles\"}).\n\t\tPost(ts.URL + conf.uri)\n\n\tassertNotNil(t, err)\n\tassertTrue(t, strings.Contains(err.Error(), errCopyMsg), \"expected io copy error\")\n\tassertEqual(t, 0, resp.StatusCode(), \"expected response status code to be zero on error\")\n}\n\nfunc TestClientDigestAuthRoundTripError(t *testing.T) {\n\tconf := *defaultDigestServerConf()\n\tts := createDigestServer(t, &conf)\n\tdefer ts.Close()\n\n\tc := dcnl().SetTransport(&CustomRoundTripper2{returnErr: true})\n\tc.SetDigestAuth(conf.username, conf.password)\n\n\t_, err := c.R().\n\t\tSetResult(&AuthSuccess{}).\n\t\tSetHeader(hdrContentTypeKey, \"application/json\").\n\t\tSetBody(map[string]any{\"zip_code\": \"00000\", \"city\": \"Los Angeles\"}).\n\t\tPost(ts.URL + conf.uri)\n\n\tassertNotNil(t, err)\n\tassertTrue(t, strings.Contains(err.Error(), \"test req mock error\"), \"expected round trip error\")\n}\n\nfunc TestClientDigestAuthWithBodyQopAuthIntGetBodyNil(t *testing.T) {\n\tconf := *defaultDigestServerConf()\n\tconf.qop = \"auth-int\"\n\tts := createDigestServer(t, &conf)\n\tdefer ts.Close()\n\n\tc := dcnl().SetDigestAuth(conf.username, conf.password)\n\tc.SetRequestMiddlewares(\n\t\tMiddlewareRequestCreate,\n\t\tfunc(c *Client, r *Request) error {\n\t\t\tr.RawRequest.GetBody = nil\n\t\t\treturn nil\n\t\t},\n\t)\n\n\tresp, err := c.R().\n\t\tSetResult(&AuthSuccess{}).\n\t\tSetHeader(hdrContentTypeKey, \"application/json\").\n\t\tSetBody(map[string]any{\"zip_code\": \"00000\", \"city\": \"Los Angeles\"}).\n\t\tPost(ts.URL + conf.uri)\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n}\n\nfunc TestClientDigestAuthWithGetBodyError(t *testing.T) {\n\tconf := *defaultDigestServerConf()\n\tconf.qop = \"auth-int\"\n\tts := createDigestServer(t, &conf)\n\tdefer ts.Close()\n\n\tc := dcnl().SetDigestAuth(conf.username, conf.password)\n\tc.SetRequestMiddlewares(\n\t\tMiddlewareRequestCreate,\n\t\tfunc(c *Client, r *Request) error {\n\t\t\tr.RawRequest.GetBody = func() (_ io.ReadCloser, _ error) {\n\t\t\t\treturn nil, errors.New(\"get body test error\")\n\t\t\t}\n\t\t\treturn nil\n\t\t},\n\t)\n\n\tresp, err := c.R().\n\t\tSetResult(&AuthSuccess{}).\n\t\tSetHeader(hdrContentTypeKey, \"application/json\").\n\t\tSetBody(map[string]any{\"zip_code\": \"00000\", \"city\": \"Los Angeles\"}).\n\t\tPost(ts.URL + conf.uri)\n\n\tassertNotNil(t, err)\n\tassertTrue(t, strings.Contains(err.Error(), \"resty: digest: failed to get body for auth-int: get body test error\"),\n\t\t\"expected get body error\")\n\tassertEqual(t, 0, resp.StatusCode(), \"expected response status code to be zero on error\")\n}\n\nfunc TestClientDigestAuthWithGetBodyNilReadError(t *testing.T) {\n\tconf := *defaultDigestServerConf()\n\tconf.qop = \"auth-int\"\n\tts := createDigestServer(t, &conf)\n\tdefer ts.Close()\n\n\tc := dcnl().SetDigestAuth(conf.username, conf.password)\n\tc.SetRequestMiddlewares(\n\t\tMiddlewareRequestCreate,\n\t\tfunc(c *Client, r *Request) error {\n\t\t\tr.RawRequest.GetBody = nil\n\t\t\treturn nil\n\t\t},\n\t)\n\n\tresp, err := c.R().\n\t\tSetResult(&AuthSuccess{}).\n\t\tSetHeader(hdrContentTypeKey, \"application/json\").\n\t\tSetBody(&brokenReadCloser{}).\n\t\tPost(ts.URL + conf.uri)\n\n\tassertNotNil(t, err)\n\tassertTrue(t, strings.Contains(err.Error(), \"resty: digest: failed to prepare body for auth-int: read error\"),\n\t\t\"expected read error\")\n\tassertEqual(t, 0, resp.StatusCode(), \"expected response status code to be zero on error\")\n}\n\nfunc TestClientDigestAuthWithNoBodyQopAuthInt(t *testing.T) {\n\tconf := *defaultDigestServerConf()\n\tconf.qop = \"auth-int\"\n\tts := createDigestServer(t, &conf)\n\tdefer ts.Close()\n\n\tc := dcnl().SetDigestAuth(conf.username, conf.password)\n\n\tresp, err := c.R().Get(ts.URL + conf.uri)\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n}\n\nfunc TestClientDigestAuthNoQop(t *testing.T) {\n\tconf := *defaultDigestServerConf()\n\tconf.qop = \"\"\n\n\tts := createDigestServer(t, &conf)\n\tdefer ts.Close()\n\n\tc := dcnl().SetDigestAuth(conf.username, conf.password)\n\n\tresp, err := c.R().\n\t\tSetResult(&AuthSuccess{}).\n\t\tSetHeader(hdrContentTypeKey, \"application/json\").\n\t\tSetBody(map[string]any{\"zip_code\": \"00000\", \"city\": \"Los Angeles\"}).\n\t\tPost(ts.URL + conf.uri)\n\n\tassertNil(t, err)\n\tassertEqual(t, \"200 OK\", resp.Status())\n}\n\nfunc TestClientDigestAuthWithIncorrectNcValue(t *testing.T) {\n\tconf := *defaultDigestServerConf()\n\tconf.nc = \"1234567890\"\n\n\tts := createDigestServer(t, &conf)\n\tdefer ts.Close()\n\n\tc := dcnl().SetDigestAuth(conf.username, conf.password)\n\n\tresp, err := c.R().\n\t\tSetResult(&AuthSuccess{}).\n\t\tSetHeader(hdrContentTypeKey, \"application/json\").\n\t\tSetBody(map[string]any{\"zip_code\": \"00000\", \"city\": \"Los Angeles\"}).\n\t\tPost(ts.URL + conf.uri)\n\n\tassertNotNil(t, err)\n\tassertTrue(t, strings.Contains(err.Error(), `parsing \"1234567890\": value out of range`),\n\t\t\"expected nc value out of range error\")\n\tassertEqual(t, \"\", resp.Status(), \"expected empty response status on error\")\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module resty.dev/v3\n\ngo 1.23.0\n\nrequire golang.org/x/net v0.43.0\n"
  },
  {
    "path": "go.sum",
    "content": "golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=\ngolang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=\n"
  },
  {
    "path": "hedging.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// 2025 Ahmet Demir (https://github.com/ahmet2mir)\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\n// This hedging implementation draws inspiration from the reference provided here: https://github.com/cristalhq/hedgedhttp.\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n)\n\n// NewHedging creates a new Hedging instance with default configuration.\n// By default values are:\n//   - 50ms delay between requests\n//   - Maximum 3 requests\n//   - Maximum 3 requests per second\n//   - Only read-only methods are hedged\n//\n// You can customize these settings using the corresponding setter methods.\n// For example:\n//\n//\thedging := NewHedging().\n//\t\tSetDelay(100 * time.Millisecond).\n//\t\tSetMaxRequest(5).\n//\t\tSetMaxRequestPerSecond(10)\n//\n//\t// Assign the hedging instance to the Resty client\n//\tclient := resty.New().\n//\t\tSetHedging(hedging)\n//\n//\tdefer c.Close()\nfunc NewHedging() *Hedging {\n\th := &Hedging{\n\t\tlock:                 new(sync.RWMutex),\n\t\tdelay:                50 * time.Millisecond, // delay between requests\n\t\tmaxRequest:           3,                     // max requests\n\t\tmaxRequestPerSecond:  3,                     // max requests per second\n\t\tisNonReadOnlyAllowed: false,                 // only hedge read-only methods by default\n\t}\n\th.calculateRateDelay()\n\treturn h\n}\n\n// Hedging struct implements the http.RoundTripper interface to perform hedged HTTP requests.\n// It sends multiple requests in parallel with a specified delay and returns the first successful\n// response. Hedging is particularly useful for improving latency and reliability in scenarios\n// where requests may occasionally fail or experience high latency.\n//\n// By default only read-only HTTP methods (GET, HEAD, OPTIONS, TRACE) are hedged to avoid unintended\n// side effects on the server. Unless SetHedgingAllowNonReadOnly is used to allow non-read-only methods,\n// in which case all HTTP methods will be hedged.\n//\n// NOTE:\n//   - Hedging should be used with caution, especially for non-read-only methods, as it can lead to\n//     unintended consequences if multiple requests are processed by the server.\n//   - Ensure that the server can safely handle multiple concurrent requests when using hedging,\n//     as otherwise, hedging requests can overwhelm the server.\n//\n// For more information on hedging and its use cases, refer to the following resources:\n//   - [The Tail at Scale]\n//\n// [The Tail at Scale]: https://research.google/pubs/the-tail-at-scale/\ntype Hedging struct {\n\tlock                 *sync.RWMutex\n\ttransport            http.RoundTripper\n\tdelay                time.Duration\n\tmaxRequest           int\n\tmaxRequestPerSecond  float64\n\trateDelay            time.Duration // delay between requests based on maxPerSecond\n\tisNonReadOnlyAllowed bool\n}\n\n// Delay method returns the configured hedging delay.\nfunc (h *Hedging) Delay() time.Duration {\n\th.lock.RLock()\n\tdefer h.lock.RUnlock()\n\treturn h.delay\n}\n\n// SetDelay method sets the delay between hedged requests.\nfunc (h *Hedging) SetDelay(delay time.Duration) *Hedging {\n\th.lock.Lock()\n\tdefer h.lock.Unlock()\n\th.delay = delay\n\treturn h\n}\n\n// MaxRequest method returns the maximum concurrent requests.\nfunc (h *Hedging) MaxRequest() int {\n\th.lock.RLock()\n\tdefer h.lock.RUnlock()\n\treturn h.maxRequest\n}\n\n// SetMaxRequest method sets maximum concurrent hedged requests.\nfunc (h *Hedging) SetMaxRequest(count int) *Hedging {\n\th.lock.Lock()\n\tdefer h.lock.Unlock()\n\th.maxRequest = count\n\treturn h\n}\n\n// MaxRequestPerSecond method returns the hedging rate limit.\nfunc (h *Hedging) MaxRequestPerSecond() float64 {\n\th.lock.RLock()\n\tdefer h.lock.RUnlock()\n\treturn h.maxRequestPerSecond\n}\n\n// SetMaxRequestPerSecond method sets rate limit for hedged requests.\nfunc (h *Hedging) SetMaxRequestPerSecond(count float64) *Hedging {\n\th.lock.Lock()\n\tdefer h.lock.Unlock()\n\th.maxRequestPerSecond = count\n\th.calculateRateDelay()\n\treturn h\n}\n\n// IsNonReadOnlyAllowed method returns true if hedging is enabled for non-read-only\n// HTTP methods.\nfunc (h *Hedging) IsNonReadOnlyAllowed() bool {\n\th.lock.RLock()\n\tdefer h.lock.RUnlock()\n\treturn h.isNonReadOnlyAllowed\n}\n\n// SetNonReadOnlyAllowed method allows hedging for non-read-only HTTP methods.\n// By default, only read-only methods (GET, HEAD, OPTIONS, TRACE) are hedged.\n//\n// NOTE:\n//   - Use this with caution as hedging write operations can lead to duplicates.\nfunc (h *Hedging) SetNonReadOnlyAllowed(allow bool) *Hedging {\n\th.lock.Lock()\n\tdefer h.lock.Unlock()\n\th.isNonReadOnlyAllowed = allow\n\treturn h\n}\n\n// calculateRateDelay method calculates the delay between requests based on the maxPerSecond setting.\n// If maxPerSecond is greater than 0, it sets rateDelay to 1 second divided by maxPerSecond.\n// Otherwise, it sets rateDelay to 0 (no delay).\n//\n// NOTE: It should be called within lock region.\nfunc (h *Hedging) calculateRateDelay() {\n\tif h.maxRequestPerSecond > 0 {\n\t\t// Calculate rate delay: if maxPerSecond is 10, delay is 100ms (1s / 10)\n\t\th.rateDelay = time.Duration(float64(time.Second) / h.maxRequestPerSecond)\n\t} else {\n\t\th.rateDelay = 0 // no delay if maxPerSecond is 0 or negative\n\t}\n}\n\nfunc (ht *Hedging) RoundTrip(req *http.Request) (*http.Response, error) {\n\tif !ht.isNonReadOnlyAllowed && !isReadOnlyMethod(req.Method) {\n\t\treturn ht.transport.RoundTrip(req)\n\t}\n\n\tif ht.MaxRequest() <= 1 {\n\t\treturn ht.transport.RoundTrip(req)\n\t}\n\n\tctx := req.Context()\n\tdeadline, hasDeadline := ctx.Deadline()\n\n\t// Derive hedgeCtx from the original request context to respect cancellations\n\tvar (\n\t\thedgeCtx context.Context\n\t\tcancel   context.CancelFunc\n\t)\n\tif hasDeadline {\n\t\t// Use original deadline for the race (first to complete wins)\n\t\tremaining := time.Until(deadline)\n\t\tif remaining > 0 {\n\t\t\thedgeCtx, cancel = context.WithTimeout(ctx, remaining)\n\t\t} else {\n\t\t\t// Deadline already expired, use context with cancel\n\t\t\thedgeCtx, cancel = context.WithCancel(ctx)\n\t\t}\n\t} else {\n\t\t// No deadline in original context, create cancellable context from it\n\t\thedgeCtx, cancel = context.WithCancel(ctx)\n\t}\n\n\t// defer cancel() ensures cleanup on all paths (timeout, cancellation, or normal return)\n\t// cancel() may also be called inside once.Do() when a request wins, but calling it\n\t// multiple times is safe and ensures the context is canceled as soon as any goroutine completes\n\tdefer cancel()\n\n\ttype result struct {\n\t\tresp *http.Response\n\t\terr  error\n\t}\n\n\tht.lock.RLock()\n\tmaxReq := ht.maxRequest\n\tdelay := ht.delay\n\trateDelay := ht.rateDelay\n\tht.lock.RUnlock()\n\n\tresultCh := make(chan result, maxReq)\n\tvar once sync.Once\n\n\tfor i := range maxReq {\n\t\tif i > 0 {\n\t\t\tif delay > 0 {\n\t\t\t\tselect {\n\t\t\t\tcase <-time.After(delay):\n\t\t\t\tcase <-hedgeCtx.Done():\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Rate limiting: add delay between requests based on maxPerSecond\n\t\t\t// to prevent overwhelming the server.\n\t\t\tif rateDelay > 0 {\n\t\t\t\tselect {\n\t\t\t\tcase <-time.After(rateDelay):\n\t\t\t\tcase <-hedgeCtx.Done():\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tgo func() {\n\t\t\thedgedReq := req.Clone(hedgeCtx)\n\t\t\tresp, err := ht.transport.RoundTrip(hedgedReq)\n\n\t\t\twon := false\n\t\t\tonce.Do(func() {\n\t\t\t\twon = true\n\t\t\t\tresultCh <- result{resp: resp, err: err}\n\n\t\t\t\t// Cancel inside once.Do() to stop other goroutines immediately when a request wins\n\t\t\t\t// defer cancel() ensures cleanup even if no request completes successfully\n\t\t\t\tcancel()\n\t\t\t})\n\n\t\t\tif !won && resp != nil && resp.Body != nil {\n\t\t\t\tdrainReadCloser(resp.Body)\n\t\t\t}\n\t\t}()\n\t}\n\n\tres := <-resultCh\n\tclose(resultCh)\n\treturn res.resp, res.err\n}\n\n// isReadOnlyMethod verifies if the HTTP method is read-only (safe for hedging)\nfunc isReadOnlyMethod(method string) bool {\n\tswitch method {\n\tcase MethodGet, MethodHead, MethodOptions, MethodTrace:\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n"
  },
  {
    "path": "hedging_test.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc createHedgingTestServer(t *testing.T, attemptCount *int32) *httptest.Server {\n\ttimeouts := [5]time.Duration{800 * time.Millisecond, 400 * time.Millisecond, 10 * time.Millisecond, 5 * time.Millisecond, 1 * time.Millisecond}\n\treturn createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tattempt := atomic.AddInt32(attemptCount, 1)\n\t\ttime.Sleep(timeouts[attempt-1])\n\t\tw.Header().Set(\"X-Attempt\", fmt.Sprintf(\"%d\", attempt))\n\t\t_, _ = fmt.Fprintf(w, \"Attempt %d\", attempt)\n\t})\n}\n\nfunc TestHedgingBasic(t *testing.T) {\n\tvar attemptCount int32\n\tts := createHedgingTestServer(t, &attemptCount)\n\tdefer ts.Close()\n\n\tconst maxRequests = 3\n\th := NewHedging().\n\t\tSetDelay(10 * time.Millisecond).\n\t\tSetMaxRequest(3).\n\t\tSetMaxRequestPerSecond(0)\n\n\tc := dcnl().SetHedging(h)\n\n\tresp, err := c.R().Get(ts.URL)\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, int32(maxRequests), atomic.LoadInt32(&attemptCount), \"total attempts should match max requests\")\n}\n\nfunc TestHedgingSecondWins(t *testing.T) {\n\tvar attemptCount int32\n\twinnerAttempt := atomic.Int32{}\n\ttimeouts := [2]time.Duration{400 * time.Millisecond, 20 * time.Millisecond}\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tattempt := atomic.AddInt32(&attemptCount, 1)\n\t\ttime.Sleep(timeouts[attempt-1])\n\t\twinnerAttempt.CompareAndSwap(0, attempt)\n\n\t\tw.Header().Set(\"X-Attempt\", fmt.Sprintf(\"%d\", attempt))\n\t\tw.WriteHeader(http.StatusOK)\n\t\tfmt.Fprintf(w, \"Attempt %d\", attempt)\n\t})\n\tdefer ts.Close()\n\n\th := NewHedging().\n\t\tSetDelay(10 * time.Millisecond).\n\t\tSetMaxRequest(2).\n\t\tSetMaxRequestPerSecond(0)\n\n\tc := dcnl().SetHedging(h)\n\n\tresp, err := c.R().Get(ts.URL)\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\twinnerRequest := winnerAttempt.Load()\n\tassertEqual(t, fmt.Sprintf(\"Attempt %d\", winnerRequest), resp.String(), \"expected second attempt to win\")\n\tassertEqual(t, int32(2), winnerRequest, \"expected second request to win\")\n\tassertEqual(t, int32(2), atomic.LoadInt32(&attemptCount), \"total attempts should be 2\")\n}\n\nfunc TestHedgingTimeout(t *testing.T) {\n\tvar attemptCount int32\n\trequestTimes := make([]time.Time, 0, 3)\n\tvar timesLock atomic.Value\n\ttimesLock.Store(requestTimes)\n\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tattempt := atomic.AddInt32(&attemptCount, 1)\n\t\tnow := time.Now()\n\n\t\ttimes := timesLock.Load().([]time.Time)\n\t\ttimes = append(times, now)\n\t\ttimesLock.Store(times)\n\n\t\tif attempt == 1 {\n\t\t\ttime.Sleep(300 * time.Millisecond)\n\t\t}\n\n\t\tw.WriteHeader(http.StatusOK)\n\t\tfmt.Fprintf(w, \"Attempt %d\", attempt)\n\t})\n\tdefer ts.Close()\n\n\tdelay := 50 * time.Millisecond\n\th := NewHedging().\n\t\tSetDelay(delay).\n\t\tSetMaxRequest(3).\n\t\tSetMaxRequestPerSecond(0)\n\n\tc := dcnl().SetHedging(h)\n\n\tresp, err := c.R().Get(ts.URL)\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\ttime.Sleep(200 * time.Millisecond)\n\n\ttimes := timesLock.Load().([]time.Time)\n\tif len(times) >= 2 {\n\t\tdiff := times[1].Sub(times[0])\n\t\tif diff < delay || diff > delay+30*time.Millisecond {\n\t\t\tt.Logf(\"Expected delay between requests to be ~%v, got %v\", delay, diff)\n\t\t}\n\t}\n}\n\nfunc TestHedgingReadOnlyMethodsOnly(t *testing.T) {\n\tvar attemptCount int32\n\tts := createHedgingTestServer(t, &attemptCount)\n\tdefer ts.Close()\n\n\th := NewHedging().\n\t\tSetDelay(10 * time.Millisecond).\n\t\tSetMaxRequest(3).\n\t\tSetMaxRequestPerSecond(0)\n\n\tc := dcnl().SetHedging(h)\n\n\ttestCases := []struct {\n\t\tmethod        string\n\t\texpectHedging bool\n\t\trequestFunc   func(*Client, string) (*Response, error)\n\t}{\n\t\t{MethodGet, true, func(c *Client, url string) (*Response, error) { return c.R().Get(url) }},\n\t\t{MethodHead, true, func(c *Client, url string) (*Response, error) { return c.R().Head(url) }},\n\t\t{MethodOptions, true, func(c *Client, url string) (*Response, error) { return c.R().Options(url) }},\n\t\t{MethodPost, false, func(c *Client, url string) (*Response, error) { return c.R().Post(url) }},\n\t\t{MethodPut, false, func(c *Client, url string) (*Response, error) { return c.R().Put(url) }},\n\t\t{MethodPatch, false, func(c *Client, url string) (*Response, error) { return c.R().Patch(url) }},\n\t\t{MethodDelete, false, func(c *Client, url string) (*Response, error) { return c.R().Delete(url) }},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.method, func(t *testing.T) {\n\t\t\tatomic.StoreInt32(&attemptCount, 0)\n\n\t\t\tresp, err := tc.requestFunc(c, ts.URL)\n\t\t\tassertError(t, err)\n\t\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\t\t\ttime.Sleep(20 * time.Millisecond)\n\n\t\t\tcount := atomic.LoadInt32(&attemptCount)\n\t\t\tif tc.expectHedging {\n\t\t\t\tassertNotEqual(t, 1, count, fmt.Sprintf(\"%s: expected hedging with multiple requests, got %d request(s)\", tc.method, count))\n\t\t\t} else {\n\t\t\t\tassertEqual(t, int32(1), count, fmt.Sprintf(\"%s: no hedging 1 request only\", tc.method))\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestHedgingRateLimit(t *testing.T) {\n\tvar attemptCount int32\n\tts := createHedgingTestServer(t, &attemptCount)\n\tdefer ts.Close()\n\n\th := NewHedging().\n\t\tSetDelay(10 * time.Millisecond).\n\t\tSetMaxRequest(10).\n\t\tSetMaxRequestPerSecond(5.0)\n\n\tc := dcnl().SetHedging(h)\n\n\tstart := time.Now()\n\tresp, err := c.R().Get(ts.URL)\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\tduration := time.Since(start)\n\n\tif duration < 200*time.Millisecond {\n\t\tt.Logf(\"Rate limiting may have limited hedged requests. Duration: %v, Attempts: %d\", duration, atomic.LoadInt32(&attemptCount))\n\t}\n}\n\nfunc TestHedgingWithRetryFallback(t *testing.T) {\n\tc := dcnl()\n\n\t// Set retry first\n\tc.SetRetryCount(2)\n\tassertEqual(t, 2, c.RetryCount())\n\n\th := NewHedging().\n\t\tSetDelay(50 * time.Millisecond).\n\t\tSetMaxRequest(3).\n\t\tSetMaxRequestPerSecond(0)\n\n\t// Enable hedging should disable retry by default\n\tc.SetHedging(h)\n\tassertEqual(t, 0, c.RetryCount())\n\n\t// But user can re-enable retry as fallback\n\tc.SetRetryCount(1)\n\tassertEqual(t, 1, c.RetryCount())\n\tassertEqual(t, true, c.isHedgingEnabled())\n\n\t// Disable hedging\n\tc.SetHedging(nil)\n\tassertEqual(t, false, c.isHedgingEnabled())\n\tassertEqual(t, 1, c.RetryCount()) // Retry count should remain\n}\n\nfunc TestHedgingDisable(t *testing.T) {\n\tvar attemptCount int32\n\tts := createHedgingTestServer(t, &attemptCount)\n\tdefer ts.Close()\n\n\th := NewHedging().\n\t\tSetDelay(10 * time.Millisecond).\n\t\tSetMaxRequest(3).\n\t\tSetMaxRequestPerSecond(0)\n\n\tc := dcnl()\n\tc.SetHedging(h)\n\tassertEqual(t, true, c.isHedgingEnabled())\n\n\tc.SetHedging(nil)\n\tassertEqual(t, false, c.isHedgingEnabled())\n\n\tatomic.StoreInt32(&attemptCount, 0)\n\tresp, err := c.R().Get(ts.URL)\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\ttime.Sleep(50 * time.Millisecond)\n\n\tassertEqual(t, int32(1), atomic.LoadInt32(&attemptCount))\n}\n\nfunc TestHedgingContextCancellation(t *testing.T) {\n\tattemptCount := atomic.Int32{}\n\tstartedCount := atomic.Int32{}\n\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tstartedCount.Add(1)\n\t\ttime.Sleep(200 * time.Millisecond)\n\t\tattemptCount.Add(1)\n\t\tw.WriteHeader(http.StatusOK)\n\t})\n\tdefer ts.Close()\n\n\th := NewHedging().\n\t\tSetDelay(10 * time.Millisecond).\n\t\tSetMaxRequest(3).\n\t\tSetMaxRequestPerSecond(0)\n\n\tc := dcnl().SetHedging(h)\n\n\tctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond)\n\tdefer cancel()\n\n\t_, err := c.R().SetContext(ctx).Get(ts.URL)\n\tassertErrorIs(t, context.DeadlineExceeded, err)\n\n\ttime.Sleep(50 * time.Millisecond)\n\n\tstarted := startedCount.Load()\n\tcompleted := attemptCount.Load()\n\tassertTrue(t, started > 1, \"expected multiple hedged request to start\")\n\tassertEqual(t, int32(0), completed, \"context cancellation should have prevented completion\")\n}\n\nfunc TestHedgingConfiguration(t *testing.T) {\n\th := NewHedging().\n\t\tSetDelay(50 * time.Millisecond).\n\t\tSetMaxRequest(3).\n\t\tSetMaxRequestPerSecond(10.0)\n\tassertEqual(t, 50*time.Millisecond, h.Delay())\n\tassertEqual(t, 3, h.MaxRequest())\n\tassertEqual(t, 10.0, h.MaxRequestPerSecond())\n\n\t// Now we can update individual settings\n\th.SetDelay(100 * time.Millisecond)\n\tassertEqual(t, 100*time.Millisecond, h.Delay())\n\n\th.SetMaxRequest(5)\n\tassertEqual(t, 5, h.MaxRequest())\n\n\th.SetMaxRequestPerSecond(20.0)\n\tassertEqual(t, 20.0, h.MaxRequestPerSecond())\n}\n\nfunc TestHedgingConfigurationViaClient(t *testing.T) {\n\tc := dcnl()\n\n\t// Setters require hedging to be enabled first\n\tassertEqual(t, false, c.isHedgingEnabled())\n\n\th := NewHedging().\n\t\tSetDelay(50 * time.Millisecond).\n\t\tSetMaxRequest(3).\n\t\tSetMaxRequestPerSecond(10.0)\n\tc.SetHedging(h)\n\n\tassertEqual(t, true, c.isHedgingEnabled())\n\tassertEqual(t, 50*time.Millisecond, c.Hedging().Delay())\n\tassertEqual(t, 3, c.Hedging().MaxRequest())\n\tassertEqual(t, 10.0, c.Hedging().MaxRequestPerSecond())\n\n\t// Now we can update individual settings\n\tc.Hedging().SetDelay(100 * time.Millisecond)\n\tassertEqual(t, 100*time.Millisecond, c.Hedging().Delay())\n\n\tc.Hedging().SetMaxRequest(5)\n\tassertEqual(t, 5, c.Hedging().MaxRequest())\n\n\tc.Hedging().SetMaxRequestPerSecond(20.0)\n\tassertEqual(t, 20.0, c.Hedging().MaxRequestPerSecond())\n}\n\nfunc TestHedgingWithCustomTransport(t *testing.T) {\n\tvar attemptCount int32\n\tts := createHedgingTestServer(t, &attemptCount)\n\tdefer ts.Close()\n\n\tcustomTransport := &http.Transport{}\n\tc := NewWithClient(&http.Client{Transport: customTransport})\n\n\th := NewHedging().\n\t\tSetDelay(10 * time.Millisecond).\n\t\tSetMaxRequest(3).\n\t\tSetMaxRequestPerSecond(0)\n\tc.SetHedging(h)\n\n\tresp, err := c.R().Get(ts.URL)\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, int32(3), atomic.LoadInt32(&attemptCount), \"Expected 3 attempts with hedging enabled\")\n\n\t// disable hedging and verify transport is unwrapped\n\tc.SetHedging(nil)\n\t_, ok := c.httpClient.Transport.(*Hedging)\n\tassertFalse(t, ok, \"transport should be unwrapped after disabling hedging\")\n}\n\nfunc TestHedgingSingleRequest(t *testing.T) {\n\tvar attemptCount int32\n\tts := createHedgingTestServer(t, &attemptCount)\n\tdefer ts.Close()\n\n\th := NewHedging().\n\t\tSetDelay(10 * time.Millisecond).\n\t\tSetMaxRequest(1).\n\t\tSetMaxRequestPerSecond(0)\n\n\tc := dcnl().SetHedging(h)\n\n\tresp, err := c.R().Get(ts.URL)\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, int32(1), atomic.LoadInt32(&attemptCount))\n}\n\nfunc TestHedgingAllowNonReadOnly(t *testing.T) {\n\tvar attemptCount int32\n\tts := createHedgingTestServer(t, &attemptCount)\n\tdefer ts.Close()\n\n\th := NewHedging().\n\t\tSetDelay(10 * time.Millisecond).\n\t\tSetMaxRequest(3).\n\t\tSetMaxRequestPerSecond(0)\n\n\tc := dcnl().SetHedging(h)\n\n\t// By default, non-read-only methods should not be hedged\n\tassertEqual(t, false, c.Hedging().IsNonReadOnlyAllowed())\n\n\t// Test POST without allowing non-read-only\n\tatomic.StoreInt32(&attemptCount, 0)\n\tresp, err := c.R().Post(ts.URL)\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, int32(1), atomic.LoadInt32(&attemptCount), \"no hedging for POST without allow flag\")\n\n\t// Enable non-read-only methods\n\tc.Hedging().SetNonReadOnlyAllowed(true)\n\tassertEqual(t, true, c.Hedging().IsNonReadOnlyAllowed())\n\n\t// Test POST with allowing non-read-only\n\tatomic.StoreInt32(&attemptCount, 0)\n\tresp, err = c.R().Post(ts.URL)\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, int32(3), atomic.LoadInt32(&attemptCount), \"hedging for POST with allow flag\")\n}\n\nfunc TestHedgingWithNilTransport(t *testing.T) {\n\tvar attemptCount int32\n\tts := createHedgingTestServer(t, &attemptCount)\n\tdefer ts.Close()\n\n\t// Create client with nil transport\n\tc := NewWithClient(&http.Client{Transport: nil})\n\n\th := NewHedging().\n\t\tSetDelay(10 * time.Millisecond).\n\t\tSetMaxRequest(3).\n\t\tSetMaxRequestPerSecond(0)\n\tc.SetHedging(h)\n\n\tresp, err := c.R().Get(ts.URL)\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, int32(3), atomic.LoadInt32(&attemptCount), \"hedging with nil transport should still work\")\n}\n\nfunc TestHedgingEnableMultipleTimes(t *testing.T) {\n\tvar attemptCount int32\n\tts := createHedgingTestServer(t, &attemptCount)\n\tdefer ts.Close()\n\n\th := NewHedging().\n\t\tSetDelay(10 * time.Millisecond).\n\t\tSetMaxRequest(3).\n\t\tSetMaxRequestPerSecond(0)\n\n\tc := dcnl()\n\n\t// Enable hedging first time\n\tc.SetHedging(h)\n\tassertEqual(t, true, c.isHedgingEnabled())\n\n\t// Enable hedging again without disabling - should handle already wrapped transport\n\tnh := NewHedging().\n\t\tSetDelay(30 * time.Millisecond).\n\t\tSetMaxRequest(5).\n\t\tSetMaxRequestPerSecond(10.0)\n\tc.SetHedging(nh)\n\tassertEqual(t, true, c.isHedgingEnabled())\n\tassertEqual(t, 30*time.Millisecond, c.Hedging().Delay())\n\tassertEqual(t, 5, c.Hedging().MaxRequest())\n\tassertEqual(t, 10.0, c.Hedging().MaxRequestPerSecond())\n\n\t// Verify hedging still works\n\tresp, err := c.R().Get(ts.URL)\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, int32(3), atomic.LoadInt32(&attemptCount), \"expected hedging after re-enabling\")\n}\n\nfunc TestHedgingWrapWithDisabledHedging(t *testing.T) {\n\tc := dcnl()\n\n\th := NewHedging().\n\t\tSetDelay(20 * time.Millisecond).\n\t\tSetMaxRequest(3).\n\t\tSetMaxRequestPerSecond(0)\n\t// Enable and then disable hedging\n\tc.SetHedging(h)\n\tassertEqual(t, true, c.isHedgingEnabled())\n\n\tc.SetHedging(nil)\n\tassertEqual(t, false, c.isHedgingEnabled())\n\n\t// Verify transport is not a hedgingTransport\n\t_, ok := c.httpClient.Transport.(*Hedging)\n\tassertFalse(t, ok, \"transport should not be hedging transport\")\n}\n\nfunc TestHedgingRateDelayBetweenRequests(t *testing.T) {\n\trequestTimes := make([]time.Time, 0, 3)\n\tvar mu sync.Mutex\n\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tmu.Lock()\n\t\trequestTimes = append(requestTimes, time.Now())\n\t\tmu.Unlock()\n\n\t\t// Slow response to ensure multiple hedged requests are sent\n\t\ttime.Sleep(500 * time.Millisecond)\n\t\tw.WriteHeader(http.StatusOK)\n\t})\n\tdefer ts.Close()\n\n\tc := dcnl()\n\t// delay=10ms, maxRequest=3, maxRequestPerSecond=5.0 (rateDelay = 200ms)\n\t// Expected timing: req1 at 0, req2 at ~10ms + 200ms = ~210ms, req3 at ~420ms\n\th := NewHedging().\n\t\tSetDelay(10 * time.Millisecond).\n\t\tSetMaxRequest(3).\n\t\tSetMaxRequestPerSecond(5.0)\n\tc.SetHedging(h)\n\n\t_, err := c.R().Get(ts.URL)\n\tassertError(t, err)\n\n\t// Wait for all requests to be recorded\n\ttime.Sleep(600 * time.Millisecond)\n\n\tmu.Lock()\n\ttimes := make([]time.Time, len(requestTimes))\n\tcopy(times, requestTimes)\n\tmu.Unlock()\n\n\tassertEqual(t, 3, len(times), \"expected 3 hedged requests to be sent\")\n\n\t// Verify rate delay was applied between requests\n\t// With maxPerSecond=5.0, rateDelay should be 200ms\n\t// The gap between requests should be at least rateDelay (200ms)\n\texpectedRateDelay := 200 * time.Millisecond\n\ttolerance := 50 * time.Millisecond\n\n\tfor i := 1; i < len(times); i++ {\n\t\tgap := times[i].Sub(times[i-1])\n\t\t// Gap should be >= (delay + rateDelay) - tolerance\n\t\tminExpectedGap := expectedRateDelay - tolerance\n\t\tif gap < minExpectedGap {\n\t\t\tt.Errorf(\"Gap between request %d and %d was %v, expected at least %v (rate delay should be ~%v)\",\n\t\t\t\ti-1, i, gap, minExpectedGap, expectedRateDelay)\n\t\t}\n\t}\n}\n\nfunc TestHedgingNoDoubleWrap(t *testing.T) {\n\th1 := NewHedging().SetDelay(50 * time.Millisecond)\n\th2 := NewHedging().SetDelay(100 * time.Millisecond)\n\n\tc := dcnl()\n\n\t// Enable hedging first time\n\tc.SetHedging(h1)\n\t_, ok := c.httpClient.Transport.(*Hedging)\n\tassertTrue(t, ok, \"Hedging transport\")\n\n\t// Enable different hedging without disabling first\n\tc.SetHedging(h2)\n\n\t// Both should be Hedging\n\thedging2, ok := c.httpClient.Transport.(*Hedging)\n\tassertTrue(t, ok, \"Hedging transport\")\n\n\t// The wrapped transport should NOT be another Hedging\n\t_, isHedging := hedging2.transport.(*Hedging)\n\tassertFalse(t, isHedging, \"Double-wrapped hedging detected - transport should be unwrapped\")\n\n\t// Verify transport chain depth, should only have one Hedging layer\n\tif hedging, ok := c.httpClient.Transport.(*Hedging); ok {\n\t\t_, isHedging := hedging.transport.(*Hedging)\n\t\tassertFalse(t, isHedging, \"Double-wrapped hedging detected\")\n\t}\n\n\t// Verify the configuration is the new one\n\tassertEqual(t, hedging2.Delay(), 100*time.Millisecond, \"Expected 100ms delay\")\n}\n\nfunc TestHedgingRoundTripDeadlineExpired(t *testing.T) {\n\tvar attemptCount int32\n\tts := createHedgingTestServer(t, &attemptCount)\n\tdefer ts.Close()\n\n\th := NewHedging().\n\t\tSetDelay(10 * time.Millisecond).\n\t\tSetMaxRequest(3).\n\t\tSetMaxRequestPerSecond(0)\n\n\tc := dcnl().SetHedging(h)\n\n\tctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(-1*time.Millisecond))\n\tdefer cancel()\n\n\t_, err := c.R().SetContext(ctx).Get(ts.URL)\n\tassertErrorIs(t, context.DeadlineExceeded, err, \"Expected context deadline expired error\")\n\n\ttime.Sleep(50 * time.Millisecond)\n\tassertEqual(t, int32(0), atomic.LoadInt32(&attemptCount))\n}\n"
  },
  {
    "path": "load_balancer.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\n// ErrNoBaseURLs error returned when no base URLs are found\nvar ErrNoBaseURLs = errors.New(\"resty: no base URLs found\")\n\n// LoadBalancer is the interface that wraps the HTTP client load-balancing\n// algorithm that returns the \"Next\" Base URL for the request to target\ntype LoadBalancer interface {\n\tNextWithContext(ctx context.Context) (string, error)\n\tFeedback(*RequestFeedback)\n\tClose() error\n}\n\n// RequestFeedback struct is used to send the request feedback to load balancing\n// algorithm\ntype RequestFeedback struct {\n\tBaseURL string\n\tSuccess bool\n\tAttempt int\n}\n\n// NewRoundRobin method creates the new Round-Robin(RR) request load balancer\n// instance with given base URLs\nfunc NewRoundRobin(baseURLs ...string) (*RoundRobin, error) {\n\tif len(baseURLs) == 0 {\n\t\treturn nil, ErrNoBaseURLs\n\t}\n\n\trr := &RoundRobin{lock: new(sync.Mutex)}\n\tif err := rr.Refresh(baseURLs...); err != nil {\n\t\treturn rr, err\n\t}\n\treturn rr, nil\n}\n\nvar _ LoadBalancer = (*RoundRobin)(nil)\n\n// RoundRobin struct used to implement the Round-Robin(RR) request\n// load balancer algorithm\ntype RoundRobin struct {\n\tlock     *sync.Mutex\n\tbaseURLs []string\n\tcurrent  int\n}\n\n// NextWithContext method returns the next Base URL based on the Round-Robin(RR) algorithm\n// with context support for cancellation\nfunc (rr *RoundRobin) NextWithContext(ctx context.Context) (string, error) {\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn \"\", ctx.Err()\n\tdefault:\n\t}\n\n\trr.lock.Lock()\n\tdefer rr.lock.Unlock()\n\n\tif len(rr.baseURLs) == 0 {\n\t\treturn \"\", ErrNoBaseURLs\n\t}\n\n\tbaseURL := rr.baseURLs[rr.current]\n\trr.current = (rr.current + 1) % len(rr.baseURLs)\n\treturn baseURL, nil\n}\n\n// Feedback method does nothing in Round-Robin(RR) request load balancer\nfunc (rr *RoundRobin) Feedback(_ *RequestFeedback) {}\n\n// Close method does nothing in Round-Robin(RR) request load balancer\nfunc (rr *RoundRobin) Close() error { return nil }\n\n// Refresh method reset the existing Base URLs with the given Base URLs slice to refresh it\nfunc (rr *RoundRobin) Refresh(baseURLs ...string) error {\n\trr.lock.Lock()\n\tdefer rr.lock.Unlock()\n\tresult := make([]string, 0)\n\tfor _, u := range baseURLs {\n\t\tbaseURL, err := extractBaseURL(u)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tresult = append(result, baseURL)\n\t}\n\n\t// after processing, assign the updates\n\trr.baseURLs = result\n\treturn nil\n}\n\n// Host struct used to represent the host information and its weight\n// to load balance the requests\ntype Host struct {\n\t// BaseURL represents the targeted host base URL\n\t//\thttps://resty.dev\n\tBaseURL string\n\n\t// Weight represents the host weight to determine\n\t// the percentage of requests to send\n\tWeight int\n\n\t// MaxFailures represents the value to mark the host as\n\t// not usable until it reaches the Recovery duration\n\t//\tDefault value is 5\n\tMaxFailures int\n\n\tstate          HostState\n\tcurrentWeight  int\n\tfailedRequests int\n}\n\nfunc (h *Host) addWeight() {\n\th.currentWeight += h.Weight\n}\n\nfunc (h *Host) resetWeight(totalWeight int) {\n\th.currentWeight -= totalWeight\n}\n\ntype HostState int\n\n// Host transition states\nconst (\n\tHostStateInActive HostState = iota\n\tHostStateActive\n)\n\n// HostStateChangeFunc type provides feedback on host state transitions\ntype HostStateChangeFunc func(baseURL string, from, to HostState)\n\n// ErrNoActiveHost error returned when all hosts are inactive on the load balancer\nvar ErrNoActiveHost = errors.New(\"resty: no active host\")\n\n// NewWeightedRoundRobin method creates the new Weighted Round-Robin(WRR)\n// request load balancer instance with given recovery duration and hosts slice\nfunc NewWeightedRoundRobin(recovery time.Duration, hosts ...*Host) (*WeightedRoundRobin, error) {\n\tif recovery == 0 {\n\t\trecovery = 120 * time.Second // defaults to 120 seconds\n\t}\n\twrr := &WeightedRoundRobin{\n\t\tlock:     new(sync.RWMutex),\n\t\thosts:    make([]*Host, 0),\n\t\ttick:     time.NewTicker(recovery),\n\t\trecovery: recovery,\n\t}\n\n\terr := wrr.Refresh(hosts...)\n\n\tgo wrr.ticker()\n\n\treturn wrr, err\n}\n\nvar _ LoadBalancer = (*WeightedRoundRobin)(nil)\n\n// WeightedRoundRobin struct used to represent the host details for\n// Weighted Round-Robin(WRR) algorithm implementation\ntype WeightedRoundRobin struct {\n\tlock          *sync.RWMutex\n\thosts         []*Host\n\ttotalWeight   int\n\ttick          *time.Ticker\n\tonStateChange HostStateChangeFunc\n\n\t// Recovery duration is used to set the timer to put\n\t// the host back in the pool for the next turn and\n\t// reset the failed request count for the segment\n\trecovery time.Duration\n}\n\n// NextWithContext method returns the next Base URL based on Weighted Round-Robin(WRR)\n// with context support for cancellation\nfunc (wrr *WeightedRoundRobin) NextWithContext(ctx context.Context) (string, error) {\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn \"\", ctx.Err()\n\tdefault:\n\t}\n\n\twrr.lock.Lock()\n\tdefer wrr.lock.Unlock()\n\n\tvar best *Host\n\ttotal := 0\n\tfor _, h := range wrr.hosts {\n\t\tif h.state == HostStateInActive {\n\t\t\tcontinue\n\t\t}\n\n\t\th.addWeight()\n\t\ttotal += h.Weight\n\n\t\tif best == nil || h.currentWeight > best.currentWeight {\n\t\t\tbest = h\n\t\t}\n\t}\n\n\tif best == nil {\n\t\treturn \"\", ErrNoActiveHost\n\t}\n\n\tbest.resetWeight(total)\n\treturn best.BaseURL, nil\n}\n\n// Feedback method process the request feedback for Weighted Round-Robin(WRR)\n// request load balancer\nfunc (wrr *WeightedRoundRobin) Feedback(f *RequestFeedback) {\n\tif f == nil {\n\t\treturn\n\t}\n\n\twrr.lock.Lock()\n\tdefer wrr.lock.Unlock()\n\n\tfor _, host := range wrr.hosts {\n\t\tif host.BaseURL == f.BaseURL {\n\t\t\tif !f.Success {\n\t\t\t\thost.failedRequests++\n\t\t\t}\n\t\t\tif host.failedRequests >= host.MaxFailures {\n\t\t\t\thost.state = HostStateInActive\n\t\t\t\tif wrr.onStateChange != nil {\n\t\t\t\t\twrr.onStateChange(host.BaseURL, HostStateActive, HostStateInActive)\n\t\t\t\t}\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}\n}\n\n// Close method does the cleanup by stopping the [time.Ticker] on\n// Weighted Round-Robin(WRR) request load balancer\nfunc (wrr *WeightedRoundRobin) Close() error {\n\twrr.lock.Lock()\n\tdefer wrr.lock.Unlock()\n\twrr.tick.Stop()\n\treturn nil\n}\n\n// Refresh method reset the existing values with the given [Host] slice to refresh it\nfunc (wrr *WeightedRoundRobin) Refresh(hosts ...*Host) error {\n\tif hosts == nil {\n\t\treturn nil\n\t}\n\n\twrr.lock.Lock()\n\tdefer wrr.lock.Unlock()\n\tnewTotalWeight := 0\n\tfor _, h := range hosts {\n\t\tbaseURL, err := extractBaseURL(h.BaseURL)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\th.BaseURL = baseURL\n\t\th.state = HostStateActive\n\t\tnewTotalWeight += h.Weight\n\n\t\t// assign defaults if not provided\n\t\tif h.MaxFailures == 0 {\n\t\t\th.MaxFailures = 5 // default value is 5\n\t\t}\n\t}\n\n\t// after processing, assign the updates\n\twrr.hosts = hosts\n\twrr.totalWeight = newTotalWeight\n\treturn nil\n}\n\n// SetOnStateChange method used to set a callback for the host transition state\nfunc (wrr *WeightedRoundRobin) SetOnStateChange(fn HostStateChangeFunc) {\n\twrr.lock.Lock()\n\tdefer wrr.lock.Unlock()\n\twrr.onStateChange = fn\n}\n\n// SetRecoveryDuration method is used to change the existing recovery duration for the host\nfunc (wrr *WeightedRoundRobin) SetRecoveryDuration(d time.Duration) {\n\twrr.lock.Lock()\n\tdefer wrr.lock.Unlock()\n\twrr.recovery = d\n\twrr.tick.Reset(d)\n}\n\nfunc (wrr *WeightedRoundRobin) ticker() {\n\tfor range wrr.tick.C {\n\t\twrr.lock.Lock()\n\t\thosts := make([]*Host, len(wrr.hosts))\n\t\tcopy(hosts, wrr.hosts)\n\t\twrr.lock.Unlock()\n\n\t\tfor _, host := range hosts {\n\t\t\tif host.state == HostStateInActive {\n\t\t\t\thost.state = HostStateActive\n\t\t\t\thost.failedRequests = 0\n\n\t\t\t\tif wrr.onStateChange != nil {\n\t\t\t\t\twrr.onStateChange(host.BaseURL, HostStateInActive, HostStateActive)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n// NewSRVWeightedRoundRobin method creates a new Weighted Round-Robin(WRR) load balancer instance\n// with given SRV values\nfunc NewSRVWeightedRoundRobin(service, proto, domainName, httpScheme string) (*SRVWeightedRoundRobin, error) {\n\tif isStringEmpty(proto) {\n\t\tproto = \"tcp\"\n\t}\n\tif isStringEmpty(httpScheme) {\n\t\thttpScheme = \"https\"\n\t}\n\n\twrr, _ := NewWeightedRoundRobin(0) // with this input error will not occur\n\tswrr := &SRVWeightedRoundRobin{\n\t\tService:    service,\n\t\tProto:      proto,\n\t\tDomainName: domainName,\n\t\tHttpScheme: httpScheme,\n\t\twrr:        wrr,\n\t\ttick:       time.NewTicker(180 * time.Second), // default is 180 seconds\n\t\tlock:       new(sync.Mutex),\n\t\tlookupSRV: func() ([]*net.SRV, error) {\n\t\t\t_, addrs, err := net.LookupSRV(service, proto, domainName)\n\t\t\treturn addrs, err\n\t\t},\n\t}\n\n\terr := swrr.Refresh()\n\n\tgo swrr.ticker()\n\n\treturn swrr, err\n}\n\nvar _ LoadBalancer = (*SRVWeightedRoundRobin)(nil)\n\n// SRVWeightedRoundRobin struct used to implement SRV Weighted Round-Robin(RR) algorithm\ntype SRVWeightedRoundRobin struct {\n\tService    string\n\tProto      string\n\tDomainName string\n\tHttpScheme string\n\n\twrr       *WeightedRoundRobin\n\ttick      *time.Ticker\n\tlock      *sync.Mutex\n\tlookupSRV func() ([]*net.SRV, error)\n}\n\n// NextWithContext method returns the next SRV Base URL based on Weighted Round-Robin(RR)\n// with context support for cancellation\nfunc (swrr *SRVWeightedRoundRobin) NextWithContext(ctx context.Context) (string, error) {\n\treturn swrr.wrr.NextWithContext(ctx)\n}\n\n// Feedback method does nothing in SRV Base URL based on Weighted Round-Robin(WRR)\n// request load balancer\nfunc (swrr *SRVWeightedRoundRobin) Feedback(f *RequestFeedback) {\n\tswrr.wrr.Feedback(f)\n}\n\n// Close method does the cleanup by stopping the [time.Ticker] SRV Base URL based\n// on Weighted Round-Robin(WRR) request load balancer\nfunc (swrr *SRVWeightedRoundRobin) Close() error {\n\tswrr.lock.Lock()\n\tdefer swrr.lock.Unlock()\n\tswrr.wrr.Close()\n\tswrr.tick.Stop()\n\treturn nil\n}\n\n// Refresh method reset the values based [net.LookupSRV] values to refresh it\nfunc (swrr *SRVWeightedRoundRobin) Refresh() error {\n\tswrr.lock.Lock()\n\tdefer swrr.lock.Unlock()\n\taddrs, err := swrr.lookupSRV()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thosts := make([]*Host, len(addrs))\n\tfor idx, addr := range addrs {\n\t\tdomain := strings.TrimRight(addr.Target, \".\")\n\t\tbaseURL := fmt.Sprintf(\"%s://%s:%d\", swrr.HttpScheme, domain, addr.Port)\n\t\thosts[idx] = &Host{BaseURL: baseURL, Weight: int(addr.Weight)}\n\t}\n\n\treturn swrr.wrr.Refresh(hosts...)\n}\n\n// SetRefreshDuration method assists in changing the default (180 seconds) refresh duration\nfunc (swrr *SRVWeightedRoundRobin) SetRefreshDuration(d time.Duration) {\n\tswrr.lock.Lock()\n\tdefer swrr.lock.Unlock()\n\tswrr.tick.Reset(d)\n}\n\n// SetOnStateChange method used to set a callback for the host transition state\nfunc (swrr *SRVWeightedRoundRobin) SetOnStateChange(fn HostStateChangeFunc) {\n\tswrr.wrr.SetOnStateChange(fn)\n}\n\n// SetRecoveryDuration method is used to change the existing recovery duration for the host\nfunc (swrr *SRVWeightedRoundRobin) SetRecoveryDuration(d time.Duration) {\n\tswrr.wrr.SetRecoveryDuration(d)\n}\n\nfunc (swrr *SRVWeightedRoundRobin) ticker() {\n\tfor range swrr.tick.C {\n\t\tswrr.Refresh()\n\t}\n}\n\nfunc extractBaseURL(u string) (string, error) {\n\tbaseURL, err := url.Parse(u)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// we only require base URL LB\n\tbaseURL.Path = \"\"\n\tbaseURL.RawQuery = \"\"\n\n\treturn strings.TrimRight(baseURL.String(), \"/\"), nil\n}\n"
  },
  {
    "path": "load_balancer_test.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestRoundRobin(t *testing.T) {\n\n\tt.Run(\"2 base urls\", func(t *testing.T) {\n\t\trr, err := NewRoundRobin(\"https://example1.com\", \"https://example2.com\")\n\t\tassertNil(t, err)\n\n\t\trunCount := 5\n\t\tvar result []string\n\t\tctx := context.Background()\n\t\tfor i := 0; i < runCount; i++ {\n\t\t\tbaseURL, _ := rr.NextWithContext(ctx)\n\t\t\tresult = append(result, baseURL)\n\t\t}\n\n\t\texpected := []string{\n\t\t\t\"https://example1.com\", \"https://example2.com\", \"https://example1.com\",\n\t\t\t\"https://example2.com\", \"https://example1.com\",\n\t\t}\n\n\t\tassertEqual(t, runCount, len(expected))\n\t\tassertEqual(t, runCount, len(result))\n\t\tassertEqual(t, expected, result)\n\n\t\trr.Feedback(&RequestFeedback{})\n\t\trr.Close()\n\t})\n\n\tt.Run(\"5 base urls\", func(t *testing.T) {\n\t\tinput := []string{\"https://example1.com\", \"https://example2.com\",\n\t\t\t\"https://example3.com\", \"https://example4.com\", \"https://example5.com\"}\n\t\trr, err := NewRoundRobin(input...)\n\t\tassertNil(t, err)\n\n\t\trunCount := 30\n\t\tvar result []string\n\t\tctx := context.Background()\n\t\tfor i := 0; i < runCount; i++ {\n\t\t\tbaseURL, _ := rr.NextWithContext(ctx)\n\t\t\tresult = append(result, baseURL)\n\t\t}\n\n\t\tvar expected []string\n\t\tfor i := 0; i < runCount/len(input); i++ {\n\t\t\texpected = append(expected, input...)\n\t\t}\n\n\t\tassertEqual(t, runCount, len(expected))\n\t\tassertEqual(t, runCount, len(result))\n\t\tassertEqual(t, expected, result)\n\n\t\trr.Feedback(&RequestFeedback{})\n\t\trr.Close()\n\t})\n\n\tt.Run(\"2 base urls with refresh\", func(t *testing.T) {\n\t\trr, err := NewRoundRobin(\"https://example1.com\", \"https://example2.com\")\n\t\tassertNil(t, err)\n\n\t\terr = rr.Refresh(\"https://example3.com\", \"https://example4.com\")\n\t\tassertNil(t, err)\n\n\t\trunCount := 5\n\t\tvar result []string\n\t\tctx := context.Background()\n\t\tfor i := 0; i < runCount; i++ {\n\t\t\tbaseURL, _ := rr.NextWithContext(ctx)\n\t\t\tresult = append(result, baseURL)\n\t\t}\n\n\t\texpected := []string{\n\t\t\t\"https://example3.com\", \"https://example4.com\", \"https://example3.com\",\n\t\t\t\"https://example4.com\", \"https://example3.com\",\n\t\t}\n\n\t\tassertEqual(t, runCount, len(expected))\n\t\tassertEqual(t, runCount, len(result))\n\t\tassertEqual(t, expected, result)\n\n\t\trr.Feedback(&RequestFeedback{})\n\t\trr.Close()\n\t})\n\n\tt.Run(\"NextWithContext context cancellation\", func(t *testing.T) {\n\t\trr, _ := NewRoundRobin(\"https://example.com\")\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tcancel()\n\t\t_, err := rr.NextWithContext(ctx)\n\t\tassertErrorIs(t, context.Canceled, err)\n\t})\n\n\tt.Run(\"NextWithContext normal operation\", func(t *testing.T) {\n\t\trr, _ := NewRoundRobin(\"https://example1.com\", \"https://example2.com\")\n\t\tctx := context.Background()\n\t\turl1, err := rr.NextWithContext(ctx)\n\t\tassertNil(t, err)\n\t\turl2, err := rr.NextWithContext(ctx)\n\t\tassertNil(t, err)\n\t\tassertNotEqual(t, url1, url2)\n\t})\n}\n\nfunc TestRoundRobinNoBaseURLs(t *testing.T) {\n\tt.Run(\"new round robin no base urls\", func(t *testing.T) {\n\t\trr, err := NewRoundRobin()\n\t\tassertErrorIs(t, ErrNoBaseURLs, err)\n\t\tassertNil(t, rr)\n\t})\n\n\tt.Run(\"new round robin no base urls on next with context\", func(t *testing.T) {\n\t\trr, err := NewRoundRobin(\"https://example1.com\")\n\t\tassertNil(t, err)\n\t\tassertNotNil(t, rr)\n\n\t\trr.Refresh()\n\t\tctx := context.Background()\n\t\t_, err = rr.NextWithContext(ctx)\n\t\tassertErrorIs(t, ErrNoBaseURLs, err)\n\t})\n}\n\nfunc TestWeightedRoundRobin(t *testing.T) {\n\tt.Run(\"3 hosts with weight {5,2,1}\", func(t *testing.T) {\n\t\thosts := []*Host{\n\t\t\t{BaseURL: \"https://example1.com\", Weight: 5},\n\t\t\t{BaseURL: \"https://example2.com\", Weight: 2},\n\t\t\t{BaseURL: \"https://example3.com\", Weight: 1},\n\t\t}\n\n\t\twrr, err := NewWeightedRoundRobin(200*time.Millisecond, hosts...)\n\t\tassertNil(t, err)\n\t\tdefer wrr.Close()\n\n\t\trunCount := 5\n\t\tvar result []string\n\t\tctx := context.Background()\n\t\tfor i := 0; i < runCount; i++ {\n\t\t\tbaseURL, err := wrr.NextWithContext(ctx)\n\t\t\tassertNil(t, err)\n\t\t\tresult = append(result, baseURL)\n\t\t}\n\n\t\texpected := []string{\n\t\t\t\"https://example1.com\", \"https://example2.com\", \"https://example1.com\",\n\t\t\t\"https://example1.com\", \"https://example3.com\",\n\t\t}\n\n\t\tassertEqual(t, runCount, len(expected))\n\t\tassertEqual(t, runCount, len(result))\n\t\tassertEqual(t, expected, result)\n\n\t\twrr.Feedback(nil)\n\t})\n\n\tt.Run(\"3 hosts with weight {2,1,10}\", func(t *testing.T) {\n\t\thosts := []*Host{\n\t\t\t{BaseURL: \"https://example1.com\", Weight: 2},\n\t\t\t{BaseURL: \"https://example2.com\", Weight: 1},\n\t\t\t{BaseURL: \"https://example3.com\", Weight: 10, MaxFailures: 3},\n\t\t}\n\n\t\twrr, err := NewWeightedRoundRobin(200*time.Millisecond, hosts...)\n\t\tassertNil(t, err)\n\t\tdefer wrr.Close()\n\n\t\tvar stateChangeCalled int32\n\t\twrr.SetOnStateChange(func(baseURL string, from, to HostState) {\n\t\t\tatomic.AddInt32(&stateChangeCalled, 1)\n\t\t})\n\n\t\trunCount := 10\n\t\tvar result []string\n\t\tctx := context.Background()\n\t\tfor i := 0; i < runCount; i++ {\n\t\t\tbaseURL, err := wrr.NextWithContext(ctx)\n\t\t\tassertNil(t, err)\n\t\t\tresult = append(result, baseURL)\n\t\t\tif baseURL == \"https://example3.com\" && i%2 != 0 {\n\t\t\t\twrr.Feedback(&RequestFeedback{BaseURL: baseURL, Success: false, Attempt: 1})\n\t\t\t} else {\n\t\t\t\twrr.Feedback(&RequestFeedback{BaseURL: baseURL, Success: true, Attempt: 1})\n\t\t\t}\n\t\t}\n\n\t\texpected := []string{\n\t\t\t\"https://example3.com\", \"https://example3.com\", \"https://example1.com\",\n\t\t\t\"https://example3.com\", \"https://example3.com\", \"https://example3.com\",\n\t\t\t\"https://example2.com\", \"https://example2.com\", \"https://example1.com\",\n\t\t\t\"https://example1.com\",\n\t\t}\n\n\t\tassertEqual(t, int32(1), stateChangeCalled)\n\t\tassertEqual(t, runCount, len(expected))\n\t\tassertEqual(t, runCount, len(result))\n\t\tassertEqual(t, expected, result)\n\t})\n\n\tt.Run(\"2 hosts with weight {5,5} and refresh\", func(t *testing.T) {\n\t\twrr, err := NewWeightedRoundRobin(\n\t\t\t200*time.Millisecond,\n\t\t\t&Host{BaseURL: \"https://example1.com\", Weight: 5},\n\t\t\t&Host{BaseURL: \"https://example2.com\", Weight: 5},\n\t\t)\n\t\tassertNil(t, err)\n\t\tdefer wrr.Close()\n\n\t\terr = wrr.Refresh(\n\t\t\t&Host{BaseURL: \"https://example3.com\", Weight: 5},\n\t\t\t&Host{BaseURL: \"https://example4.com\", Weight: 5},\n\t\t)\n\t\tassertNil(t, err)\n\n\t\trunCount := 5\n\t\tvar result []string\n\t\tctx := context.Background()\n\t\tfor i := 0; i < runCount; i++ {\n\t\t\tbaseURL, err := wrr.NextWithContext(ctx)\n\t\t\tassertNil(t, err)\n\t\t\tresult = append(result, baseURL)\n\t\t}\n\n\t\texpected := []string{\n\t\t\t\"https://example3.com\", \"https://example4.com\", \"https://example3.com\",\n\t\t\t\"https://example4.com\", \"https://example3.com\",\n\t\t}\n\n\t\tassertEqual(t, runCount, len(expected))\n\t\tassertEqual(t, runCount, len(result))\n\t\tassertEqual(t, expected, result)\n\t})\n\n\tt.Run(\"no active hosts error\", func(t *testing.T) {\n\t\twrr, err := NewWeightedRoundRobin(200 * time.Millisecond)\n\t\tassertNil(t, err)\n\t\tdefer wrr.Close()\n\n\t\t_, err = wrr.NextWithContext(context.Background())\n\t\tassertErrorIs(t, ErrNoActiveHost, err)\n\t})\n\n\tt.Run(\"NextWithContext context cancellation\", func(t *testing.T) {\n\t\twrr, _ := NewWeightedRoundRobin(0, &Host{BaseURL: \"https://example.com\", Weight: 1})\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tcancel()\n\t\t_, err := wrr.NextWithContext(ctx)\n\t\tassertErrorIs(t, context.Canceled, err)\n\t})\n\n\tt.Run(\"NextWithContext normal operation\", func(t *testing.T) {\n\t\thosts := []*Host{\n\t\t\t{BaseURL: \"https://example1.com\", Weight: 1},\n\t\t\t{BaseURL: \"https://example2.com\", Weight: 1},\n\t\t}\n\t\twrr, _ := NewWeightedRoundRobin(0, hosts...)\n\t\tctx := context.Background()\n\t\turl1, err := wrr.NextWithContext(ctx)\n\t\tassertNil(t, err)\n\t\turl2, err := wrr.NextWithContext(ctx)\n\t\tassertNil(t, err)\n\t\tassertNotEqual(t, url1, url2)\n\t})\n}\n\nfunc TestSRVWeightedRoundRobin(t *testing.T) {\n\tt.Run(\"3 records with weight {50,30,20}\", func(t *testing.T) {\n\t\tsrv, err := NewSRVWeightedRoundRobin(\"_sample-server\", \"\", \"example.com\", \"\")\n\t\tassertNotNil(t, err)\n\t\tassertNotNil(t, srv)\n\t\tvar dnsErr *net.DNSError\n\t\tassertTrue(t, errors.As(err, &dnsErr), \"expected net.DNSError type\")\n\n\t\t// mock net.LookupSRV call\n\t\tsrv.lookupSRV = func() ([]*net.SRV, error) {\n\t\t\treturn []*net.SRV{\n\t\t\t\t{Target: \"service1.example.com.\", Port: 443, Priority: 10, Weight: 50},\n\t\t\t\t{Target: \"service2.example.com.\", Port: 443, Priority: 20, Weight: 30},\n\t\t\t\t{Target: \"service3.example.com.\", Port: 443, Priority: 20, Weight: 20},\n\t\t\t}, nil\n\t\t}\n\t\terr = srv.Refresh()\n\t\tassertNil(t, err)\n\n\t\tsrv.SetRecoveryDuration(200 * time.Millisecond)\n\n\t\trunCount := 5\n\t\tvar result []string\n\t\tctx := context.Background()\n\t\tfor i := 0; i < runCount; i++ {\n\t\t\tbaseURL, err := srv.NextWithContext(ctx)\n\t\t\tassertNil(t, err)\n\t\t\tresult = append(result, baseURL)\n\t\t}\n\n\t\texpected := []string{\n\t\t\t\"https://service1.example.com:443\", \"https://service2.example.com:443\",\n\t\t\t\"https://service3.example.com:443\", \"https://service1.example.com:443\",\n\t\t\t\"https://service1.example.com:443\",\n\t\t}\n\n\t\tassertEqual(t, runCount, len(expected))\n\t\tassertEqual(t, runCount, len(result))\n\t\tassertEqual(t, expected, result)\n\t})\n\n\tt.Run(\"2 records with weight {50,50}\", func(t *testing.T) {\n\t\tsrv, err := NewSRVWeightedRoundRobin(\"_sample-server\", \"\", \"example.com\", \"\")\n\t\tassertNotNil(t, err)\n\t\tassertNotNil(t, srv)\n\t\tvar dnsErr *net.DNSError\n\t\tassertTrue(t, errors.As(err, &dnsErr), \"expected net.DNSError type\")\n\n\t\t// mock net.LookupSRV call\n\t\tsrv.lookupSRV = func() ([]*net.SRV, error) {\n\t\t\treturn []*net.SRV{\n\t\t\t\t{Target: \"service1.example.com.\", Port: 443, Priority: 10, Weight: 50},\n\t\t\t\t{Target: \"service2.example.com.\", Port: 443, Priority: 20, Weight: 50},\n\t\t\t}, nil\n\t\t}\n\t\terr = srv.Refresh()\n\t\tassertNil(t, err)\n\n\t\tsrv.SetRecoveryDuration(200 * time.Millisecond)\n\n\t\trunCount := 5\n\t\tvar result []string\n\t\tctx := context.Background()\n\t\tfor i := 0; i < runCount; i++ {\n\t\t\tbaseURL, err := srv.NextWithContext(ctx)\n\t\t\tassertNil(t, err)\n\t\t\tresult = append(result, baseURL)\n\t\t}\n\n\t\texpected := []string{\n\t\t\t\"https://service1.example.com:443\", \"https://service2.example.com:443\",\n\t\t\t\"https://service1.example.com:443\", \"https://service2.example.com:443\",\n\t\t\t\"https://service1.example.com:443\",\n\t\t}\n\n\t\tassertEqual(t, runCount, len(expected))\n\t\tassertEqual(t, runCount, len(result))\n\t\tassertEqual(t, expected, result)\n\t})\n\n\tt.Run(\"3 records with weight {60,20,20}\", func(t *testing.T) {\n\t\tsrv, err := NewSRVWeightedRoundRobin(\"_sample-server\", \"\", \"example.com\", \"\")\n\t\tassertNotNil(t, err)\n\t\tassertNotNil(t, srv)\n\t\tvar dnsErr *net.DNSError\n\t\tassertTrue(t, errors.As(err, &dnsErr), \"expected net.DNSError type\")\n\n\t\t// mock net.LookupSRV call\n\t\tsrv.lookupSRV = func() ([]*net.SRV, error) {\n\t\t\treturn []*net.SRV{\n\t\t\t\t{Target: \"service1.example.com.\", Port: 443, Priority: 10, Weight: 60},\n\t\t\t\t{Target: \"service2.example.com.\", Port: 443, Priority: 20, Weight: 20},\n\t\t\t\t{Target: \"service3.example.com.\", Port: 443, Priority: 20, Weight: 20},\n\t\t\t}, nil\n\t\t}\n\t\terr = srv.Refresh()\n\t\tassertNil(t, err)\n\n\t\tvar stateChangeCalled int32\n\t\tsrv.SetOnStateChange(func(baseURL string, from, to HostState) {\n\t\t\tatomic.AddInt32(&stateChangeCalled, 1)\n\t\t})\n\n\t\tsrv.SetRecoveryDuration(200 * time.Millisecond)\n\n\t\trunCount := 20\n\t\tvar result []string\n\t\tctx := context.Background()\n\t\tfor i := 0; i < runCount; i++ {\n\t\t\tbaseURL, err := srv.NextWithContext(ctx)\n\t\t\tassertNil(t, err)\n\t\t\tresult = append(result, baseURL)\n\n\t\t\tif baseURL == \"https://service1.example.com:443\" {\n\t\t\t\tsrv.Feedback(&RequestFeedback{BaseURL: baseURL, Success: false, Attempt: 1})\n\t\t\t} else {\n\t\t\t\tsrv.Feedback(&RequestFeedback{BaseURL: baseURL, Success: true, Attempt: 1})\n\t\t\t}\n\t\t}\n\n\t\texpected := []string{\n\t\t\t\"https://service1.example.com:443\", \"https://service2.example.com:443\", \"https://service1.example.com:443\",\n\t\t\t\"https://service3.example.com:443\", \"https://service1.example.com:443\", \"https://service1.example.com:443\",\n\t\t\t\"https://service2.example.com:443\", \"https://service1.example.com:443\", \"https://service3.example.com:443\",\n\t\t\t\"https://service3.example.com:443\", \"https://service3.example.com:443\", \"https://service2.example.com:443\",\n\t\t\t\"https://service3.example.com:443\", \"https://service2.example.com:443\", \"https://service3.example.com:443\",\n\t\t\t\"https://service2.example.com:443\", \"https://service3.example.com:443\", \"https://service2.example.com:443\",\n\t\t\t\"https://service3.example.com:443\", \"https://service2.example.com:443\",\n\t\t}\n\n\t\tassertEqual(t, runCount, len(expected))\n\t\tassertEqual(t, runCount, len(result))\n\t\tassertEqual(t, expected, result)\n\t})\n\n\tt.Run(\"srv record with refresh duration 100ms\", func(t *testing.T) {\n\t\tsrv, err := NewSRVWeightedRoundRobin(\"_sample-server\", \"\", \"example.com\", \"\")\n\t\tassertNotNil(t, err)\n\t\tassertNotNil(t, srv)\n\t\tvar dnsErr *net.DNSError\n\t\tassertTrue(t, errors.As(err, &dnsErr), \"expected net.DNSError type\")\n\n\t\t// mock net.LookupSRV call\n\t\tsrv.lookupSRV = func() ([]*net.SRV, error) {\n\t\t\treturn []*net.SRV{\n\t\t\t\t{Target: \"service1.example.com.\", Port: 443, Priority: 10, Weight: 50},\n\t\t\t\t{Target: \"service2.example.com.\", Port: 443, Priority: 20, Weight: 50},\n\t\t\t}, nil\n\t\t}\n\t\terr = srv.Refresh()\n\t\tassertNil(t, err)\n\n\t\tsrv.SetRecoveryDuration(200 * time.Millisecond)\n\n\t\tgo func() {\n\t\t\tfor i := 0; i < 10; i++ {\n\t\t\t\tbaseURL, _ := srv.NextWithContext(context.Background())\n\t\t\t\tassertNotNil(t, baseURL)\n\t\t\t\ttime.Sleep(15 * time.Millisecond)\n\t\t\t}\n\t\t}()\n\n\t\tsrv.SetRefreshDuration(150 * time.Millisecond)\n\t\ttime.Sleep(320 * time.Millisecond)\n\t\tsrv.Close()\n\t})\n\n\tt.Run(\"srv record with error on default lookupSRV\", func(t *testing.T) {\n\t\tsrv, err := NewSRVWeightedRoundRobin(\"_sample-server\", \"\", \"example.com\", \"\")\n\t\tassertNotNil(t, err)\n\t\tassertNotNil(t, srv)\n\t\tvar dnsErr *net.DNSError\n\t\tassertTrue(t, errors.As(err, &dnsErr), \"expected net.DNSError type\")\n\n\t\t// default error flow\n\t\terr = srv.Refresh()\n\t\tassertNotNil(t, err)\n\t\tassertTrue(t, errors.As(err, &dnsErr), \"expected net.DNSError type\")\n\n\t\t// replace with mock error flow\n\t\terrMockTest := errors.New(\"network error\")\n\t\tsrv.lookupSRV = func() ([]*net.SRV, error) { return nil, errMockTest }\n\t\terr = srv.Refresh()\n\t\tassertNotNil(t, err)\n\t\tassertErrorIs(t, errMockTest, err, \"expected network error type\")\n\n\t})\n\n}\n\nfunc TestLoadBalancerRequest(t *testing.T) {\n\tts1 := createGetServer(t)\n\tdefer ts1.Close()\n\n\tts2 := createGetServer(t)\n\tdefer ts2.Close()\n\n\trr, err := NewRoundRobin(ts1.URL, ts2.URL)\n\tassertNil(t, err)\n\n\tc := dcnl()\n\tdefer c.Close()\n\n\tc.SetLoadBalancer(rr)\n\n\tts1URL, ts2URL := 0, 0\n\tfor i := 0; i < 20; i++ {\n\t\tresp, err := c.R().Get(\"/\")\n\t\tassertNil(t, err)\n\t\tswitch resp.Request.baseURL {\n\t\tcase ts1.URL:\n\t\t\tts1URL++\n\t\tcase ts2.URL:\n\t\t\tts2URL++\n\t\t}\n\t}\n\tassertEqual(t, ts1URL, ts2URL)\n}\n\nfunc TestLoadBalancerRequestFlowError(t *testing.T) {\n\n\tt.Run(\"obtain next url error\", func(t *testing.T) {\n\t\twrr, err := NewWeightedRoundRobin(0)\n\t\tassertNil(t, err)\n\n\t\tc := dcnl()\n\t\tdefer c.Close()\n\n\t\tc.SetLoadBalancer(wrr)\n\n\t\tresp, err := c.R().Get(\"/\")\n\t\tassertErrorIs(t, ErrNoActiveHost, err)\n\t\tassertNil(t, resp)\n\t})\n\n\tt.Run(\"round-robin invalid url input\", func(t *testing.T) {\n\t\trr, err := NewRoundRobin(\"://example.com\")\n\t\tassertType(t, url.Error{}, err)\n\t\tassertNotNil(t, rr)\n\n\t\twrr, err := NewWeightedRoundRobin(0, &Host{BaseURL: \"://example.com\"})\n\t\tassertType(t, url.Error{}, err)\n\t\tassertNotNil(t, wrr)\n\t})\n\n\tt.Run(\"weighted round-robin invalid url input\", func(t *testing.T) {\n\t\twrr, err := NewWeightedRoundRobin(0, &Host{BaseURL: \"://example.com\"})\n\t\tassertType(t, url.Error{}, err)\n\t\tassertNotNil(t, wrr)\n\t})\n}\n\nfunc Test_extractBaseURL(t *testing.T) {\n\tfor _, tt := range []struct {\n\t\tname        string\n\t\tinputURL    string\n\t\texpectedURL string\n\t\texpectedErr error\n\t}{\n\t\t{\n\t\t\tname:        \"simple relative path\",\n\t\t\tinputURL:    \"https://resty.dev/welcome\",\n\t\t\texpectedURL: \"https://resty.dev\",\n\t\t},\n\t\t{\n\t\t\tname:        \"longer relative path with file extension\",\n\t\t\tinputURL:    \"https://resty.dev/welcome/path/to/remove.html\",\n\t\t\texpectedURL: \"https://resty.dev\",\n\t\t},\n\t\t{\n\t\t\tname:        \"longer relative path with file extension and query params\",\n\t\t\tinputURL:    \"https://resty.dev/welcome/path/to/remove.html?a=1&b=2\",\n\t\t\texpectedURL: \"https://resty.dev\",\n\t\t},\n\t\t{\n\t\t\tname:        \"invalid url input\",\n\t\t\tinputURL:    \"://resty.dev/welcome\",\n\t\t\texpectedURL: \"\",\n\t\t\texpectedErr: &url.Error{Op: \"parse\", URL: \"://resty.dev/welcome\", Err: errors.New(\"missing protocol scheme\")},\n\t\t},\n\t} {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\toutputURL, err := extractBaseURL(tt.inputURL)\n\t\t\tif tt.expectedErr != nil {\n\t\t\t\tassertEqual(t, tt.expectedErr, err)\n\t\t\t}\n\t\t\tassertEqual(t, tt.expectedURL, outputURL)\n\t\t})\n\t}\n}\n\nfunc TestLoadBalancerRequestFailures(t *testing.T) {\n\tts1 := createGetServer(t)\n\tts1.Close()\n\n\tts2 := createGetServer(t)\n\tdefer ts2.Close()\n\n\trr, err := NewWeightedRoundRobin(200*time.Millisecond,\n\t\t&Host{BaseURL: ts1.URL, Weight: 50, MaxFailures: 3}, &Host{BaseURL: ts2.URL, Weight: 50})\n\tassertNil(t, err)\n\n\tc := dcnl()\n\tdefer c.Close()\n\n\tc.SetLoadBalancer(rr)\n\n\tts1URL, ts2URL := 0, 0\n\tfor i := 0; i < 10; i++ {\n\t\tresp, _ := c.R().Get(\"/\")\n\t\tswitch resp.Request.baseURL {\n\t\tcase ts1.URL:\n\t\t\tts1URL++\n\t\tcase ts2.URL:\n\t\t\tassertError(t, err)\n\t\t\tts2URL++\n\t\t}\n\t}\n\tassertEqual(t, 3, ts1URL)\n\tassertEqual(t, 7, ts2URL)\n}\n\ntype mockTimeoutErr struct{}\n\nfunc (e *mockTimeoutErr) Error() string { return \"i/o timeout\" }\nfunc (e *mockTimeoutErr) Timeout() bool { return true }\n\nfunc TestLoadBalancerCoverage(t *testing.T) {\n\tt.Run(\"mock net op timeout error\", func(t *testing.T) {\n\t\twrr, err := NewWeightedRoundRobin(0)\n\t\tassertNil(t, err)\n\n\t\tc := dcnl()\n\t\tdefer c.Close()\n\n\t\tc.SetLoadBalancer(wrr)\n\n\t\treq := c.R()\n\n\t\tnetOpErr := &net.OpError{Op: \"mock\", Net: \"mock\", Err: &mockTimeoutErr{}}\n\t\treq.sendLoadBalancerFeedback(&Response{}, netOpErr)\n\n\t\treq.sendLoadBalancerFeedback(&Response{RawResponse: &http.Response{\n\t\t\tStatusCode: http.StatusInternalServerError,\n\t\t}}, nil)\n\t})\n}\n"
  },
  {
    "path": "middleware.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/textproto\"\n\t\"net/url\"\n\t\"path\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strings\"\n)\n\n//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\n// Request Middleware(s)\n//_______________________________________________________________________\n\n// MiddlewareRequestCreate method is used to prepare HTTP requests using the\n// user-provided request values. It performs the following operations -\n//   - Parse the request URL with path params and query params\n//   - Parse the request headers from client and request level\n//   - Parse the request body based on the content type and body type\n//   - Create the underlying [http.Request] object\n//   - Add credentials such as Basic Auth and Token Auth into the request\n//\n// Returns an error if request preparation fails.\nfunc MiddlewareRequestCreate(c *Client, r *Request) (err error) {\n\tif err = parseRequestURL(c, r); err != nil {\n\t\treturn err\n\t}\n\n\t// no error returned\n\tparseRequestHeader(c, r)\n\n\tif err = parseRequestBody(c, r); err != nil {\n\t\treturn err\n\t}\n\n\t// at this point, possible error from `http.NewRequestWithContext`\n\t// is URL-related, and those get caught up in the `parseRequestURL`\n\tcreateRawRequest(c, r)\n\n\taddCredentials(c, r)\n\n\t_ = r.generateCurlCommand()\n\n\treturn nil\n}\n\nfunc parseRequestURL(c *Client, r *Request) error {\n\tif len(c.PathParams())+len(r.PathParams) > 0 {\n\t\t// GitHub #103 Path Params, #663 Raw Path Params\n\t\tfor p, v := range c.PathParams() {\n\t\t\tif _, ok := r.PathParams[p]; ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tr.PathParams[p] = v\n\t\t}\n\n\t\tvar prev int\n\t\tbuf := acquireBuffer()\n\t\tdefer releaseBuffer(buf)\n\t\t// search for the next or first opened curly bracket\n\t\tfor curr := strings.Index(r.URL, \"{\"); curr == 0 || curr > prev; curr = prev + strings.Index(r.URL[prev:], \"{\") {\n\t\t\t// write everything from the previous position up to the current\n\t\t\tif curr > prev {\n\t\t\t\tbuf.WriteString(r.URL[prev:curr])\n\t\t\t}\n\t\t\t// search for the closed curly bracket from current position\n\t\t\tnext := curr + strings.Index(r.URL[curr:], \"}\")\n\t\t\t// if not found, then write the remainder and exit\n\t\t\tif next < curr {\n\t\t\t\tbuf.WriteString(r.URL[curr:])\n\t\t\t\tprev = len(r.URL)\n\t\t\t\tbreak\n\t\t\t}\n\t\t\t// special case for {}, without parameter's name\n\t\t\tif next == curr+1 {\n\t\t\t\tbuf.WriteString(\"{}\")\n\t\t\t} else {\n\t\t\t\t// check for the replacement\n\t\t\t\tkey := r.URL[curr+1 : next]\n\t\t\t\tvalue, ok := r.PathParams[key]\n\t\t\t\t// keep the original string if the replacement not found\n\t\t\t\tif !ok {\n\t\t\t\t\tvalue = r.URL[curr : next+1]\n\t\t\t\t}\n\t\t\t\tbuf.WriteString(value)\n\t\t\t}\n\n\t\t\t// set the previous position after the closed curly bracket\n\t\t\tprev = next + 1\n\t\t\tif prev >= len(r.URL) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif buf.Len() > 0 {\n\t\t\t// write remainder\n\t\t\tif prev < len(r.URL) {\n\t\t\t\tbuf.WriteString(r.URL[prev:])\n\t\t\t}\n\t\t\tr.URL = buf.String()\n\t\t}\n\t}\n\n\t// Parsing request URL\n\treqURL, err := url.Parse(r.URL)\n\tif err != nil {\n\t\treturn &invalidRequestError{Err: err}\n\t}\n\n\t// If [Request.URL] is a relative path, then the following\n\t// gets evaluated in the order\n\t//\t1. [Client.LoadBalancer] is used to obtain the base URL if not nil\n\t//\t2. [Client.BaseURL] is used to obtain the base URL\n\t//\t3. Otherwise [Request.URL] is used as-is\n\tif !reqURL.IsAbs() {\n\t\tr.URL = reqURL.String()\n\t\tif len(r.URL) > 0 && r.URL[0] != '/' {\n\t\t\tr.URL = \"/\" + r.URL\n\t\t}\n\n\t\tif r.client.LoadBalancer() != nil {\n\t\t\tr.baseURL, err = r.client.LoadBalancer().NextWithContext(r.Context())\n\t\t\tif err != nil {\n\t\t\t\treturn &invalidRequestError{Err: err}\n\t\t\t}\n\t\t}\n\n\t\treqURL, err = url.Parse(r.baseURL + r.URL)\n\t\tif err != nil {\n\t\t\treturn &invalidRequestError{Err: err}\n\t\t}\n\t}\n\n\t// GH #407 && #318\n\tif reqURL.Scheme == \"\" && len(c.Scheme()) > 0 {\n\t\treqURL.Scheme = c.Scheme()\n\t}\n\n\t// Adding Query Param\n\tif len(c.QueryParams())+len(r.QueryParams) > 0 {\n\t\tfor k, v := range c.QueryParams() {\n\t\t\tif _, ok := r.QueryParams[k]; ok {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tr.QueryParams[k] = v[:]\n\t\t}\n\n\t\t// GitHub #123 Preserve query string order partially.\n\t\t// Since not feasible in `SetQuery*` resty methods, because\n\t\t// standard package `url.Encode(...)` sorts the query params\n\t\t// alphabetically\n\t\tif isStringEmpty(reqURL.RawQuery) {\n\t\t\treqURL.RawQuery = r.QueryParams.Encode()\n\t\t} else {\n\t\t\treqURL.RawQuery = reqURL.RawQuery + \"&\" + r.QueryParams.Encode()\n\t\t}\n\t}\n\n\t// GH#797 Unescape query parameters (non-standard - not recommended)\n\tif r.unescapeQueryParams && len(reqURL.RawQuery) > 0 {\n\t\t// at this point, all errors caught up in the above operations\n\t\t// so ignore the return error on query unescape; I realized\n\t\t// while writing the unit test\n\t\tunescapedQuery, _ := url.QueryUnescape(reqURL.RawQuery)\n\t\treqURL.RawQuery = strings.ReplaceAll(unescapedQuery, \" \", \"+\") // otherwise request becomes bad request\n\t}\n\n\tr.URL = reqURL.String()\n\n\treturn nil\n}\n\nfunc parseRequestHeader(c *Client, r *Request) error {\n\tfor k, v := range c.Header() {\n\t\tif _, ok := r.Header[k]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tr.Header[k] = v[:]\n\t}\n\n\tif !r.isHeaderExists(hdrUserAgentKey) {\n\t\tr.Header.Set(hdrUserAgentKey, hdrUserAgentValue)\n\t}\n\n\tif !r.isHeaderExists(hdrAcceptEncodingKey) {\n\t\tr.Header.Set(hdrAcceptEncodingKey, r.client.ContentDecompresserKeys())\n\t}\n\n\treturn nil\n}\n\nfunc parseRequestBody(c *Client, r *Request) error {\n\tif r.isMultiPart && !(r.Method == MethodPost || r.Method == MethodPut || r.Method == MethodPatch) {\n\t\terr := fmt.Errorf(\"resty: multipart is not allowed in HTTP verb: %v\", r.Method)\n\t\treturn &invalidRequestError{Err: err}\n\t}\n\n\tif r.isPayloadSupported() {\n\t\tswitch {\n\t\tcase r.isMultiPart: // Handling Multipart\n\t\t\tif err := handleMultipart(c, r); err != nil {\n\t\t\t\treturn &invalidRequestError{Err: err}\n\t\t\t}\n\t\tcase len(c.FormData()) > 0 || len(r.FormData) > 0: // Handling Form Data\n\t\t\thandleFormData(c, r)\n\t\tcase r.Body != nil: // Handling Request body\n\t\t\tif err := handleRequestBody(c, r); err != nil {\n\t\t\t\treturn &invalidRequestError{Err: err}\n\t\t\t}\n\t\t}\n\t} else {\n\t\tr.Body = nil // if the payload is not supported by HTTP verb, set explicit nil\n\t}\n\n\treturn nil\n}\n\nfunc createRawRequest(c *Client, r *Request) (err error) {\n\t// init client trace if enabled\n\tr.initTraceIfEnabled()\n\n\tif r.bodyBuf == nil {\n\t\tif reader, ok := r.Body.(io.Reader); ok {\n\t\t\tr.RawRequest, err = http.NewRequestWithContext(r.Context(), r.Method, r.URL, reader)\n\t\t} else {\n\t\t\tr.RawRequest, err = http.NewRequestWithContext(r.Context(), r.Method, r.URL, nil)\n\t\t}\n\t} else {\n\t\tr.RawRequest, err = http.NewRequestWithContext(r.Context(), r.Method, r.URL, r.bodyBuf)\n\t}\n\n\tif err != nil {\n\t\treturn &invalidRequestError{Err: err}\n\t}\n\n\t// get the context reference back from underlying RawRequest\n\tr.SetContext(r.RawRequest.Context())\n\n\t// Assign close connection option\n\tr.RawRequest.Close = r.IsCloseConnection\n\n\t// Add headers into http request\n\tr.RawRequest.Header = r.Header.Clone()\n\n\t// Add cookies from client instance into http request\n\tfor _, cookie := range c.Cookies() {\n\t\tr.RawRequest.AddCookie(cookie)\n\t}\n\n\t// Add cookies from request instance into http request\n\tfor _, cookie := range r.Cookies {\n\t\tr.RawRequest.AddCookie(cookie)\n\t}\n\n\t// Set given content length value into the request\n\tif r.isContentLengthSet {\n\t\tr.RawRequest.ContentLength = r.contentLength\n\t} else {\n\t\tr.contentLength = r.RawRequest.ContentLength\n\t}\n\n\treturn\n}\n\nfunc addCredentials(c *Client, r *Request) error {\n\tcredentialsAdded := false\n\t// Basic Auth\n\tif r.credentials != nil {\n\t\tcredentialsAdded = true\n\t\tr.RawRequest.SetBasicAuth(r.credentials.Username, r.credentials.Password)\n\t}\n\n\t// Build the token Auth header\n\tif !isStringEmpty(r.AuthToken) {\n\t\tcredentialsAdded = true\n\t\tr.RawRequest.Header.Set(r.HeaderAuthorizationKey, strings.TrimSpace(r.AuthScheme+\" \"+r.AuthToken))\n\t}\n\n\tif !c.IsDisableWarn() && credentialsAdded {\n\t\tif r.RawRequest.URL.Scheme == \"http\" {\n\t\t\tr.log.Warnf(\"Using sensitive credentials in HTTP mode is not secure. Use HTTPS\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\nvar multipartWriteField = func(w *multipart.Writer, name, value string) error {\n\treturn w.WriteField(name, value)\n}\n\nvar multipartWriteFormData = func(w *multipart.Writer, r *Request) error {\n\tfor k, v := range r.FormData {\n\t\tfor _, iv := range v {\n\t\t\tif err := multipartWriteField(w, k, iv); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nvar multipartCreatePart = func(w *multipart.Writer, h textproto.MIMEHeader) (io.Writer, error) {\n\treturn w.CreatePart(h)\n}\n\nvar multipartSetBoundary = func(w *multipart.Writer, r *Request) error {\n\tif isStringEmpty(r.multipartBoundary) {\n\t\treturn nil\n\t}\n\treturn w.SetBoundary(r.multipartBoundary)\n}\n\nfunc handleMultipartFormData(r *Request) error {\n\tr.bodyBuf = acquireBuffer()\n\tmw := multipart.NewWriter(r.bodyBuf)\n\tdefer mw.Close()\n\n\t// set custom multipart boundary if exists\n\tif err := multipartSetBoundary(mw, r); err != nil {\n\t\treturn err\n\t}\n\n\tr.Header.Set(hdrContentTypeKey, mw.FormDataContentType())\n\n\treturn multipartWriteFormData(mw, r)\n}\n\nfunc handleMultipart(c *Client, r *Request) error {\n\tfor k, v := range c.FormData() {\n\t\tif _, ok := r.FormData[k]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tr.FormData[k] = v[:]\n\t}\n\n\tif len(r.multipartFields) == 0 {\n\t\treturn handleMultipartFormData(r)\n\t}\n\n\t// pre-process multipart fields to catch possible errors\n\tfor _, mf := range r.multipartFields {\n\t\tif mf.isValues() {\n\t\t\tcontinue\n\t\t}\n\n\t\tif err := mf.openFile(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif err := mf.detectContentType(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// multipart streaming\n\tbr, bw := io.Pipe()\n\tmw := multipart.NewWriter(bw)\n\tr.Body = br\n\n\t// set custom multipart boundary if exists\n\tif err := multipartSetBoundary(mw, r); err != nil {\n\t\tcloseq(bw)\n\t\treturn err\n\t}\n\n\tr.Header.Set(hdrContentTypeKey, mw.FormDataContentType())\n\n\tr.multipartErrChan = make(chan error, 1)\n\tgo func() {\n\t\tdefer close(r.multipartErrChan)\n\t\tdefer func() {\n\t\t\tif err := mw.Close(); err != nil {\n\t\t\t\tr.multipartErrChan <- err\n\t\t\t}\n\t\t\tif err := bw.Close(); err != nil {\n\t\t\t\tr.multipartErrChan <- err\n\t\t\t}\n\t\t}()\n\n\t\tif err := multipartWriteFormData(mw, r); err != nil {\n\t\t\tr.multipartErrChan <- err\n\t\t\treturn\n\t\t}\n\n\t\tctx, cancel := context.WithCancel(r.Context())\n\t\tr.multipartCancelFunc = cancel\n\t\tfor _, mf := range r.multipartFields {\n\t\t\tif mf.isValues() {\n\t\t\t\tfor _, v := range mf.Values {\n\t\t\t\t\tif err := multipartWriteField(mw, mf.Name, v); err != nil {\n\t\t\t\t\t\tr.multipartErrChan <- err\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tpartWriter, err := multipartCreatePart(mw, mf.createHeader())\n\t\t\tif err != nil {\n\t\t\t\tr.multipartErrChan <- err\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tpartWriter = mf.wrapProgressCallbackIfPresent(partWriter)\n\t\t\tif len(mf.tempBuf) > 0 {\n\t\t\t\tif _, err = partWriter.Write(mf.tempBuf); err != nil {\n\t\t\t\t\tr.multipartErrChan <- err\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treader := &gracefulStopReader{ctx: ctx, r: mf.Reader}\n\t\t\tif _, err = ioCopy(partWriter, reader); err != nil {\n\t\t\t\tr.multipartErrChan <- err\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn nil\n}\n\nfunc handleFormData(c *Client, r *Request) {\n\tfor k, v := range c.FormData() {\n\t\tif _, ok := r.FormData[k]; ok {\n\t\t\tcontinue\n\t\t}\n\t\tr.FormData[k] = v[:]\n\t}\n\n\tr.bodyBuf = acquireBuffer()\n\tr.bodyBuf.WriteString(r.FormData.Encode())\n\tr.Header.Set(hdrContentTypeKey, formContentType)\n\tr.isFormData = true\n}\n\nfunc handleRequestBody(c *Client, r *Request) error {\n\tcontentType := strings.ToLower(r.Header.Get(hdrContentTypeKey))\n\tif isStringEmpty(contentType) {\n\t\t// it is highly recommended that the user provide a request content-type\n\t\t// so that we can minimize memory allocation and compute.\n\t\tcontentType = detectContentType(r.Body)\n\t}\n\tif !r.isHeaderExists(hdrContentTypeKey) {\n\t\tr.Header.Set(hdrContentTypeKey, contentType)\n\t}\n\n\tr.bodyBuf = acquireBuffer()\n\n\tswitch body := r.Body.(type) {\n\tcase io.Reader:\n\t\t// Resty v3 onwards io.Reader used as-is with the request body.\n\t\treleaseBuffer(r.bodyBuf)\n\t\tr.bodyBuf = nil\n\n\t\t// enable multiple reads if body is *bytes.Buffer\n\t\tif b, ok := r.Body.(*bytes.Buffer); ok {\n\t\t\tv := b.Bytes()\n\t\t\tr.Body = bytes.NewReader(v)\n\t\t}\n\n\t\t// do seek start for retry attempt if io.ReadSeeker\n\t\t// interface supported\n\t\tif r.Attempt > 1 {\n\t\t\tif rs, ok := r.Body.(io.ReadSeeker); ok {\n\t\t\t\t_, _ = rs.Seek(0, io.SeekStart)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\tcase []byte:\n\t\tr.bodyBuf.Write(body)\n\tcase string:\n\t\tr.bodyBuf.Write([]byte(body))\n\tdefault:\n\t\tencKey := inferContentTypeMapKey(contentType)\n\t\tif jsonKey == encKey {\n\t\t\tif !r.jsonEscapeHTML {\n\t\t\t\treturn encodeJSONEscapeHTML(r.bodyBuf, r.Body, r.jsonEscapeHTML)\n\t\t\t}\n\t\t} else if xmlKey == encKey {\n\t\t\tif inferKind(r.Body) != reflect.Struct {\n\t\t\t\treleaseBuffer(r.bodyBuf)\n\t\t\t\tr.bodyBuf = nil\n\t\t\t\treturn ErrUnsupportedRequestBodyKind\n\t\t\t}\n\t\t}\n\n\t\t// user registered encoders with resty fallback key\n\t\tencFunc, found := c.inferContentTypeEncoder(contentType, encKey)\n\t\tif !found {\n\t\t\treleaseBuffer(r.bodyBuf)\n\t\t\tr.bodyBuf = nil\n\t\t\treturn fmt.Errorf(\"resty: content-type encoder not found for %s\", contentType)\n\t\t}\n\t\tif err := encFunc(r.bodyBuf, r.Body); err != nil {\n\t\t\treleaseBuffer(r.bodyBuf)\n\t\t\tr.bodyBuf = nil\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\n// Response Middleware(s)\n//_______________________________________________________________________\n\n// MiddlewareResponseAutoParse method is used to parse the response body automatically\n// based on the registered HTTP response `Content-Type` decoder, see [Client.AddContentTypeDecoder];\n// if [Request.SetResult], [Request.SetResultError], or [Client.SetResultError] is used, it performs\n// the auto unmarshalling into the respective object.\nfunc MiddlewareResponseAutoParse(c *Client, res *Response) (err error) {\n\tif (res.CascadeError != nil && (res.Request.isMultiPart && res.StatusCode() == 0)) ||\n\t\tres.Request.IsResponseDoNotParse {\n\t\treturn // move on\n\t}\n\n\tif res.StatusCode() == http.StatusNoContent {\n\t\tres.Request.ResultError = nil\n\t\treturn\n\t}\n\n\trct := strings.ToLower(firstNonEmpty(\n\t\tres.Request.ResponseForceContentType,\n\t\tres.Header().Get(hdrContentTypeKey),\n\t\tres.Request.ResponseExpectContentType,\n\t))\n\tdecKey := inferContentTypeMapKey(rct)\n\tdecFunc, found := c.inferContentTypeDecoder(rct, decKey)\n\tif !found {\n\t\t// the Content-Type decoder is not found; just read all the body bytes\n\t\terr = res.readAll()\n\t\treturn\n\t}\n\n\t// HTTP status code > 199 and < 300, considered as Result\n\tif res.IsStatusSuccess() && res.Request.Result != nil {\n\t\tres.Request.ResultError = nil\n\t\tdefer closeq(res.Body)\n\t\terr = decFunc(res.Body, res.Request.Result)\n\t\tres.IsRead = true\n\t\treturn\n\t}\n\n\t// HTTP status code > 399, considered as Error\n\tif res.IsStatusFailure() {\n\t\t// global error type registered at client-instance\n\t\tif res.Request.ResultError == nil {\n\t\t\tres.Request.ResultError = c.newErrorInterface()\n\t\t}\n\n\t\tif res.Request.ResultError != nil {\n\t\t\tdefer closeq(res.Body)\n\t\t\terr = decFunc(res.Body, res.Request.ResultError)\n\t\t\tres.IsRead = true\n\t\t\treturn\n\t\t}\n\t}\n\n\treturn\n}\n\nvar hostnameReplacer = strings.NewReplacer(\":\", \"_\", \".\", \"_\")\n\n// MiddlewareResponseSaveToFile method used to write HTTP response body into\n// file. The filename is determined in the following order -\n//   - [Request.SetResponseSaveFileName]\n//   - Content-Disposition header\n//   - Request URL using [path.Base]\nfunc MiddlewareResponseSaveToFile(c *Client, res *Response) error {\n\tif res.CascadeError != nil || !res.Request.IsResponseSaveToFile {\n\t\treturn nil\n\t}\n\n\tfile := res.Request.ResponseSaveFileName\n\tif isStringEmpty(file) {\n\t\tcntDispositionValue := res.Header().Get(hdrContentDisposition)\n\t\tif len(cntDispositionValue) > 0 {\n\t\t\tif _, params, err := mime.ParseMediaType(cntDispositionValue); err == nil {\n\t\t\t\tfile = params[\"filename\"]\n\t\t\t}\n\t\t}\n\t\tif isStringEmpty(file) {\n\t\t\trURL, _ := url.Parse(res.Request.URL)\n\t\t\tif isStringEmpty(rURL.Path) || rURL.Path == \"/\" {\n\t\t\t\tfile = hostnameReplacer.Replace(rURL.Host)\n\t\t\t} else {\n\t\t\t\tfile = path.Base(rURL.Path)\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(c.ResponseSaveDirectory()) > 0 && !filepath.IsAbs(file) {\n\t\tfile = filepath.Join(c.ResponseSaveDirectory(), string(filepath.Separator), file)\n\t}\n\n\tfile = filepath.Clean(file)\n\tif err := createDirectory(filepath.Dir(file)); err != nil {\n\t\treturn err\n\t}\n\n\toutFile, err := createFile(file)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer func() {\n\t\tcloseq(outFile)\n\t\tcloseq(res.Body)\n\t}()\n\n\t// io.Copy reads maximum 32kb size, it is perfect for large file download too\n\tres.size, err = ioCopy(outFile, res.Body)\n\n\treturn err\n}\n"
  },
  {
    "path": "middleware_test.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"mime\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/textproto\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n)\n\nfunc Test_parseRequestURL(t *testing.T) {\n\tfor _, tt := range []struct {\n\t\tname        string\n\t\tinitClient  func(c *Client)\n\t\tinitRequest func(r *Request)\n\t\texpectedURL string\n\t}{\n\t\t{\n\t\t\tname: \"apply client path parameters\",\n\t\t\tinitClient: func(c *Client) {\n\t\t\t\tc.SetPathParams(map[string]string{\n\t\t\t\t\t\"foo\": \"1\",\n\t\t\t\t\t\"bar\": \"2/3\",\n\t\t\t\t})\n\t\t\t},\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.URL = \"https://example.com/{foo}/{bar}\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/1/2%2F3\",\n\t\t},\n\t\t{\n\t\t\tname: \"apply request path parameters\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetPathParams(map[string]string{\n\t\t\t\t\t\"foo\": \"4\",\n\t\t\t\t\t\"bar\": \"5/6\",\n\t\t\t\t})\n\t\t\t\tr.URL = \"https://example.com/{foo}/{bar}\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/4/5%2F6\",\n\t\t},\n\t\t{\n\t\t\tname: \"apply request and client path parameters\",\n\t\t\tinitClient: func(c *Client) {\n\t\t\t\tc.SetPathParams(map[string]string{\n\t\t\t\t\t\"foo\": \"1\", // ignored, because of the request's \"foo\"\n\t\t\t\t\t\"bar\": \"2/3\",\n\t\t\t\t})\n\t\t\t},\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetPathParams(map[string]string{\n\t\t\t\t\t\"foo\": \"4/5\",\n\t\t\t\t})\n\t\t\t\tr.URL = \"https://example.com/{foo}/{bar}\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/4%2F5/2%2F3\",\n\t\t},\n\t\t{\n\t\t\tname: \"apply client raw path parameters\",\n\t\t\tinitClient: func(c *Client) {\n\t\t\t\tc.SetPathRawParams(map[string]string{\n\t\t\t\t\t\"foo\": \"1/2\",\n\t\t\t\t\t\"bar\": \"3\",\n\t\t\t\t})\n\t\t\t},\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.URL = \"https://example.com/{foo}/{bar}\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/1/2/3\",\n\t\t},\n\t\t{\n\t\t\tname: \"apply request raw path parameters\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetPathRawParams(map[string]string{\n\t\t\t\t\t\"foo\": \"4\",\n\t\t\t\t\t\"bar\": \"5/6\",\n\t\t\t\t})\n\t\t\t\tr.URL = \"https://example.com/{foo}/{bar}\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/4/5/6\",\n\t\t},\n\t\t{\n\t\t\tname: \"apply request and client raw path parameters\",\n\t\t\tinitClient: func(c *Client) {\n\t\t\t\tc.SetPathRawParams(map[string]string{\n\t\t\t\t\t\"foo\": \"1\", // ignored, because of the request's \"foo\"\n\t\t\t\t\t\"bar\": \"2/3\",\n\t\t\t\t})\n\t\t\t},\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetPathRawParams(map[string]string{\n\t\t\t\t\t\"foo\": \"4/5\",\n\t\t\t\t})\n\t\t\t\tr.URL = \"https://example.com/{foo}/{bar}\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/4/5/2/3\",\n\t\t},\n\t\t{\n\t\t\tname: \"apply request path and raw path parameters\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetPathParams(map[string]string{\n\t\t\t\t\t\"foo\": \"4/5\",\n\t\t\t\t}).SetPathRawParams(map[string]string{\n\t\t\t\t\t\"foo\": \"4/5\", // it gets overwritten since same key name\n\t\t\t\t\t\"bar\": \"6/7\",\n\t\t\t\t})\n\t\t\t\tr.URL = \"https://example.com/{foo}/{bar}\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/4/5/6/7\",\n\t\t},\n\t\t{\n\t\t\tname: \"empty path parameter in URL\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetPathParams(map[string]string{\n\t\t\t\t\t\"bar\": \"4\",\n\t\t\t\t})\n\t\t\t\tr.URL = \"https://example.com/{}/{bar}\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/%7B%7D/4\",\n\t\t},\n\t\t{\n\t\t\tname: \"not closed path parameter in URL\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetPathParams(map[string]string{\n\t\t\t\t\t\"foo\": \"4\",\n\t\t\t\t})\n\t\t\t\tr.URL = \"https://example.com/{foo}/{bar/1\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/4/%7Bbar/1\",\n\t\t},\n\t\t{\n\t\t\tname: \"extra path parameter in URL\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetPathParams(map[string]string{\n\t\t\t\t\t\"foo\": \"1\",\n\t\t\t\t})\n\t\t\t\tr.URL = \"https://example.com/{foo}/{bar}\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/1/%7Bbar%7D\",\n\t\t},\n\t\t{\n\t\t\tname: \" path parameter with remainder\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetPathParams(map[string]string{\n\t\t\t\t\t\"foo\": \"1\",\n\t\t\t\t})\n\t\t\t\tr.URL = \"https://example.com/{foo}/2\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/1/2\",\n\t\t},\n\t\t{\n\t\t\tname: \"using base url with path param at index 0\",\n\t\t\tinitClient: func(c *Client) {\n\t\t\t\tc.SetBaseURL(\"https://example.com/prefix\")\n\t\t\t},\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetPathParam(\"first\", \"1\").\n\t\t\t\t\tSetPathParam(\"second\", \"2\")\n\t\t\t\tr.URL = \"{first}/{second}\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/prefix/1/2\",\n\t\t},\n\t\t{\n\t\t\tname: \"using BaseURL with absolute URL in request\",\n\t\t\tinitClient: func(c *Client) {\n\t\t\t\tc.SetBaseURL(\"https://foo.bar\") // ignored\n\t\t\t},\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.URL = \"https://example.com/\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/\",\n\t\t},\n\t\t{\n\t\t\tname: \"using BaseURL with relative path in request URL without leading slash\",\n\t\t\tinitClient: func(c *Client) {\n\t\t\t\tc.SetBaseURL(\"https://example.com\")\n\t\t\t},\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.URL = \"foo/bar\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"using BaseURL with relative path in request URL with leading slash\",\n\t\t\tinitClient: func(c *Client) {\n\t\t\t\tc.SetBaseURL(\"https://example.com\")\n\t\t\t},\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.URL = \"/foo/bar\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"using deprecated HostURL with relative path in request URL\",\n\t\t\tinitClient: func(c *Client) {\n\t\t\t\tc.SetBaseURL(\"https://example.com\")\n\t\t\t},\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.URL = \"foo/bar\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"request URL without scheme\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.URL = \"example.com/foo/bar\"\n\t\t\t},\n\t\t\texpectedURL: \"/example.com/foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"BaseURL without scheme\",\n\t\t\tinitClient: func(c *Client) {\n\t\t\t\tc.SetBaseURL(\"example.com\")\n\t\t\t},\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.URL = \"foo/bar\"\n\t\t\t},\n\t\t\texpectedURL: \"example.com/foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"using SetScheme and BaseURL without scheme\",\n\t\t\tinitClient: func(c *Client) {\n\t\t\t\tc.SetBaseURL(\"example.com\").\n\t\t\t\t\tSetScheme(\"https\")\n\t\t\t},\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.URL = \"foo/bar\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/foo/bar\",\n\t\t},\n\t\t{\n\t\t\tname: \"adding query parameters by client\",\n\t\t\tinitClient: func(c *Client) {\n\t\t\t\tc.SetQueryParams(map[string]string{\n\t\t\t\t\t\"foo\": \"1\",\n\t\t\t\t\t\"bar\": \"2\",\n\t\t\t\t})\n\t\t\t},\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.URL = \"https://example.com/\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/?foo=1&bar=2\",\n\t\t},\n\t\t{\n\t\t\tname: \"adding query parameters by request\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\t\"foo\": \"1\",\n\t\t\t\t\t\"bar\": \"2\",\n\t\t\t\t})\n\t\t\t\tr.URL = \"https://example.com/\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/?foo=1&bar=2\",\n\t\t},\n\t\t{\n\t\t\tname: \"adding query parameters by client and request\",\n\t\t\tinitClient: func(c *Client) {\n\t\t\t\tc.SetQueryParams(map[string]string{\n\t\t\t\t\t\"foo\": \"1\", // ignored, because of the \"foo\" parameter in request\n\t\t\t\t\t\"bar\": \"2\",\n\t\t\t\t})\n\t\t\t},\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\t\"foo\": \"3\",\n\t\t\t\t})\n\t\t\t\tr.URL = \"https://example.com/\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/?foo=3&bar=2\",\n\t\t},\n\t\t{\n\t\t\tname: \"adding query parameters by request to URL with existent\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetQueryParams(map[string]string{\n\t\t\t\t\t\"bar\": \"2\",\n\t\t\t\t})\n\t\t\t\tr.URL = \"https://example.com/?foo=1\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/?foo=1&bar=2\",\n\t\t},\n\t\t{\n\t\t\tname: \"adding query parameters by request with multiple values\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.QueryParams.Add(\"foo\", \"1\")\n\t\t\t\tr.QueryParams.Add(\"foo\", \"2\")\n\t\t\t\tr.URL = \"https://example.com/\"\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com/?foo=1&foo=2\",\n\t\t},\n\t\t{\n\t\t\tname: \"unescape query params\",\n\t\t\tinitClient: func(c *Client) {\n\t\t\t\tc.SetBaseURL(\"https://example.com/\").\n\t\t\t\t\tSetQueryParamsUnescape(true). // this line is just code coverage; I will restructure this test in v3 for the client and request the respective init method\n\t\t\t\t\tSetQueryParam(\"fromclient\", \"hey unescape\").\n\t\t\t\t\tSetQueryParam(\"initone\", \"cáfe\")\n\t\t\t},\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetQueryParamsUnescape(true) // this line takes effect\n\t\t\t\tr.SetQueryParams(\n\t\t\t\t\tmap[string]string{\n\t\t\t\t\t\t\"registry\": \"nacos://test:6801\", // GH #797\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t},\n\t\t\texpectedURL: \"https://example.com?initone=cáfe&fromclient=hey+unescape&registry=nacos://test:6801\",\n\t\t},\n\t} {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := New()\n\t\t\tif tt.initClient != nil {\n\t\t\t\ttt.initClient(c)\n\t\t\t}\n\n\t\t\tr := c.R()\n\t\t\tif tt.initRequest != nil {\n\t\t\t\ttt.initRequest(r)\n\t\t\t}\n\t\t\tif err := parseRequestURL(c, r); err != nil {\n\t\t\t\tt.Errorf(\"parseRequestURL() error = %v\", err)\n\t\t\t}\n\n\t\t\t// compare URLs without query parameters first\n\t\t\t// then compare query parameters, because the order of the items in a map is not guarantied\n\t\t\texpectedURL, _ := url.Parse(tt.expectedURL)\n\t\t\texpectedQuery := expectedURL.Query()\n\t\t\texpectedURL.RawQuery = \"\"\n\t\t\tactualURL, _ := url.Parse(r.URL)\n\t\t\tactualQuery := actualURL.Query()\n\t\t\tactualURL.RawQuery = \"\"\n\t\t\tif expectedURL.String() != actualURL.String() {\n\t\t\t\tt.Errorf(\"r.URL = %q does not match expected %q\", r.URL, tt.expectedURL)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(expectedQuery, actualQuery) {\n\t\t\t\tt.Errorf(\"r.URL = %q does not match expected %q\", r.URL, tt.expectedURL)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc Test_parseRequestHeader(t *testing.T) {\n\tfor _, tt := range []struct {\n\t\tname           string\n\t\tinit           func(c *Client, r *Request)\n\t\texpectedHeader http.Header\n\t}{\n\t\t{\n\t\t\tname: \"headers in request\",\n\t\t\tinit: func(c *Client, r *Request) {\n\t\t\t\tr.SetHeaders(map[string]string{\n\t\t\t\t\t\"foo\": \"1\",\n\t\t\t\t\t\"bar\": \"2\",\n\t\t\t\t})\n\t\t\t},\n\t\t\texpectedHeader: http.Header{\n\t\t\t\thttp.CanonicalHeaderKey(\"foo\"): []string{\"1\"},\n\t\t\t\thttp.CanonicalHeaderKey(\"bar\"): []string{\"2\"},\n\t\t\t\thdrUserAgentKey:                []string{hdrUserAgentValue},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"headers in client\",\n\t\t\tinit: func(c *Client, r *Request) {\n\t\t\t\tc.SetHeaders(map[string]string{\n\t\t\t\t\t\"foo\": \"1\",\n\t\t\t\t\t\"bar\": \"2\",\n\t\t\t\t})\n\t\t\t},\n\t\t\texpectedHeader: http.Header{\n\t\t\t\thttp.CanonicalHeaderKey(\"foo\"): []string{\"1\"},\n\t\t\t\thttp.CanonicalHeaderKey(\"bar\"): []string{\"2\"},\n\t\t\t\thdrUserAgentKey:                []string{hdrUserAgentValue},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"headers in client and request\",\n\t\t\tinit: func(c *Client, r *Request) {\n\t\t\t\tc.SetHeaders(map[string]string{\n\t\t\t\t\t\"foo\": \"1\", // ignored, because of the same header in the request\n\t\t\t\t\t\"bar\": \"2\",\n\t\t\t\t})\n\t\t\t\tr.SetHeaders(map[string]string{\n\t\t\t\t\t\"foo\": \"3\",\n\t\t\t\t\t\"xyz\": \"4\",\n\t\t\t\t})\n\t\t\t},\n\t\t\texpectedHeader: http.Header{\n\t\t\t\thttp.CanonicalHeaderKey(\"foo\"): []string{\"3\"},\n\t\t\t\thttp.CanonicalHeaderKey(\"bar\"): []string{\"2\"},\n\t\t\t\thttp.CanonicalHeaderKey(\"xyz\"): []string{\"4\"},\n\t\t\t\thdrUserAgentKey:                []string{hdrUserAgentValue},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"no headers\",\n\t\t\tinit: func(c *Client, r *Request) {},\n\t\t\texpectedHeader: http.Header{\n\t\t\t\thdrUserAgentKey: []string{hdrUserAgentValue},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"user agent\",\n\t\t\tinit: func(c *Client, r *Request) {\n\t\t\t\tc.SetHeader(hdrUserAgentKey, \"foo bar\")\n\t\t\t},\n\t\t\texpectedHeader: http.Header{\n\t\t\t\thttp.CanonicalHeaderKey(hdrUserAgentKey): []string{\"foo bar\"},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"json content type\",\n\t\t\tinit: func(c *Client, r *Request) {\n\t\t\t\tc.SetHeader(hdrContentTypeKey, \"application/json\")\n\t\t\t},\n\t\t\texpectedHeader: http.Header{\n\t\t\t\thdrContentTypeKey: []string{\"application/json\"},\n\t\t\t\thdrUserAgentKey:   []string{hdrUserAgentValue},\n\t\t\t},\n\t\t},\n\t} {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := New()\n\t\t\tr := c.R()\n\t\t\ttt.init(c, r)\n\n\t\t\t// add common expected headers from client into expectedHeader\n\t\t\ttt.expectedHeader.Set(hdrAcceptEncodingKey, c.ContentDecompresserKeys())\n\n\t\t\tif err := parseRequestHeader(c, r); err != nil {\n\t\t\t\tt.Errorf(\"parseRequestHeader() error = %v\", err)\n\t\t\t}\n\t\t\tif !reflect.DeepEqual(tt.expectedHeader, r.Header) {\n\t\t\t\tt.Errorf(\"r.Header = %#+v does not match expected %#+v\", r.Header, tt.expectedHeader)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestParseRequestBody(t *testing.T) {\n\tfor _, tt := range []struct {\n\t\tname                  string\n\t\tinitClient            func(c *Client)\n\t\tinitRequest           func(r *Request)\n\t\texpectedBodyBuf       []byte\n\t\texpectedContentLength int64\n\t\texpectedContentType   string\n\t\twantErr               bool\n\t}{\n\t\t{\n\t\t\tname: \"empty body\",\n\t\t},\n\t\t{\n\t\t\tname:                  \"empty body with SetContentLength by request\",\n\t\t\texpectedContentLength: 0,\n\t\t},\n\t\t{\n\t\t\tname: \"string body\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetMethod(MethodPost).\n\t\t\t\t\tSetBody(\"foo\")\n\t\t\t},\n\t\t\texpectedBodyBuf:       []byte(\"foo\"),\n\t\t\texpectedContentType:   plainTextType,\n\t\t\texpectedContentLength: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"string body with GET method\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetBody(\"foo\")\n\t\t\t\tr.Method = http.MethodGet\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"string body with GET method and AllowMethodGetPayload by client\",\n\t\t\tinitClient: func(c *Client) {\n\t\t\t\tc.SetMethodGetAllowPayload(true)\n\t\t\t},\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetBody(\"foo\")\n\t\t\t\tr.Method = http.MethodGet\n\t\t\t},\n\t\t\texpectedBodyBuf:       []byte(\"foo\"),\n\t\t\texpectedContentType:   plainTextType,\n\t\t\texpectedContentLength: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"string body with GET method and AllowMethodGetPayload by request\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetMethodGetAllowPayload(true)\n\t\t\t\tr.SetBody(\"foo\")\n\t\t\t\tr.Method = http.MethodGet\n\t\t\t},\n\t\t\texpectedBodyBuf:       []byte(\"foo\"),\n\t\t\texpectedContentType:   plainTextType,\n\t\t\texpectedContentLength: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"string body with HEAD method\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetBody(\"foo\")\n\t\t\t\tr.Method = http.MethodHead\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"string body with OPTIONS method\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetBody(\"foo\")\n\t\t\t\tr.Method = http.MethodOptions\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"string body with POST method\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetBody(\"foo\")\n\t\t\t\tr.Method = http.MethodPost\n\t\t\t},\n\t\t\texpectedBodyBuf:       []byte(\"foo\"),\n\t\t\texpectedContentType:   plainTextType,\n\t\t\texpectedContentLength: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"string body with PATCH method\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetBody(\"foo\")\n\t\t\t\tr.Method = http.MethodPatch\n\t\t\t},\n\t\t\texpectedBodyBuf:       []byte(\"foo\"),\n\t\t\texpectedContentType:   plainTextType,\n\t\t\texpectedContentLength: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"string body with PUT method\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetBody(\"foo\")\n\t\t\t\tr.Method = http.MethodPut\n\t\t\t},\n\t\t\texpectedBodyBuf:       []byte(\"foo\"),\n\t\t\texpectedContentType:   plainTextType,\n\t\t\texpectedContentLength: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"string body with DELETE method\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetBody(\"foo\")\n\t\t\t\tr.Method = http.MethodDelete\n\t\t\t},\n\t\t\texpectedBodyBuf:     nil,\n\t\t\texpectedContentType: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"string body with DELETE method with AllowMethodDeletePayload by request\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetMethodDeleteAllowPayload(true)\n\t\t\t\tr.SetBody(\"foo\")\n\t\t\t\tr.Method = http.MethodDelete\n\t\t\t},\n\t\t\texpectedBodyBuf:       []byte(\"foo\"),\n\t\t\texpectedContentType:   plainTextType,\n\t\t\texpectedContentLength: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"string body with CONNECT method\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetBody(\"foo\")\n\t\t\t\tr.Method = http.MethodConnect\n\t\t\t},\n\t\t\texpectedBodyBuf:     nil,\n\t\t\texpectedContentType: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"string body with TRACE method\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetBody(\"foo\")\n\t\t\t\tr.Method = http.MethodTrace\n\t\t\t},\n\t\t\texpectedBodyBuf:     nil,\n\t\t\texpectedContentType: \"\",\n\t\t},\n\t\t{\n\t\t\tname: \"byte body with method post\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetMethod(MethodPost).\n\t\t\t\t\tSetBody([]byte(\"foo\"))\n\t\t\t},\n\t\t\texpectedBodyBuf:       []byte(\"foo\"),\n\t\t\texpectedContentType:   plainTextType,\n\t\t\texpectedContentLength: 3,\n\t\t},\n\t\t{\n\t\t\tname: \"io.Reader body, no bodyBuf with method put\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetMethod(MethodPut).\n\t\t\t\t\tSetBody(bytes.NewBufferString(\"foo\"))\n\t\t\t},\n\t\t\texpectedContentType: jsonContentType,\n\t\t},\n\t\t{\n\t\t\tname: \"form data by request with method post\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetMethod(MethodPost).\n\t\t\t\t\tSetFormData(map[string]string{\n\t\t\t\t\t\t\"foo\": \"1\",\n\t\t\t\t\t\t\"bar\": \"2\",\n\t\t\t\t\t})\n\t\t\t},\n\t\t\texpectedBodyBuf:       []byte(\"foo=1&bar=2\"),\n\t\t\texpectedContentType:   formContentType,\n\t\t\texpectedContentLength: 11,\n\t\t},\n\t\t{\n\t\t\tname: \"form data by client with method patch\",\n\t\t\tinitClient: func(c *Client) {\n\t\t\t\tc.SetFormData(map[string]string{\n\t\t\t\t\t\"foo\": \"1\",\n\t\t\t\t\t\"bar\": \"2\",\n\t\t\t\t})\n\t\t\t},\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetMethod(MethodPatch)\n\t\t\t},\n\t\t\texpectedBodyBuf:       []byte(\"foo=1&bar=2\"),\n\t\t\texpectedContentType:   formContentType,\n\t\t\texpectedContentLength: 11,\n\t\t},\n\t\t{\n\t\t\tname: \"form data by client and request\",\n\t\t\tinitClient: func(c *Client) {\n\t\t\t\tc.SetFormData(map[string]string{\n\t\t\t\t\t\"foo\": \"1\",\n\t\t\t\t\t\"bar\": \"2\",\n\t\t\t\t})\n\t\t\t},\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetMethod(MethodPatch).\n\t\t\t\t\tSetFormData(map[string]string{\n\t\t\t\t\t\t\"foo\": \"3\",\n\t\t\t\t\t\t\"baz\": \"4\",\n\t\t\t\t\t})\n\t\t\t},\n\t\t\texpectedBodyBuf:       []byte(\"foo=3&bar=2&baz=4\"),\n\t\t\texpectedContentType:   formContentType,\n\t\t\texpectedContentLength: 17,\n\t\t},\n\t\t{\n\t\t\tname: \"json from struct\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetMethod(MethodPut)\n\t\t\t\tr.SetBody(struct {\n\t\t\t\t\tFoo string `json:\"foo\"`\n\t\t\t\t\tBar string `json:\"bar\"`\n\t\t\t\t}{\n\t\t\t\t\tFoo: \"1\",\n\t\t\t\t\tBar: \"2\",\n\t\t\t\t})\n\t\t\t},\n\t\t\texpectedBodyBuf:       append([]byte(`{\"foo\":\"1\",\"bar\":\"2\"}`), '\\n'),\n\t\t\texpectedContentType:   jsonContentType,\n\t\t\texpectedContentLength: 22,\n\t\t},\n\t\t{\n\t\t\tname: \"json from slice\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetMethod(MethodPost).\n\t\t\t\t\tSetBody([]string{\"foo\", \"bar\"})\n\t\t\t},\n\t\t\texpectedBodyBuf:       append([]byte(`[\"foo\",\"bar\"]`), '\\n'),\n\t\t\texpectedContentType:   jsonContentType,\n\t\t\texpectedContentLength: 14,\n\t\t},\n\t\t{\n\t\t\tname: \"json from map\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetMethod(MethodPost).\n\t\t\t\t\tSetBody(map[string]any{\n\t\t\t\t\t\t\"foo\": \"1\",\n\t\t\t\t\t\t\"bar\": []int{1, 2, 3},\n\t\t\t\t\t\t\"baz\": map[string]string{\n\t\t\t\t\t\t\t\"qux\": \"4\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"xyz\": nil,\n\t\t\t\t\t})\n\t\t\t},\n\t\t\texpectedBodyBuf:       append([]byte(`{\"bar\":[1,2,3],\"baz\":{\"qux\":\"4\"},\"foo\":\"1\",\"xyz\":null}`), '\\n'),\n\t\t\texpectedContentType:   jsonContentType,\n\t\t\texpectedContentLength: 55,\n\t\t},\n\t\t{\n\t\t\tname: \"json from map\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetMethod(MethodPut).\n\t\t\t\t\tSetBody(map[string]any{\n\t\t\t\t\t\t\"foo\": \"1\",\n\t\t\t\t\t\t\"bar\": []int{1, 2, 3},\n\t\t\t\t\t\t\"baz\": map[string]string{\n\t\t\t\t\t\t\t\"qux\": \"4\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"xyz\": nil,\n\t\t\t\t\t})\n\t\t\t},\n\t\t\texpectedBodyBuf:       append([]byte(`{\"bar\":[1,2,3],\"baz\":{\"qux\":\"4\"},\"foo\":\"1\",\"xyz\":null}`), '\\n'),\n\t\t\texpectedContentType:   jsonContentType,\n\t\t\texpectedContentLength: 55,\n\t\t},\n\t\t{\n\t\t\tname: \"json from map\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetMethod(MethodPost).\n\t\t\t\t\tSetBody(map[string]any{\n\t\t\t\t\t\t\"foo\": \"1\",\n\t\t\t\t\t\t\"bar\": []int{1, 2, 3},\n\t\t\t\t\t\t\"baz\": map[string]string{\n\t\t\t\t\t\t\t\"qux\": \"4\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"xyz\": nil,\n\t\t\t\t\t})\n\t\t\t},\n\t\t\texpectedBodyBuf:       append([]byte(`{\"bar\":[1,2,3],\"baz\":{\"qux\":\"4\"},\"foo\":\"1\",\"xyz\":null}`), '\\n'),\n\t\t\texpectedContentType:   jsonContentType,\n\t\t\texpectedContentLength: 55,\n\t\t},\n\t\t{\n\t\t\tname: \"xml from struct\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\ttype FooBar struct {\n\t\t\t\t\tFoo string `xml:\"foo\"`\n\t\t\t\t\tBar string `xml:\"bar\"`\n\t\t\t\t}\n\t\t\t\tr.SetMethod(MethodPatch).\n\t\t\t\t\tSetBody(FooBar{\n\t\t\t\t\t\tFoo: \"1\",\n\t\t\t\t\t\tBar: \"2\",\n\t\t\t\t\t}).\n\t\t\t\t\tSetHeader(hdrContentTypeKey, \"text/xml\")\n\t\t\t},\n\t\t\texpectedBodyBuf:       []byte(`<FooBar><foo>1</foo><bar>2</bar></FooBar>`),\n\t\t\texpectedContentType:   \"text/xml\",\n\t\t\texpectedContentLength: 41,\n\t\t},\n\t\t{\n\t\t\tname: \"unsupported type\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetMethod(MethodPost).\n\t\t\t\t\tSetBody(1)\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t\t{\n\t\t\tname: \"unsupported xml\",\n\t\t\tinitRequest: func(r *Request) {\n\t\t\t\tr.SetMethod(MethodPut).\n\t\t\t\t\tSetBody(struct {\n\t\t\t\t\t\tFoo string `xml:\"foo\"`\n\t\t\t\t\t\tBar string `xml:\"bar\"`\n\t\t\t\t\t}{\n\t\t\t\t\t\tFoo: \"1\",\n\t\t\t\t\t\tBar: \"2\",\n\t\t\t\t\t}).\n\t\t\t\t\tSetHeader(hdrContentTypeKey, \"text/xml\")\n\t\t\t},\n\t\t\twantErr: true,\n\t\t},\n\t} {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tc := New()\n\t\t\tif tt.initClient != nil {\n\t\t\t\ttt.initClient(c)\n\t\t\t}\n\n\t\t\tr := c.R()\n\t\t\tif tt.initRequest != nil {\n\t\t\t\ttt.initRequest(r)\n\t\t\t}\n\n\t\t\tif err := parseRequestBody(c, r); err != nil {\n\t\t\t\tif tt.wantErr {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tt.Errorf(\"parseRequestBody() error = %v\", err)\n\t\t\t} else if tt.wantErr {\n\t\t\t\tt.Errorf(\"wanted error, but got nil\")\n\t\t\t}\n\t\t\t// obtain value, since this is only parse request body method test\n\t\t\tif r.bodyBuf != nil {\n\t\t\t\tr.contentLength = int64(r.bodyBuf.Len())\n\t\t\t}\n\t\t\tswitch {\n\t\t\tcase r.bodyBuf == nil && tt.expectedBodyBuf != nil:\n\t\t\t\tt.Errorf(\"bodyBuf is nil, but expected: %s\", string(tt.expectedBodyBuf))\n\t\t\tcase r.bodyBuf != nil && tt.expectedBodyBuf == nil:\n\t\t\t\tt.Errorf(\"bodyBuf is not nil, but expected nil: %s\", r.bodyBuf.String())\n\t\t\tcase r.bodyBuf != nil && tt.expectedBodyBuf != nil:\n\t\t\t\tvar actual, expected any = r.bodyBuf.Bytes(), tt.expectedBodyBuf\n\t\t\t\tif r.isFormData {\n\t\t\t\t\tvar err error\n\t\t\t\t\tactual, err = url.ParseQuery(r.bodyBuf.String())\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Errorf(\"ParseQuery(r.bodyBuf) error = %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\texpected, err = url.ParseQuery(string(tt.expectedBodyBuf))\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Errorf(\"ParseQuery(tt.expectedBodyBuf) error = %v\", err)\n\t\t\t\t\t}\n\t\t\t\t} else if r.isMultiPart {\n\t\t\t\t\t_, params, err := mime.ParseMediaType(r.Header.Get(hdrContentTypeKey))\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Errorf(\"ParseMediaType(hdrContentTypeKey) error = %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tboundary, ok := params[\"boundary\"]\n\t\t\t\t\tif !ok {\n\t\t\t\t\t\tt.Errorf(\"boundary not found in Content-Type header\")\n\t\t\t\t\t}\n\t\t\t\t\treader := multipart.NewReader(r.bodyBuf, boundary)\n\t\t\t\t\tbody := make(map[string]any)\n\t\t\t\t\tfor part, perr := reader.NextPart(); perr != io.EOF; part, perr = reader.NextPart() {\n\t\t\t\t\t\tif perr != nil {\n\t\t\t\t\t\t\tt.Errorf(\"NextPart() error = %v\", perr)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tname := part.FormName()\n\t\t\t\t\t\tif name == \"\" {\n\t\t\t\t\t\t\tname = part.FileName()\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdata, err := io.ReadAll(part)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tt.Errorf(\"ReadAll(part) error = %v\", err)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tbody[name] = string(data)\n\t\t\t\t\t}\n\t\t\t\t\tactual = body\n\t\t\t\t\texpected = nil\n\t\t\t\t\tif err := json.Unmarshal(tt.expectedBodyBuf, &expected); err != nil {\n\t\t\t\t\t\tt.Errorf(\"json.Unmarshal(tt.expectedBodyBuf) error = %v\", err)\n\t\t\t\t\t}\n\t\t\t\t\tt.Logf(`in case of an error, the expected body should be set as json for object: %#+v`, actual)\n\t\t\t\t}\n\t\t\t\tif !reflect.DeepEqual(actual, expected) {\n\t\t\t\t\tt.Errorf(\"bodyBuf = %q does not match expected %q\", r.bodyBuf.String(), string(tt.expectedBodyBuf))\n\t\t\t\t}\n\t\t\t}\n\t\t\tif tt.expectedContentLength != r.contentLength {\n\t\t\t\tt.Errorf(\"Content length value = %v does not match expected %v\", r.contentLength, tt.expectedContentLength)\n\t\t\t}\n\t\t\tif ct := r.Header.Get(hdrContentTypeKey); !((tt.expectedContentType == \"\" && ct != \"\") || strings.Contains(ct, tt.expectedContentType)) {\n\t\t\t\tt.Errorf(\"Content-Type header = %q does not match expected %q\", r.Header.Get(hdrContentTypeKey), tt.expectedContentType)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestMiddlewareSaveToFileErrorCases(t *testing.T) {\n\tc := dcnl()\n\ttempDir := t.TempDir()\n\n\terrDirMsg := \"test dir error\"\n\tmkdirAll = func(_ string, _ os.FileMode) error {\n\t\treturn errors.New(errDirMsg)\n\t}\n\terrFileMsg := \"test file error\"\n\tcreateFile = func(_ string) (*os.File, error) {\n\t\treturn nil, errors.New(errFileMsg)\n\t}\n\tt.Cleanup(func() {\n\t\tmkdirAll = os.MkdirAll\n\t\tcreateFile = os.Create\n\t})\n\n\t// dir create error\n\treq1 := c.R()\n\treq1.SetResponseSaveFileName(filepath.Join(tempDir, \"new-res-dir\", \"sample.txt\"))\n\terr1 := MiddlewareResponseSaveToFile(c, &Response{Request: req1})\n\tassertEqual(t, errDirMsg, err1.Error())\n\n\t// file create error\n\treq2 := c.R()\n\treq2.SetResponseSaveFileName(filepath.Join(tempDir, \"sample.txt\"))\n\terr2 := MiddlewareResponseSaveToFile(c, &Response{Request: req2})\n\tassertEqual(t, errFileMsg, err2.Error())\n}\n\nfunc TestMiddlewareSaveToFileCopyError(t *testing.T) {\n\tc := dcnl()\n\ttempDir := t.TempDir()\n\n\terrCopyMsg := \"test copy error\"\n\tioCopy = func(dst io.Writer, src io.Reader) (written int64, err error) {\n\t\treturn 0, errors.New(errCopyMsg)\n\t}\n\tt.Cleanup(func() {\n\t\tioCopy = io.Copy\n\t})\n\n\t// copy error\n\treq1 := c.R()\n\treq1.SetResponseSaveFileName(filepath.Join(tempDir, \"new-res-dir\", \"sample.txt\"))\n\terr1 := MiddlewareResponseSaveToFile(c, &Response{Request: req1, Body: io.NopCloser(bytes.NewBufferString(\"Test context\"))})\n\tassertEqual(t, errCopyMsg, err1.Error())\n}\n\nfunc TestRequestURL_GH797(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\tc := dcnl().\n\t\tSetBaseURL(ts.URL).\n\t\tSetQueryParamsUnescape(true). // this line is just code coverage; I will restructure this test in v3 for the client and request the respective init method\n\t\tSetQueryParam(\"fromclient\", \"hey unescape\").\n\t\tSetQueryParam(\"initone\", \"cáfe\")\n\tresp, err := c.R().\n\t\tSetQueryParamsUnescape(true). // this line takes effect\n\t\tSetQueryParams(\n\t\t\tmap[string]string{\n\t\t\t\t\"registry\": \"nacos://test:6801\", // GH #797\n\t\t\t},\n\t\t).\n\t\tGet(\"/unescape-query-params\")\n\tassertError(t, err)\n\tassertEqual(t, \"query params looks good\", resp.String())\n}\n\nfunc TestMiddleware_multipartWriteFormData(t *testing.T) {\n\tc := dcnl()\n\n\toldFunc := multipartWriteFormData\n\terrMsg := \"test write form data error\"\n\tmultipartWriteFormData = func(*multipart.Writer, *Request) error {\n\t\treturn errors.New(errMsg)\n\t}\n\tt.Cleanup(func() {\n\t\tmultipartWriteFormData = oldFunc\n\t})\n\n\treq := &Request{\n\t\tHeader:      http.Header{},\n\t\tisMultiPart: true,\n\t\tmultipartFields: []*MultipartField{\n\t\t\t{\n\t\t\t\tName:   \"field1\",\n\t\t\t\tValues: []string{\"field1value1\", \"field1value2\"},\n\t\t\t},\n\t\t},\n\t}\n\terr := handleMultipart(c, req)\n\tassertNil(t, err)\n\n\terr = <-req.multipartErrChan\n\tassertNotNil(t, err)\n\tassertEqual(t, errMsg, err.Error())\n}\n\nfunc TestMiddleware_multipartWriteField(t *testing.T) {\n\tc := dcnl()\n\n\toldFunc := multipartWriteField\n\terrMsg := \"test write field error\"\n\tmultipartWriteField = func(w *multipart.Writer, name, value string) error {\n\t\treturn errors.New(errMsg)\n\t}\n\tt.Cleanup(func() {\n\t\tmultipartWriteField = oldFunc\n\t})\n\n\treq := &Request{\n\t\tmu:          new(sync.Mutex),\n\t\tHeader:      http.Header{},\n\t\tisMultiPart: true,\n\t\tmultipartFields: []*MultipartField{\n\t\t\t{\n\t\t\t\tName:   \"field1\",\n\t\t\t\tValues: []string{\"field1value1\", \"field1value2\"},\n\t\t\t},\n\t\t},\n\t}\n\terr := handleMultipart(c, req)\n\tassertNil(t, err)\n\n\terr = <-req.multipartErrChan\n\tassertNotNil(t, err)\n\tassertEqual(t, errMsg, err.Error())\n}\n\nfunc TestMiddleware_multipartCreatePart(t *testing.T) {\n\tc := dcnl()\n\n\toldFunc := multipartCreatePart\n\terrMsg := \"test create part error\"\n\tmultipartCreatePart = func(w *multipart.Writer, h textproto.MIMEHeader) (io.Writer, error) {\n\t\treturn nil, errors.New(errMsg)\n\t}\n\tt.Cleanup(func() {\n\t\tmultipartCreatePart = oldFunc\n\t})\n\n\tjsonStr1 := `{\"input\": {\"name\": \"Uploaded document 1\", \"_filename\" : [\"file1.txt\"]}}`\n\treq := &Request{\n\t\tmu:          new(sync.Mutex),\n\t\tHeader:      http.Header{},\n\t\tisMultiPart: true,\n\t\tmultipartFields: []*MultipartField{\n\t\t\t{\n\t\t\t\tName:        \"uploadManifest1\",\n\t\t\t\tFileName:    \"upload-file-1.json\",\n\t\t\t\tContentType: \"application/json\",\n\t\t\t\tReader:      bytes.NewBufferString(jsonStr1),\n\t\t\t},\n\t\t},\n\t}\n\terr := handleMultipart(c, req)\n\tassertNil(t, err)\n\n\terr = <-req.multipartErrChan\n\tassertNotNil(t, err)\n\tassertEqual(t, errMsg, err.Error())\n}\n\nfunc TestMiddleware_multipartCreatePart_WriteError(t *testing.T) {\n\tc := dcnl()\n\n\toldFunc := multipartCreatePart\n\tmultipartCreatePart = func(w *multipart.Writer, h textproto.MIMEHeader) (io.Writer, error) {\n\t\treturn &mpWriterError{}, nil\n\t}\n\tt.Cleanup(func() {\n\t\tmultipartCreatePart = oldFunc\n\t})\n\n\tjsonStr1 := `{\"input\": {\"name\": \"Uploaded document 1\", \"_filename\" : [\"file1.txt\"]}}`\n\treq := &Request{\n\t\tmu:          new(sync.Mutex),\n\t\tHeader:      http.Header{},\n\t\tisMultiPart: true,\n\t\tmultipartFields: []*MultipartField{\n\t\t\t{\n\t\t\t\tName:        \"uploadManifest1\",\n\t\t\t\tFileName:    \"upload-file-1.json\",\n\t\t\t\tContentType: \"application/json\",\n\t\t\t\tReader:      bytes.NewBufferString(jsonStr1),\n\t\t\t\ttempBuf:     []byte(\"test data\"),\n\t\t\t},\n\t\t},\n\t}\n\terr := handleMultipart(c, req)\n\tassertNil(t, err)\n\n\terr = <-req.multipartErrChan\n\tassertNotNil(t, err)\n\tassertEqual(t, \"multipart write error\", err.Error())\n}\n\nfunc TestMiddlewareCoverage(t *testing.T) {\n\tc := dcnl()\n\n\treq1 := c.R()\n\treq1.URL = \"//invalid-url  .local\"\n\terr1 := createRawRequest(c, req1)\n\tassertTrue(t, strings.Contains(err1.Error(), \"invalid character\"), \"invalid URL error expected\")\n}\n"
  },
  {
    "path": "multipart.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/textproto\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\nvar quoteEscaper = strings.NewReplacer(\"\\\\\", \"\\\\\\\\\", `\"`, \"\\\\\\\"\")\n\nfunc escapeQuotes(s string) string {\n\treturn quoteEscaper.Replace(s)\n}\n\n// MultipartField struct represents the multipart field to compose\n// all [io.Reader] capable input for multipart form request\ntype MultipartField struct {\n\t// Name of the multipart field name that the server expects it\n\tName string\n\n\t// FileName is used to set the file name we have to send to the server\n\tFileName string\n\n\t// ContentType is a multipart file content-type value. It is highly\n\t// recommended setting it if you know the content-type so that Resty\n\t// don't have to do additional computing to auto-detect (Optional)\n\tContentType string\n\n\t// Reader is an input of [io.Reader] for multipart upload. It\n\t// is optional if you set the FilePath value\n\tReader io.Reader\n\n\t// FilePath is a file path for multipart upload. It\n\t// is optional if you set the Reader value\n\tFilePath string\n\n\t// FileSize in bytes is used just for the information purpose of\n\t// sharing via [MultipartFieldCallbackFunc] (Optional)\n\tFileSize int64\n\n\t// ProgressCallback function is used to provide live progress details\n\t// during a multipart upload (Optional)\n\t//\n\t// NOTE: It is recommended to set the FileSize value when using `MultipartField.Reader`\n\t// with `ProgressCallback` feature so that Resty sends the FileSize\n\t// value via [MultipartFieldProgress]\n\tProgressCallback MultipartFieldCallbackFunc\n\n\t// Values field is used to provide form field value. (Optional, unless it's a form-data field)\n\t//\n\t// It is primarily added for ordered multipart form-data field use cases\n\tValues []string\n\n\t// tempBuf is used to preserve the byte(s) read from the file to detect the content type.\n\t// Or any possible read error early.\n\ttempBuf []byte\n}\n\n// Clone method returns the deep copy of m except [io.Reader].\nfunc (mf *MultipartField) Clone() *MultipartField {\n\tmf2 := new(MultipartField)\n\t*mf2 = *mf\n\treturn mf2\n}\n\nfunc (mf *MultipartField) resetReader() error {\n\tif rs, ok := mf.Reader.(io.ReadSeeker); ok {\n\t\t_, err := rs.Seek(0, io.SeekStart)\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (mf *MultipartField) isValues() bool {\n\treturn len(mf.Values) > 0\n}\n\nfunc (mf *MultipartField) close() {\n\tcloseq(mf.Reader)\n}\n\nfunc (mf *MultipartField) createHeader() textproto.MIMEHeader {\n\th := make(textproto.MIMEHeader)\n\tif isStringEmpty(mf.FileName) {\n\t\th.Set(hdrContentDisposition,\n\t\t\tfmt.Sprintf(`form-data; name=\"%s\"`, escapeQuotes(mf.Name)))\n\t} else {\n\t\th.Set(hdrContentDisposition,\n\t\t\tfmt.Sprintf(`form-data; name=\"%s\"; filename=\"%s\"`,\n\t\t\t\tescapeQuotes(mf.Name), escapeQuotes(mf.FileName)))\n\t}\n\tif !isStringEmpty(mf.ContentType) {\n\t\th.Set(hdrContentTypeKey, mf.ContentType)\n\t}\n\treturn h\n}\n\nfunc (mf *MultipartField) openFile() error {\n\tif isStringEmpty(mf.FilePath) || mf.Reader != nil {\n\t\treturn nil\n\t}\n\n\tfile, err := os.Open(mf.FilePath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif isStringEmpty(mf.FileName) {\n\t\tmf.FileName = filepath.Base(mf.FilePath)\n\t}\n\n\t// if file open is success, stat will succeed\n\tfileStat, _ := file.Stat()\n\n\tmf.Reader = file\n\tmf.FileSize = fileStat.Size()\n\n\treturn nil\n}\n\nfunc (mf *MultipartField) detectContentType() error {\n\tif !isStringEmpty(mf.ContentType) || mf.Reader == nil {\n\t\treturn nil\n\t}\n\n\tp := make([]byte, 512)\n\tsize, err := mf.Reader.Read(p)\n\tif err != nil && err != io.EOF {\n\t\treturn err\n\t}\n\tmf.tempBuf = p[:size]\n\tmf.ContentType = http.DetectContentType(mf.tempBuf)\n\treturn nil\n}\n\nfunc (mf *MultipartField) wrapProgressCallbackIfPresent(pw io.Writer) io.Writer {\n\tif mf.ProgressCallback == nil {\n\t\treturn pw\n\t}\n\n\treturn &multipartProgressWriter{\n\t\tw: pw,\n\t\tf: func(pb int64) {\n\t\t\tmf.ProgressCallback(MultipartFieldProgress{\n\t\t\t\tName:     mf.Name,\n\t\t\t\tFileName: mf.FileName,\n\t\t\t\tFileSize: mf.FileSize,\n\t\t\t\tWritten:  pb,\n\t\t\t})\n\t\t},\n\t}\n}\n\n// MultipartFieldCallbackFunc function used to transmit live multipart upload\n// progress in bytes count\ntype MultipartFieldCallbackFunc func(MultipartFieldProgress)\n\n// MultipartFieldProgress struct used to provide multipart field upload progress\n// details via callback function\ntype MultipartFieldProgress struct {\n\tName     string\n\tFileName string\n\tFileSize int64\n\tWritten  int64\n}\n\n// String method creates the string representation of [MultipartFieldProgress]\nfunc (mfp MultipartFieldProgress) String() string {\n\treturn fmt.Sprintf(\"FieldName: %s, FileName: %s, FileSize: %v, Written: %v\",\n\t\tmfp.Name, mfp.FileName, mfp.FileSize, mfp.Written)\n}\n\ntype multipartProgressWriter struct {\n\tw  io.Writer\n\tpb int64\n\tf  func(int64)\n}\n\nfunc (mpw *multipartProgressWriter) Write(p []byte) (n int, err error) {\n\tn, err = mpw.w.Write(p)\n\tif n <= 0 {\n\t\treturn\n\t}\n\tmpw.pb += int64(n)\n\tmpw.f(mpw.pb)\n\treturn\n}\n"
  },
  {
    "path": "multipart_test.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"errors\"\n\t\"io/fs\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestMultipartFormDataAndUpload(t *testing.T) {\n\tts := createFormPostServer(t)\n\tdefer ts.Close()\n\tdefer cleanupFiles(\".testdata/upload\")\n\n\tc := dcnl()\n\tc.SetFormData(map[string]string{\"zip_code\": \"00001\", \"city\": \"Los Angeles\"})\n\n\tt.Run(\"form data and upload\", func(t *testing.T) {\n\t\tresp, err := c.R().\n\t\t\tSetFile(\"profile_img\", filepath.Join(getTestDataPath(), \"test-img.png\")).\n\t\t\tPost(ts.URL + \"/upload\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\t\tassertTrue(t, strings.Contains(resp.String(), \"test-img.png\"))\n\t})\n\n\tt.Run(\"request form data and upload\", func(t *testing.T) {\n\t\tresp, err := c.R().\n\t\t\tSetFormData(map[string]string{\n\t\t\t\t\"welcome1\": \"welcome value 1\",\n\t\t\t\t\"welcome2\": \"welcome value 2\",\n\t\t\t\t\"welcome3\": \"welcome value 3\",\n\t\t\t}).\n\t\t\tSetFile(\"profile_img\", filepath.Join(getTestDataPath(), \"test-img.png\")).\n\t\t\tPost(ts.URL + \"/upload\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\t\tassertTrue(t, strings.Contains(resp.String(), \"test-img.png\"))\n\t})\n}\n\nfunc TestMultipartFormDataAndUploadMethodPatch(t *testing.T) {\n\tts := createFormPatchServer(t)\n\tdefer ts.Close()\n\tdefer cleanupFiles(\".testdata/upload\")\n\n\tc := dcnl()\n\tc.SetFormData(map[string]string{\"zip_code\": \"00001\", \"city\": \"Los Angeles\"})\n\n\tresp, err := c.R().\n\t\tSetFormData(map[string]string{\"zip_code\": \"00002\", \"city\": \"Los Angeles\"}).\n\t\tSetFile(\"profile_img\", filepath.Join(getTestDataPath(), \"test-img.png\")).\n\t\tPatch(ts.URL + \"/upload\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertTrue(t, strings.Contains(resp.String(), \"test-img.png\"))\n}\n\nfunc TestMultipartUploadError(t *testing.T) {\n\tts := createFormPostServer(t)\n\tdefer ts.Close()\n\tdefer cleanupFiles(\".testdata/upload\")\n\n\tc := dcnl()\n\tc.SetFormData(map[string]string{\"zip_code\": \"00001\", \"city\": \"Los Angeles\"})\n\n\tresp, err := c.R().\n\t\tSetFile(\"profile_img\", filepath.Join(getTestDataPath(), \"test-img-not-exists.png\")).\n\t\tPost(ts.URL + \"/upload\")\n\n\tassertNotNil(t, err)\n\tassertNil(t, resp)\n\tassertEqual(t, true, errors.Is(err, fs.ErrNotExist))\n}\n\nfunc TestMultipartUploadFiles(t *testing.T) {\n\tts := createFormPostServer(t)\n\tdefer ts.Close()\n\tdefer cleanupFiles(\".testdata/upload\")\n\n\tbasePath := getTestDataPath()\n\n\tc := dcnld()\n\n\tr := c.R().\n\t\tSetFormDataFromValues(url.Values{\n\t\t\t\"first_name\": []string{\"Jeevanandam\"},\n\t\t\t\"last_name\":  []string{\"M\"},\n\t\t}).\n\t\tSetFiles(map[string]string{\n\t\t\t\"profile_img\": filepath.Join(basePath, \"test-img.png\"),\n\t\t\t\"notes\":       filepath.Join(basePath, \"text-file.txt\"),\n\t\t})\n\tresp, err := r.Post(ts.URL + \"/upload\")\n\n\tresponseStr := resp.String()\n\n\t_ = r.Clone(context.Background())\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertTrue(t, strings.Contains(responseStr, \"test-img.png\"))\n\tassertTrue(t, strings.Contains(responseStr, \"text-file.txt\"))\n}\n\nfunc TestMultipartFilesAndFormDataEmptyGH1046(t *testing.T) {\n\tts := createFormPostServer(t)\n\tdefer ts.Close()\n\tdefer cleanupFiles(\".testdata/upload\")\n\n\tbasePath := getTestDataPath()\n\n\tc := dcnld()\n\n\tresp, err := c.R().\n\t\tSetFiles(map[string]string{\n\t\t\t\"profile_img\": filepath.Join(basePath, \"test-img.png\"),\n\t\t\t\"notes\":       filepath.Join(basePath, \"text-file.txt\"),\n\t\t}).\n\t\tPost(ts.URL + \"/upload\")\n\n\tresponseStr := resp.String()\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertTrue(t, strings.Contains(responseStr, \"test-img.png\"))\n\tassertTrue(t, strings.Contains(responseStr, \"text-file.txt\"))\n}\n\nfunc TestMultipartIoReaderFiles(t *testing.T) {\n\tts := createFormPostServer(t)\n\tdefer ts.Close()\n\tdefer cleanupFiles(\".testdata/upload\")\n\n\tbasePath := getTestDataPath()\n\tprofileImgBytes, _ := os.ReadFile(filepath.Join(basePath, \"test-img.png\"))\n\tnotesBytes, _ := os.ReadFile(filepath.Join(basePath, \"text-file.txt\"))\n\n\t// Just info values\n\t// file := File{\n\t// \tName:      \"test_file_name.jpg\",\n\t// \tParamName: \"test_param\",\n\t// \tReader:    bytes.NewBuffer([]byte(\"test bytes\")),\n\t// }\n\t// t.Logf(\"File Info: %v\", file.String())\n\n\tc := dcnld()\n\n\tr := c.R().\n\t\tSetFormData(map[string]string{\"first_name\": \"Jeevanandam\", \"last_name\": \"M\"}).\n\t\tSetFileReader(\"profile_img\", \"test-img.png\", bytes.NewReader(profileImgBytes)).\n\t\tSetFileReader(\"notes\", \"text-file.txt\", bytes.NewReader(notesBytes))\n\tresp, err := r.Post(ts.URL + \"/upload\")\n\n\tresponseStr := resp.String()\n\n\t_ = r.Clone(context.Background())\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertTrue(t, strings.Contains(responseStr, \"test-img.png\"))\n\tassertTrue(t, strings.Contains(responseStr, \"text-file.txt\"))\n}\n\nfunc TestMultipartUploadFileNotOnGetOrDelete(t *testing.T) {\n\tts := createFormPostServer(t)\n\tdefer ts.Close()\n\tdefer cleanupFiles(\".testdata/upload\")\n\n\tbasePath := getTestDataPath()\n\n\t_, err := dcnldr().\n\t\tSetFile(\"profile_img\", filepath.Join(basePath, \"test-img.png\")).\n\t\tGet(ts.URL + \"/upload\")\n\n\tassertEqual(t, \"resty: multipart is not allowed in HTTP verb: GET\", err.Error())\n\n\t_, err = dcnldr().\n\t\tSetFile(\"profile_img\", filepath.Join(basePath, \"test-img.png\")).\n\t\tDelete(ts.URL + \"/upload\")\n\n\tassertEqual(t, \"resty: multipart is not allowed in HTTP verb: DELETE\", err.Error())\n\n\tvar hook1Count int\n\tvar hook2Count int\n\t_, err = dcnl().\n\t\tOnInvalid(func(r *Request, err error) {\n\t\t\tassertEqual(t, \"resty: multipart is not allowed in HTTP verb: HEAD\", err.Error())\n\t\t\tassertNotNil(t, r)\n\t\t\thook1Count++\n\t\t}).\n\t\tOnInvalid(func(r *Request, err error) {\n\t\t\tassertEqual(t, \"resty: multipart is not allowed in HTTP verb: HEAD\", err.Error())\n\t\t\tassertNotNil(t, r)\n\t\t\thook2Count++\n\t\t}).\n\t\tR().\n\t\tSetFile(\"profile_img\", filepath.Join(basePath, \"test-img.png\")).\n\t\tHead(ts.URL + \"/upload\")\n\n\tassertEqual(t, \"resty: multipart is not allowed in HTTP verb: HEAD\", err.Error())\n\tassertEqual(t, 1, hook1Count)\n\tassertEqual(t, 1, hook2Count)\n}\n\nfunc TestMultipartFormData(t *testing.T) {\n\tts := createFormPostServer(t)\n\tdefer ts.Close()\n\tresp, err := dcnldr().\n\t\tSetMultipartFormData(map[string]string{\"first_name\": \"Jeevanandam\", \"last_name\": \"M\", \"zip_code\": \"00001\"}).\n\t\tSetBasicAuth(\"myuser\", \"mypass\").\n\t\tPost(ts.URL + \"/profile\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"Success\", resp.String())\n}\n\nfunc TestMultipartFormDataFields(t *testing.T) {\n\tts := createFormPostServer(t)\n\tdefer ts.Close()\n\n\tfields := []*MultipartField{\n\t\t{\n\t\t\tName:   \"field1\",\n\t\t\tValues: []string{\"field1value1\", \"field1value2\"},\n\t\t},\n\t\t{\n\t\t\tName:   \"field1\",\n\t\t\tValues: []string{\"field1value3\", \"field1value4\"},\n\t\t},\n\t\t{\n\t\t\tName:   \"field3\",\n\t\t\tValues: []string{\"field3value1\", \"field3value2\"},\n\t\t},\n\t\t{\n\t\t\tName:   \"field4\",\n\t\t\tValues: []string{\"field4value1\", \"field4value2\"},\n\t\t},\n\t}\n\n\tresp, err := dcnldr().\n\t\tSetMultipartFields(fields...).\n\t\tSetBasicAuth(\"myuser\", \"mypass\").\n\t\tPost(ts.URL + \"/profile\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"Success\", resp.String())\n}\n\nfunc TestMultipartField(t *testing.T) {\n\tts := createFormPostServer(t)\n\tdefer ts.Close()\n\tdefer cleanupFiles(\".testdata/upload\")\n\n\tjsonBytes := []byte(`{\"input\": {\"name\": \"Uploaded document\", \"_filename\" : [\"file.txt\"]}}`)\n\n\tc := dcnld()\n\n\tr := c.R().\n\t\tSetFormDataFromValues(url.Values{\n\t\t\t\"first_name\": []string{\"Jeevanandam\"},\n\t\t\t\"last_name\":  []string{\"M\"},\n\t\t}).\n\t\tSetMultipartField(\"uploadManifest\", \"upload-file.json\", \"application/json\", bytes.NewReader(jsonBytes))\n\tresp, err := r.Post(ts.URL + \"/upload\")\n\n\tresponseStr := resp.String()\n\n\t_ = r.Clone(context.Background())\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertTrue(t, strings.Contains(responseStr, \"upload-file.json\"))\n}\n\nfunc TestMultipartFields(t *testing.T) {\n\tts := createFormPostServer(t)\n\tdefer ts.Close()\n\tdefer cleanupFiles(\".testdata/upload\")\n\n\tjsonStr1 := `{\"input\": {\"name\": \"Uploaded document 1\", \"_filename\" : [\"file1.txt\"]}}`\n\tjsonStr2 := `{\"input\": {\"name\": \"Uploaded document 2\", \"_filename\" : [\"file2.txt\"]}}`\n\n\tfields := []*MultipartField{\n\t\t{\n\t\t\tName:        \"uploadManifest1\",\n\t\t\tFileName:    \"upload-file-1.json\",\n\t\t\tContentType: \"application/json\",\n\t\t\tReader:      bytes.NewBufferString(jsonStr1),\n\t\t},\n\t\t{\n\t\t\tName:        \"uploadManifest2\",\n\t\t\tFileName:    \"upload-file-2.json\",\n\t\t\tContentType: \"application/json\",\n\t\t\tReader:      bytes.NewBufferString(jsonStr2),\n\t\t},\n\t\t{\n\t\t\tName:        \"uploadManifest3\",\n\t\t\tContentType: \"application/json\",\n\t\t\tReader:      bytes.NewBufferString(jsonStr2),\n\t\t},\n\t}\n\n\tc := dcnld()\n\n\tr := c.R().\n\t\tSetFormData(map[string]string{\"first_name\": \"Jeevanandam\", \"last_name\": \"M\"}).\n\t\tSetMultipartFields(fields...)\n\tresp, err := r.Post(ts.URL + \"/upload\")\n\n\tresponseStr := resp.String()\n\n\t_ = r.Clone(context.Background())\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertTrue(t, strings.Contains(responseStr, \"upload-file-1.json\"))\n\tassertTrue(t, strings.Contains(responseStr, \"upload-file-2.json\"))\n}\n\nfunc TestMultipartCustomBoundary(t *testing.T) {\n\tts := createFormPostServer(t)\n\tdefer ts.Close()\n\tdefer cleanupFiles(\".testdata/upload\")\n\n\tt.Run(\"incorrect custom boundary\", func(t *testing.T) {\n\t\t_, err := dcnldr().\n\t\t\tSetMultipartFormData(map[string]string{\"first_name\": \"Jeevanandam\", \"last_name\": \"M\", \"zip_code\": \"00001\"}).\n\t\t\tSetMultipartBoundary(`\"custom-boundary\"`).\n\t\t\tSetBasicAuth(\"myuser\", \"mypass\").\n\t\t\tPost(ts.URL + \"/profile\")\n\n\t\tassertEqual(t, \"mime: invalid boundary character\", err.Error())\n\t})\n\n\tt.Run(\"correct custom boundary\", func(t *testing.T) {\n\t\tresp, err := dcnldr().\n\t\t\tSetMultipartFormData(map[string]string{\"first_name\": \"Jeevanandam\", \"last_name\": \"M\", \"zip_code\": \"00001\"}).\n\t\t\tSetMultipartBoundary(\"custom-boundary-\" + strconv.FormatInt(time.Now().Unix(), 10)).\n\t\t\tPost(ts.URL + \"/profile\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\t\tassertEqual(t, \"Success\", resp.String())\n\t})\n}\n\nfunc TestMultipartLargeFile(t *testing.T) {\n\tts := createFileUploadServer(t)\n\tdefer ts.Close()\n\n\tt.Run(\"upload a 2+mb image file with content-type and custom boundary\", func(t *testing.T) {\n\t\tc := dcnl()\n\t\tresp, err := c.R().\n\t\t\tSetFile(\"file\", filepath.Join(getTestDataPath(), \"test-img.png\")).\n\t\t\tSetMultipartBoundary(\"custom-boundary-\" + strconv.FormatInt(time.Now().Unix(), 10)).\n\t\t\tSetContentType(\"image/png\").\n\t\t\tPost(ts.URL + \"/upload\")\n\t\tassertNil(t, err)\n\t\tassertNotNil(t, resp)\n\t\tassertTrue(t, strings.Contains(resp.String(), \"File Uploaded successfully, file size: 2579629\")) // 2579697\n\t})\n\n\tt.Run(\"upload a 2+mb image file with content-type and incorrect custom boundary\", func(t *testing.T) {\n\t\tc := dcnl()\n\t\t_, err := c.R().\n\t\t\tSetFile(\"file\", filepath.Join(getTestDataPath(), \"test-img.png\")).\n\t\t\tSetMultipartBoundary(`\"custom-boundary-\"` + strconv.FormatInt(time.Now().Unix(), 10)).\n\t\t\tSetContentType(\"image/png\").\n\t\t\tPost(ts.URL + \"/upload\")\n\t\tassertNotNil(t, err)\n\t\tassertEqual(t, \"mime: invalid boundary character\", err.Error())\n\t})\n\n\tt.Run(\"upload a 2+mb image file without content-type\", func(t *testing.T) {\n\t\tc := dcnl()\n\t\tresp, err := c.R().\n\t\t\tSetFile(\"file\", filepath.Join(getTestDataPath(), \"test-img.png\")).\n\t\t\tPost(ts.URL + \"/upload\")\n\t\tassertNil(t, err)\n\t\tassertNotNil(t, resp)\n\t\tassertTrue(t, strings.Contains(resp.String(), \"File Uploaded successfully, file size: 2579697\"))\n\t})\n\n\tt.Run(\"upload a 50+mb binary file\", func(t *testing.T) {\n\t\tfp := createBinFile(\"50mbfile.bin\", 50<<20)\n\t\tdefer cleanupFiles(fp)\n\t\tc := dcnl()\n\t\tresp, err := c.R().\n\t\t\tSetFile(\"file\", fp).\n\t\t\tPost(ts.URL + \"/upload\")\n\t\tassertNil(t, err)\n\t\tassertNotNil(t, resp)\n\t\tassertTrue(t, strings.Contains(resp.String(), \"File Uploaded successfully, file size: 52429044\"))\n\t})\n}\n\nfunc TestMultipartFieldProgressCallback(t *testing.T) {\n\tts := createFormPostServer(t)\n\tdefer ts.Close()\n\tdefer cleanupFiles(\".testdata/upload\")\n\n\tfile1, _ := os.Open(filepath.Join(getTestDataPath(), \"test-img.png\"))\n\tfile1Stat, _ := file1.Stat()\n\n\tfileName2 := \"50mbfile.bin\"\n\tfilePath2 := createBinFile(fileName2, 50<<20)\n\tdefer cleanupFiles(filePath2)\n\tfile2, _ := os.Open(filePath2)\n\tfile2Stat, _ := file2.Stat()\n\n\tfileName3 := \"100mbfile.bin\"\n\tfilePath3 := createBinFile(fileName3, 100<<20)\n\tdefer cleanupFiles(filePath3)\n\tfile3, _ := os.Open(filePath3)\n\tfile3Stat, _ := file3.Stat()\n\n\tprogressCallback := func(mp MultipartFieldProgress) {\n\t\tt.Logf(\"%s\\n\", mp)\n\t}\n\n\tfields := []*MultipartField{\n\t\t{\n\t\t\tName:             \"test-image\",\n\t\t\tFilePath:         filepath.Join(getTestDataPath(), \"test-img.png\"),\n\t\t\tProgressCallback: progressCallback,\n\t\t},\n\t\t{\n\t\t\tName:             \"test-image-1\",\n\t\t\tFileName:         \"test-image-1.png\",\n\t\t\tContentType:      \"image/png\",\n\t\t\tReader:           file1,\n\t\t\tFileSize:         file1Stat.Size(),\n\t\t\tProgressCallback: progressCallback,\n\t\t},\n\t\t{\n\t\t\tName:             \"50mbfile\",\n\t\t\tFileName:         fileName2,\n\t\t\tReader:           file2,\n\t\t\tFileSize:         file2Stat.Size(),\n\t\t\tProgressCallback: progressCallback,\n\t\t},\n\t\t{\n\t\t\tName:             \"100mbfile\",\n\t\t\tFileName:         fileName3,\n\t\t\tReader:           file3,\n\t\t\tFileSize:         file3Stat.Size(),\n\t\t\tProgressCallback: progressCallback,\n\t\t},\n\t}\n\n\tc := dcnld()\n\n\tr := c.R().\n\t\tSetFormData(map[string]string{\"first_name\": \"Jeevanandam\", \"last_name\": \"M\"}).\n\t\tSetMultipartFields(fields...)\n\tresp, err := r.Post(ts.URL + \"/upload\")\n\n\tresponseStr := resp.String()\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertTrue(t, strings.Contains(responseStr, \"test-image-1.png\"))\n\tassertTrue(t, strings.Contains(responseStr, \"test-img.png\"))\n\tassertTrue(t, strings.Contains(responseStr, \"50mbfile.bin\"))\n\tassertTrue(t, strings.Contains(responseStr, \"100mbfile.bin\"))\n}\n\nfunc TestMultipartOrderedFormData(t *testing.T) {\n\tts := createFormPostServer(t)\n\tdefer ts.Close()\n\tdefer cleanupFiles(\".testdata/upload\")\n\n\tjsonStr1 := `{\"input\": {\"name\": \"Uploaded document 1\", \"_filename\" : [\"file1.txt\"]}}`\n\tjsonStr2 := `{\"input\": {\"name\": \"Uploaded document 2\", \"_filename\" : [\"file2.txt\"]}}`\n\n\tfields := []*MultipartField{\n\t\t{\n\t\t\tName:   \"field1\",\n\t\t\tValues: []string{\"field1value1\", \"field1value2\"},\n\t\t},\n\t\t{\n\t\t\tName:   \"field2\",\n\t\t\tValues: []string{\"field2value1\", \"field2value2\"},\n\t\t},\n\t\t{\n\t\t\tName:        \"uploadManifest1\",\n\t\t\tFileName:    \"upload-file-1.json\",\n\t\t\tContentType: \"application/json\",\n\t\t\tReader:      bytes.NewBufferString(jsonStr1),\n\t\t},\n\t\t{\n\t\t\tName:   \"field3\",\n\t\t\tValues: []string{\"field3value1\", \"field3value2\"},\n\t\t},\n\t\t{\n\t\t\tName:        \"uploadManifest2\",\n\t\t\tFileName:    \"upload-file-2.json\",\n\t\t\tContentType: \"application/json\",\n\t\t\tReader:      bytes.NewBufferString(jsonStr2),\n\t\t},\n\t\t{\n\t\t\tName:   \"field4\",\n\t\t\tValues: []string{\"field4value1\", \"field4value2\"},\n\t\t},\n\t\t{\n\t\t\tName:        \"uploadManifest3\",\n\t\t\tContentType: \"application/json\",\n\t\t\tReader:      bytes.NewBufferString(jsonStr2),\n\t\t},\n\t}\n\n\tc := dcnld().SetBaseURL(ts.URL)\n\n\tresp, err := c.R().\n\t\tSetMultipartOrderedFormData(\"first_name\", []string{\"Jeevanandam\"}).\n\t\tSetMultipartOrderedFormData(\"last_name\", []string{\"M\"}).\n\t\tSetMultipartFields(fields...).\n\t\tPost(\"/upload\")\n\n\tresponseStr := resp.String()\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertTrue(t, strings.Contains(responseStr, \"upload-file-1.json\"))\n\tassertTrue(t, strings.Contains(responseStr, \"upload-file-2.json\"))\n}\n\nvar errTestErrorReader = errors.New(\"fake\")\n\ntype errorReader struct{}\n\nfunc (errorReader) Read(p []byte) (n int, err error) {\n\treturn 0, errTestErrorReader\n}\n\nfunc TestMultipartReaderErrors(t *testing.T) {\n\tts := createFileUploadServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl().SetBaseURL(ts.URL)\n\n\tt.Run(\"multipart fields with errorReader\", func(t *testing.T) {\n\t\tresp, err := c.R().\n\t\t\tSetMultipartFields(&MultipartField{\n\t\t\t\tName:        \"foo\",\n\t\t\t\tContentType: \"text/plain\",\n\t\t\t\tReader:      &errorReader{},\n\t\t\t}).\n\t\t\tPost(\"/upload\")\n\n\t\tassertNotNil(t, err)\n\t\tassertEqual(t, errTestErrorReader, err)\n\t\tassertNotNil(t, resp)\n\n\t\terr = resp.wrapError(errors.New(\"test error\"), true)\n\t\tassertNil(t, err)\n\t\tassertEqual(t, \"test error\", resp.CascadeError.Error())\n\t})\n\n\tt.Run(\"multipart files with errorReader\", func(t *testing.T) {\n\t\tresp, err := c.R().\n\t\t\tSetFileReader(\"foo\", \"foo.txt\", &errorReader{}).\n\t\t\tPost(\"/upload\")\n\n\t\tassertNotNil(t, err)\n\t\tassertEqual(t, errTestErrorReader, err)\n\t\tassertNil(t, resp)\n\t})\n\n\tt.Run(\"multipart with file not found\", func(t *testing.T) {\n\t\tresp, err := c.R().\n\t\t\tSetFile(\"foo\", \"foo.txt\").\n\t\t\tPost(\"/upload\")\n\n\t\tassertNotNil(t, err)\n\t\tassertEqual(t, true, errors.Is(err, fs.ErrNotExist))\n\t\tassertNil(t, resp)\n\t})\n}\n\ntype mpWriterError struct{}\n\nfunc (mwe *mpWriterError) Write(p []byte) (int, error) {\n\treturn 0, errors.New(\"multipart write error\")\n}\n\nfunc TestMultipartRequest_Errors(t *testing.T) {\n\tmw := multipart.NewWriter(&mpWriterError{})\n\n\tc := dcnl()\n\treq1 := c.R().SetFormData(map[string]string{\n\t\t\"name1\": \"value1\",\n\t\t\"name2\": \"value2\",\n\t})\n\n\tt.Run(\"writeFormData\", func(t *testing.T) {\n\t\terr1 := multipartWriteFormData(mw, req1)\n\t\tassertNotNil(t, err1)\n\t\tassertEqual(t, \"multipart write error\", err1.Error())\n\t})\n}\n\nfunc TestMultipartUploadFailAutoErrorParse(t *testing.T) {\n\ttype ErrorResponse struct {\n\t\tCode    int    `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t}\n\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(hdrContentTypeKey, \"application/json\")\n\t\tw.WriteHeader(http.StatusForbidden)\n\t\t_, _ = w.Write([]byte(`{ \"code\": 403, \"message\": \"forbidden error message\" }`))\n\t})\n\tdefer ts.Close()\n\n\tc := dcnl()\n\n\tt.Run(\"single request\", func(t *testing.T) {\n\t\tres, err := c.R().\n\t\t\tSetFile(\"profile_img\", filepath.Join(getTestDataPath(), \"test-img.png\")).\n\t\t\tSetResultError(&ErrorResponse{}).\n\t\t\tPost(ts.URL)\n\n\t\tassertNil(t, err)\n\t\tassertEqual(t, http.StatusForbidden, res.StatusCode())\n\n\t\ter := res.ResultError().(*ErrorResponse)\n\t\tassertEqual(t, 403, er.Code)\n\t\tassertEqual(t, \"forbidden error message\", er.Message)\n\t})\n\n\tt.Run(\"concurrent requests\", func(t *testing.T) {\n\t\tconcurrencyCount := 50\n\t\twg := sync.WaitGroup{}\n\t\tfor i := 0; i < concurrencyCount; i++ {\n\t\t\twg.Add(1)\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tres, _ := c.R().\n\t\t\t\t\tSetFile(\"profile_img\", filepath.Join(getTestDataPath(), \"test-img.png\")).\n\t\t\t\t\tSetResultError(&ErrorResponse{}).\n\t\t\t\t\tPost(ts.URL)\n\n\t\t\t\ter := res.ResultError().(*ErrorResponse)\n\t\t\t\tassertEqual(t, http.StatusForbidden, res.StatusCode())\n\t\t\t\tassertEqual(t, 403, er.Code)\n\t\t\t\tassertEqual(t, \"forbidden error message\", er.Message)\n\t\t\t}()\n\t\t}\n\t\twg.Wait()\n\t})\n\n}\n\nfunc TestMultipartConcurrentRequests(t *testing.T) {\n\tts := createFormPostServer(t)\n\tdefer ts.Close()\n\tdefer cleanupFiles(\".testdata/upload\")\n\n\tc := dcnl()\n\tc.SetFormData(map[string]string{\"zip_code\": \"00001\", \"city\": \"Los Angeles\"})\n\n\tconcurrencyCount := 100\n\twg := sync.WaitGroup{}\n\tfor i := 0; i < concurrencyCount; i++ {\n\t\twg.Add(1)\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tres, err := c.R().\n\t\t\t\tSetFormData(map[string]string{\n\t\t\t\t\t\"welcome1\": \"welcome value 1\",\n\t\t\t\t\t\"welcome2\": \"welcome value 2\",\n\t\t\t\t\t\"welcome3\": \"welcome value 3\",\n\t\t\t\t}).\n\t\t\t\tSetFile(\"profile_img\", filepath.Join(getTestDataPath(), \"test-img.png\")).\n\t\t\t\tPost(ts.URL + \"/upload\")\n\n\t\t\tassertError(t, err)\n\t\t\tassertEqual(t, http.StatusOK, res.StatusCode())\n\t\t\tassertEqual(t, true, strings.Contains(res.String(), \"test-img.png\"))\n\t\t}()\n\t}\n\twg.Wait()\n}\n\ntype returnValueTestWriter struct {\n}\n\nfunc (z *returnValueTestWriter) Write(p []byte) (n int, err error) {\n\treturn 0, nil\n}\n\nfunc TestMultipartCornerCoverage(t *testing.T) {\n\tmf := &MultipartField{\n\t\tName:   \"foo\",\n\t\tReader: bytes.NewBufferString(\"I have no seek capability\"),\n\t}\n\terr := mf.resetReader()\n\tassertNil(t, err)\n\n\t// wrap test writer to return 0 written value\n\tmpw := multipartProgressWriter{w: &returnValueTestWriter{}}\n\tn, err := mpw.Write([]byte(\"test return value\"))\n\tassertNil(t, err)\n\tassertEqual(t, 0, n)\n}\n"
  },
  {
    "path": "redirect.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n)\n\ntype (\n\t// RedirectPolicy to regulate the redirects in the Resty client.\n\t// Objects implementing the [RedirectPolicy] interface can be registered as\n\t//\n\t// Apply function should return nil to continue the redirect journey; otherwise\n\t// return error to stop the redirect.\n\tRedirectPolicy interface {\n\t\tApply(*http.Request, []*http.Request) error\n\t}\n\n\t// The [RedirectPolicyFunc] type is an adapter to allow the use of ordinary\n\t// functions as [RedirectPolicy]. If `f` is a function with the appropriate\n\t// signature, RedirectPolicyFunc(f) is a RedirectPolicy object that calls `f`.\n\tRedirectPolicyFunc func(*http.Request, []*http.Request) error\n\n\t// RedirectInfo struct is used to capture the URL and status code for the redirect history\n\tRedirectInfo struct {\n\t\tURL        string\n\t\tStatusCode int\n\t}\n)\n\n// Apply calls f(req, via).\nfunc (f RedirectPolicyFunc) Apply(req *http.Request, via []*http.Request) error {\n\treturn f(req, via)\n}\n\n// RedirectNoPolicy is used to disable the redirects in the Resty client\n//\n//\tresty.SetRedirectPolicy(resty.RedirectNoPolicy())\nfunc RedirectNoPolicy() RedirectPolicy {\n\treturn RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {\n\t\treturn http.ErrUseLastResponse\n\t})\n}\n\n// RedirectFlexiblePolicy method is convenient for creating several redirect policies for Resty clients.\n//\n//\tresty.SetRedirectPolicy(RedirectFlexiblePolicy(20))\nfunc RedirectFlexiblePolicy(noOfRedirect int) RedirectPolicy {\n\treturn RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {\n\t\tif len(via) >= noOfRedirect {\n\t\t\treturn fmt.Errorf(\"resty: stopped after %d redirects\", noOfRedirect)\n\t\t}\n\t\tcheckHostAndAddHeaders(req, via[0])\n\t\treturn nil\n\t})\n}\n\n// RedirectDomainCheckPolicy method is convenient for defining domain name redirect rules in Resty clients.\n// Redirect is allowed only for the host mentioned in the policy.\n//\n//\tresty.SetRedirectPolicy(resty.RedirectDomainCheckPolicy(\"host1.com\", \"host2.org\", \"host3.net\"))\nfunc RedirectDomainCheckPolicy(hostnames ...string) RedirectPolicy {\n\thosts := make(map[string]bool)\n\tfor _, h := range hostnames {\n\t\thosts[strings.ToLower(h)] = true\n\t}\n\n\treturn RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {\n\t\tif ok := hosts[getHostname(req.URL.Host)]; !ok {\n\t\t\treturn errors.New(\"redirect is not allowed as per DomainCheckRedirectPolicy\")\n\t\t}\n\t\tcheckHostAndAddHeaders(req, via[0])\n\t\treturn nil\n\t})\n}\n\nfunc getHostname(host string) (hostname string) {\n\tif strings.Index(host, \":\") > 0 {\n\t\thost, _, _ = net.SplitHostPort(host)\n\t}\n\thostname = strings.ToLower(host)\n\treturn\n}\n\n// By default, Golang will not redirect request headers.\n// After reading through the various discussion comments from the thread -\n// https://github.com/golang/go/issues/4800\n// Resty will add all the headers during a redirect for the same host and\n// adds library user-agent if the Host is different.\nfunc checkHostAndAddHeaders(cur *http.Request, pre *http.Request) {\n\tcurHostname := getHostname(cur.URL.Host)\n\tpreHostname := getHostname(pre.URL.Host)\n\tif strings.EqualFold(curHostname, preHostname) {\n\t\tfor key, val := range pre.Header {\n\t\t\tcur.Header[key] = val\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "request.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"maps\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n)\n\n//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\n// Request struct and methods\n//_______________________________________________________________________\n\n// Request struct is used to compose and fire individual requests from\n// Resty client. The [Request] provides an option to override client-level\n// settings and also an option for the request composition.\ntype Request struct {\n\t// CorrelationID used to track/relate requests.\n\t// By default, Resty sets a GUID as the correlation ID for requests with retry count > 0.\n\tCorrelationID string\n\n\tURL                          string\n\tMethod                       string\n\tAuthToken                    string\n\tAuthScheme                   string\n\tQueryParams                  url.Values\n\tFormData                     url.Values\n\tPathParams                   map[string]string\n\tHeader                       http.Header\n\tStartTime                    time.Time\n\tBody                         any\n\tResult                       any\n\tResultError                  any\n\tRawRequest                   *http.Request\n\tCookies                      []*http.Cookie\n\tIsDebug                      bool\n\tIsCloseConnection            bool\n\tIsResponseDoNotParse         bool\n\tResponseSaveFileName         string\n\tResponseExpectContentType    string\n\tResponseForceContentType     string\n\tDebugBodyLimit               int\n\tResponseBodyLimit            int64\n\tIsResponseBodyUnlimitedReads bool\n\tIsTrace                      bool\n\tIsMethodGetAllowPayload      bool\n\tIsMethodDeleteAllowPayload   bool\n\tIsDone                       bool\n\tIsResponseSaveToFile         bool\n\tTimeout                      time.Duration\n\tHeaderAuthorizationKey       string\n\tRetryCount                   int\n\tRetryWaitTime                time.Duration\n\tRetryMaxWaitTime             time.Duration\n\tRetryDelayStrategy           RetryDelayStrategyFunc\n\tIsRetryDefaultConditions     bool\n\tIsRetryAllowNonIdempotent    bool\n\n\t// Attempt provides insights into no. of attempts\n\t// Resty made.\n\t//\n\t//\tfirst attempt + retry count = total attempts\n\tAttempt int\n\n\tmu                   *sync.Mutex\n\tcredentials          *credentials\n\tisMultiPart          bool\n\tisFormData           bool\n\tisContentLengthSet   bool\n\tcontentLength        int64\n\tjsonEscapeHTML       bool\n\tctx                  context.Context\n\tctxCancelFunc        context.CancelFunc\n\tvalues               map[string]any\n\tclient               *Client\n\tbodyBuf              *bytes.Buffer\n\ttrace                *clientTrace\n\tlog                  Logger\n\tbaseURL              string\n\tmultipartBoundary    string\n\tmultipartFields      []*MultipartField\n\tretryConditions      []RetryConditionFunc\n\tisSetRetryConditions bool\n\tretryHooks           []RetryHookFunc\n\tisSetRetryHooks      bool\n\tcurlCmdString        string\n\tisCurlCmdGenerate    bool\n\tisCurlCmdDebugLog    bool\n\tunescapeQueryParams  bool\n\tmultipartErrChan     chan error\n\tmultipartCancelFunc  context.CancelFunc\n}\n\n// SetCorrelationID method is used to set the correlation ID for the request\n//\n// By default, Resty sets a GUID as the correlation ID for requests with retry count > 0.\nfunc (r *Request) SetCorrelationID(id string) *Request {\n\tr.CorrelationID = id\n\treturn r\n}\n\n// SetMethod method used to set the HTTP verb for the request\nfunc (r *Request) SetMethod(m string) *Request {\n\tr.Method = m\n\treturn r\n}\n\n// SetURL method used to set the request URL for the request\nfunc (r *Request) SetURL(url string) *Request {\n\tr.URL = url\n\treturn r\n}\n\n// Context method returns the request's [context.Context]. To change the context, use\n// [Request.Clone] or [Request.WithContext].\n//\n// The returned context is always non-nil; it defaults to the\n// background context.\nfunc (r *Request) Context() context.Context {\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\tif r.ctx == nil {\n\t\treturn context.Background()\n\t}\n\treturn r.ctx\n}\n\n// SetContext method sets the [context.Context] for current [Request].\n// It overwrites the current context in the Request instance; it does not\n// affect the [Request].RawRequest that was already created.\n//\n// If you want this method to take effect, use this method before invoking\n// [Request.Send] or [Request].HTTPVerb methods.\n//\n// See [Request.WithContext], [Request.Clone]\nfunc (r *Request) SetContext(ctx context.Context) *Request {\n\tr.mu.Lock()\n\tdefer r.mu.Unlock()\n\tr.ctx = ctx\n\treturn r\n}\n\n// WithContext method returns a shallow copy of r with its context changed\n// to ctx. The provided ctx must be non-nil. It does not\n// affect the [Request].RawRequest that was already created.\n//\n// If you want this method to take effect, use this method before invoking\n// [Request.Send] or [Request].HTTPVerb methods.\n//\n// See [Request.SetContext], [Request.Clone]\nfunc (r *Request) WithContext(ctx context.Context) *Request {\n\tif ctx == nil {\n\t\tpanic(\"resty: Request.WithContext nil context\")\n\t}\n\trr := new(Request)\n\t*rr = *r\n\trr.ctx = ctx\n\treturn rr\n}\n\n// SetContentType method is a convenient way to set the header Content-Type in the request\n//\n//\tclient.R().SetContentType(\"application/json\")\nfunc (r *Request) SetContentType(ct string) *Request {\n\tr.SetHeader(hdrContentTypeKey, ct)\n\treturn r\n}\n\n// SetHeader method sets a single header field and its value in the current request.\n//\n// For Example: To set `Content-Type` and `Accept` as `application/json`.\n//\n//\tclient.R().\n//\t\tSetHeader(\"Content-Type\", \"application/json\").\n//\t\tSetHeader(\"Accept\", \"application/json\")\n//\n// It overrides the header value set at the client instance level.\nfunc (r *Request) SetHeader(header, value string) *Request {\n\tr.Header.Set(header, value)\n\treturn r\n}\n\n// SetHeaderAny method sets a single header field and its value in the current request.\n//\n// It is similar to [Request.SetHeader] but accepts any type as the value and converts\n// it to a string using predefined formatting rules (integers, bools, time.Time, etc.).\n//\n// For Example: To set `X-Request-Id` with an integer value\n//\n//\tclient.R().SetHeaderAny(\"X-Request-Id\", 12345)\n//\n// It overrides the header value set at the client instance level.\n//\n// See [Client.SetHeaderAny].\nfunc (r *Request) SetHeaderAny(header string, value any) *Request {\n\tstrVal := formatAnyToString(value)\n\tr.Header.Set(header, strVal)\n\treturn r\n}\n\n// SetHeaders method sets multiple header fields and their values at one go in the current request.\n//\n// For Example: To set `Content-Type` and `Accept` as `application/json`\n//\n//\tclient.R().\n//\t\tSetHeaders(map[string]string{\n//\t\t\t\"Content-Type\": \"application/json\",\n//\t\t\t\"Accept\": \"application/json\",\n//\t\t})\n//\n// It overrides the header value set at the client instance level.\nfunc (r *Request) SetHeaders(headers map[string]string) *Request {\n\tfor h, v := range headers {\n\t\tr.SetHeader(h, v)\n\t}\n\treturn r\n}\n\n// SetHeaderMultiValues sets multiple header fields and their values as a list of strings in the current request.\n//\n// For Example: To set `Accept` as `text/html, application/xhtml+xml, application/xml;q=0.9, image/webp, */*;q=0.8`\n//\n//\tclient.R().\n//\t\tSetHeaderMultiValues(map[string][]string{\n//\t\t\t\"Accept\": []string{\"text/html\", \"application/xhtml+xml\", \"application/xml;q=0.9\", \"image/webp\", \"*/*;q=0.8\"},\n//\t\t})\n//\n// It overrides the header value set at the client instance level.\nfunc (r *Request) SetHeaderMultiValues(headers map[string][]string) *Request {\n\tfor key, values := range headers {\n\t\tr.SetHeader(key, strings.Join(values, \", \"))\n\t}\n\treturn r\n}\n\n// SetHeaderVerbatim method is used to set the HTTP header key and value verbatim in the current request.\n// It is typically helpful for legacy applications or servers that require HTTP headers in a certain way\n//\n// For Example: To set header key as `all_lowercase`, `UPPERCASE`, and `x-cloud-trace-id`\n//\n//\tclient.R().\n//\t\tSetHeaderVerbatim(\"all_lowercase\", \"available\").\n//\t\tSetHeaderVerbatim(\"UPPERCASE\", \"available\").\n//\t\tSetHeaderVerbatim(\"x-cloud-trace-id\", \"798e94019e5fc4d57fbb8901eb4c6cae\")\n//\n// It overrides the header value set at the client instance level.\nfunc (r *Request) SetHeaderVerbatim(header, value string) *Request {\n\tr.Header[header] = []string{value}\n\treturn r\n}\n\n// SetHeaderVerbatimAny method sets the HTTP header key and value verbatim in the current request.\n//\n// It is similar to [Request.SetHeaderVerbatim] but accepts any type as the value and converts\n// it to a string using predefined formatting rules (integers, bools, time.Time, etc.).\n//\n// For Example: To set header key as `x-trace-id` with an integer value\n//\n//\tclient.R().SetHeaderVerbatimAny(\"x-trace-id\", 798940)\n//\n// It overrides the header value set at the client instance level.\n//\n// See [Client.SetHeaderVerbatimAny].\nfunc (r *Request) SetHeaderVerbatimAny(header string, value any) *Request {\n\tstrVal := formatAnyToString(value)\n\tr.Header[header] = []string{strVal}\n\treturn r\n}\n\n// SetQueryParam method sets a single parameter and its value in the current request.\n// It will be formed as a query string for the request.\n//\n// For Example: `search=kitchen%20papers&size=large` in the URL after the `?` mark.\n//\n//\tclient.R().\n//\t\tSetQueryParam(\"search\", \"kitchen papers\").\n//\t\tSetQueryParam(\"size\", \"large\")\n//\n// It overrides the query parameter value set at the client instance level.\nfunc (r *Request) SetQueryParam(param, value string) *Request {\n\tr.QueryParams.Set(param, value)\n\treturn r\n}\n\n// SetQueryParamAny method sets a single query parameter and its value in the current request.\n// It will be formed as a query string for the request.\n//\n// It is similar to [Request.SetQueryParam] but accepts any type as the value and converts\n// it to a string using predefined formatting rules (integers, bools, time.Time, etc.).\n//\n// For Example: To set `page` and `active` query parameters\n//\n//\tclient.R().\n//\t\tSetQueryParamAny(\"page\", 5).\n//\t\tSetQueryParamAny(\"active\", true)\n//\n// It overrides the query parameter value set at the client instance level.\n//\n// See [Client.SetQueryParamAny].\nfunc (r *Request) SetQueryParamAny(param string, value any) *Request {\n\tstrVal := formatAnyToString(value)\n\tr.QueryParams.Set(param, strVal)\n\treturn r\n}\n\n// SetQueryParams method sets multiple parameters and their values at one go in the current request.\n// It will be formed as a query string for the request.\n//\n// For Example: `search=kitchen%20papers&size=large` in the URL after the `?` mark.\n//\n//\tclient.R().\n//\t\tSetQueryParams(map[string]string{\n//\t\t\t\"search\": \"kitchen papers\",\n//\t\t\t\"size\": \"large\",\n//\t\t})\n//\n// It overrides the query parameter value set at the client instance level.\nfunc (r *Request) SetQueryParams(params map[string]string) *Request {\n\tfor p, v := range params {\n\t\tr.SetQueryParam(p, v)\n\t}\n\treturn r\n}\n\n// SetQueryParamsFromValues method appends multiple parameters with multi-value\n// ([url.Values]) at one go in the current request. It will be formed as\n// query string for the request.\n//\n// For Example: `status=pending&status=approved&status=open` in the URL after the `?` mark.\n//\n//\tclient.R().\n//\t\tSetQueryParamsFromValues(url.Values{\n//\t\t\t\"status\": []string{\"pending\", \"approved\", \"open\"},\n//\t\t})\n//\n// It overrides the query parameter value set at the client instance level.\nfunc (r *Request) SetQueryParamsFromValues(params url.Values) *Request {\n\tfor p, v := range params {\n\t\tfor _, pv := range v {\n\t\t\tr.QueryParams.Add(p, pv)\n\t\t}\n\t}\n\treturn r\n}\n\n// SetQueryString method provides the ability to use string as an input to set URL query string for the request.\n//\n//\tclient.R().\n//\t\tSetQueryString(\"productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more\")\n//\n// It overrides the query parameter value set at the client instance level.\nfunc (r *Request) SetQueryString(query string) *Request {\n\tparams, err := url.ParseQuery(strings.TrimSpace(query))\n\tif err == nil {\n\t\tfor p, v := range params {\n\t\t\tfor _, pv := range v {\n\t\t\t\tr.QueryParams.Add(p, pv)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tr.log.Errorf(\"%v\", err)\n\t}\n\treturn r\n}\n\n// SetFormData method sets form parameters and their values in the current request.\n// The request content type would be set as `application/x-www-form-urlencoded`.\n//\n//\tclient.R().\n//\t\tSetFormData(map[string]string{\n//\t\t\t\"access_token\": \"BC594900-518B-4F7E-AC75-BD37F019E08F\",\n//\t\t\t\"user_id\": \"3455454545\",\n//\t\t})\n//\n// It overrides the form data value set at the client instance level.\n//\n// See [Request.SetFormDataFromValues] for the same field name with multiple values.\nfunc (r *Request) SetFormData(data map[string]string) *Request {\n\tfor k, v := range data {\n\t\tr.FormData.Set(k, v)\n\t}\n\treturn r\n}\n\n// SetFormDataFromValues method appends multiple form parameters with multi-value\n// ([url.Values]) at one go in the current request.\n//\n//\tclient.R().\n//\t\tSetFormDataFromValues(url.Values{\n//\t\t\t\"search_criteria\": []string{\"book\", \"glass\", \"pencil\"},\n//\t\t})\n//\n// It overrides the form data value set at the client instance level.\nfunc (r *Request) SetFormDataFromValues(data url.Values) *Request {\n\tfor k, v := range data {\n\t\tfor _, kv := range v {\n\t\t\tr.FormData.Add(k, kv)\n\t\t}\n\t}\n\treturn r\n}\n\n// SetBody method sets the request body for the request. It supports various practical needs as easy.\n// It's quite handy and powerful. Supported request body data types are `string`,\n// `[]byte`, `struct`, `map`, `slice` and [io.Reader].\n//\n// Body value can be pointer or non-pointer. Automatic marshalling for JSON and XML content type, if it is `struct`, `map`, or `slice`.\n//\n// NOTE: [io.Reader] is processed in bufferless mode while sending a request.\n//\n// For Example:\n//\n// `struct` gets marshaled based on the request header `Content-Type`.\n//\n//\tclient.R().\n//\t\tSetBody(User{\n//\t\t\tUsername: \"jeeva@myjeeva.com\",\n//\t\t\tPassword: \"welcome2resty\",\n//\t\t})\n//\n// 'map` gets marshaled based on the request header `Content-Type`.\n//\n//\tclient.R().\n//\t\tSetBody(map[string]any{\n//\t\t\t\"username\": \"jeeva@myjeeva.com\",\n//\t\t\t\"password\": \"welcome2resty\",\n//\t\t\t\"address\": &Address{\n//\t\t\t\tAddress1: \"1111 This is my street\",\n//\t\t\t\tAddress2: \"Apt 201\",\n//\t\t\t\tCity: \"My City\",\n//\t\t\t\tState: \"My State\",\n//\t\t\t\tZipCode: 00000,\n//\t\t\t},\n//\t\t})\n//\n// `string` as a body input. Suitable for any need as a string input.\n//\n//\tclient.R().\n//\t\tSetBody(`{\n//\t\t\t\"username\": \"jeeva@getrightcare.com\",\n//\t\t\t\"password\": \"admin\"\n//\t\t}`)\n//\n// `[]byte` as a body input. Suitable for raw requests such as file upload, serialize & deserialize, etc.\n//\n//\tclient.R().\n//\t\tSetBody([]byte(\"This is my raw request, sent as-is\"))\n//\n// and so on.\nfunc (r *Request) SetBody(body any) *Request {\n\tr.Body = body\n\treturn r\n}\n\n// SetResult method registers the response `Result` object type for automatic\n// unmarshalling of the HTTP response if the response status code is\n// between 200 and 299, and the content type is either JSON or XML.\n//\n// Note: [Request.SetResult] input can be a pointer or non-pointer.\n//\n// The pointer with handle\n//\n//\tauthToken := &AuthToken{}\n//\tclient.R().SetResult(authToken)\n//\n//\t// Can be accessed via -\n//\tfmt.Println(authToken) OR fmt.Println(response.Result().(*AuthToken))\n//\n// OR -\n//\n// The pointer without handle or non-pointer\n//\n//\tclient.R().SetResult(&AuthToken{})\n//\t// OR\n//\tclient.R().SetResult(AuthToken{})\n//\n//\t// Can be accessed via -\n//\tfmt.Println(response.Result().(*AuthToken))\nfunc (r *Request) SetResult(v any) *Request {\n\tr.Result = getPointer(v)\n\treturn r\n}\n\n// SetResultError method registers the response `ResultError` object type for automatic\n// unmarshalling for the request, if the response status code is greater than 399 and\n// the content type is either JSON or XML.\n//\n// NOTE: [Request.SetResultError] input can be a pointer or non-pointer.\n//\n//\tclient.R().SetResultError(&AuthError{})\n//\t// OR\n//\tclient.R().SetResultError(AuthError{})\n//\n// Accessing an unmarshalled error object from response instance.\n//\n//\tresponse.ResultError().(*AuthError)\n//\n// If this request ResultError object is nil, it will use the client-level error object\n// type if it is set.\nfunc (r *Request) SetResultError(err any) *Request {\n\tr.ResultError = getPointer(err)\n\treturn r\n}\n\n// SetFile method sets a single file field name and its path for multipart upload.\n//\n// Resty provides an optional multipart live upload progress callback;\n// see method [Request.SetMultipartFields]\n//\n//\tclient.R().\n//\t\tSetFile(\"my_file\", \"/Users/jeeva/Gas Bill - Sep.pdf\")\nfunc (r *Request) SetFile(fieldName, filePath string) *Request {\n\tr.isMultiPart = true\n\tr.multipartFields = append(r.multipartFields, &MultipartField{\n\t\tName:     fieldName,\n\t\tFileName: filepath.Base(filePath),\n\t\tFilePath: filePath,\n\t})\n\treturn r\n}\n\n// SetFiles method sets multiple file field names and their paths for multipart uploads.\n//\n// Resty provides an optional multipart live upload progress callback;\n// see method [Request.SetMultipartFields]\n//\n//\tclient.R().\n//\t\tSetFiles(map[string]string{\n//\t\t\t\"my_file1\": \"/Users/jeeva/Gas Bill - Sep.pdf\",\n//\t\t\t\"my_file2\": \"/Users/jeeva/Electricity Bill - Sep.pdf\",\n//\t\t\t\"my_file3\": \"/Users/jeeva/Water Bill - Sep.pdf\",\n//\t\t})\nfunc (r *Request) SetFiles(files map[string]string) *Request {\n\tr.isMultiPart = true\n\tfor f, fp := range files {\n\t\tr.multipartFields = append(r.multipartFields, &MultipartField{\n\t\t\tName:     f,\n\t\t\tFileName: filepath.Base(fp),\n\t\t\tFilePath: fp,\n\t\t})\n\t}\n\treturn r\n}\n\n// SetFileReader method is to set a file using [io.Reader] for multipart upload.\n//\n// Resty provides an optional multipart live upload progress callback;\n// see method [Request.SetMultipartFields]\n//\n//\tclient.R().\n//\t\tSetFileReader(\"profile_img\", \"my-profile-img.png\", bytes.NewReader(profileImgBytes)).\n//\t\tSetFileReader(\"notes\", \"user-notes.txt\", bytes.NewReader(notesBytes))\nfunc (r *Request) SetFileReader(fieldName, fileName string, reader io.Reader) *Request {\n\tr.SetMultipartField(fieldName, fileName, \"\", reader)\n\treturn r\n}\n\n// SetMultipartFormData method allows simple form data to be attached to the request\n// as `multipart:form-data`\nfunc (r *Request) SetMultipartFormData(data map[string]string) *Request {\n\tr.isMultiPart = true\n\tfor k, v := range data {\n\t\tr.FormData.Set(k, v)\n\t}\n\treturn r\n}\n\n// SetMultipartOrderedFormData method allows add ordered form data to be attached to the request\n// as `multipart:form-data`\nfunc (r *Request) SetMultipartOrderedFormData(name string, values []string) *Request {\n\tr.isMultiPart = true\n\tr.multipartFields = append(r.multipartFields, &MultipartField{\n\t\tName:   name,\n\t\tValues: values,\n\t})\n\treturn r\n}\n\n// SetMultipartField method sets custom data with Content-Type using [io.Reader] for multipart upload.\n//\n// Resty provides an optional multipart live upload progress callback;\n// see method [Request.SetMultipartFields]\nfunc (r *Request) SetMultipartField(fieldName, fileName, contentType string, reader io.Reader) *Request {\n\tr.isMultiPart = true\n\tr.multipartFields = append(r.multipartFields, &MultipartField{\n\t\tName:        fieldName,\n\t\tFileName:    fileName,\n\t\tContentType: contentType,\n\t\tReader:      reader,\n\t})\n\treturn r\n}\n\n// SetMultipartFields method sets multiple data fields using [io.Reader] for multipart upload.\n//\n// Resty provides an optional multipart live upload progress count in bytes; see\n// [MultipartField].ProgressCallback and [MultipartFieldProgress]\n//\n// For Example:\n//\n//\tclient.R().SetMultipartFields(\n//\t\t&resty.MultipartField{\n//\t\t\tName:        \"uploadManifest1\",\n//\t\t\tFileName:    \"upload-file-1.json\",\n//\t\t\tContentType: \"application/json\",\n//\t\t\tReader:      strings.NewReader(`{\"input\": {\"name\": \"Uploaded document 1\", \"_filename\" : [\"file1.txt\"]}}`),\n//\t\t},\n//\t\t&resty.MultipartField{\n//\t\t\tName:        \"uploadManifest2\",\n//\t\t\tFileName:    \"upload-file-2.json\",\n//\t\t\tContentType: \"application/json\",\n//\t\t\tFilePath:    \"/path/to/upload-file-2.json\",\n//\t\t},\n//\t\t&resty.MultipartField{\n//\t\t\tName:             \"image-file1\",\n//\t\t\tFileName:         \"image-file1.png\",\n//\t\t\tContentType:      \"image/png\",\n//\t\t\tReader:           bytes.NewReader(fileBytes),\n//\t\t\tProgressCallback: func(mp MultipartFieldProgress) {\n//\t\t\t\t// use the progress details\n//\t\t\t},\n//\t\t},\n//\t\t&resty.MultipartField{\n//\t\t\tName:             \"image-file2\",\n//\t\t\tFileName:         \"image-file2.png\",\n//\t\t\tContentType:      \"image/png\",\n//\t\t\tReader:           imageFile2, // instance of *os.File\n//\t\t\tProgressCallback: func(mp MultipartFieldProgress) {\n//\t\t\t\t// use the progress details\n//\t\t\t},\n//\t\t})\n//\n// If you have a `slice` of fields already, then call-\n//\n//\tclient.R().SetMultipartFields(fields...)\nfunc (r *Request) SetMultipartFields(fields ...*MultipartField) *Request {\n\tr.isMultiPart = true\n\tr.multipartFields = append(r.multipartFields, fields...)\n\treturn r\n}\n\n// SetMultipartBoundary method sets the custom multipart boundary for the multipart request.\n// Typically, the `mime/multipart` package generates a random multipart boundary if not provided.\nfunc (r *Request) SetMultipartBoundary(boundary string) *Request {\n\tr.multipartBoundary = boundary\n\treturn r\n}\n\n// SetContentLength method sets the given content length value in the HTTP request.\n// By default, Resty won't set `Content-Length`.\n//\n//\tclient.R().SetContentLength(3486547657)\nfunc (r *Request) SetContentLength(v int64) *Request {\n\tr.contentLength = v\n\tr.isContentLengthSet = true\n\treturn r\n}\n\n// SetBasicAuth method sets the basic authentication header in the current HTTP request.\n//\n// For Example:\n//\n//\tAuthorization: Basic <base64-encoded-value>\n//\n// To set the header for username \"go-resty\" and password \"welcome\"\n//\n//\tclient.R().SetBasicAuth(\"go-resty\", \"welcome\")\n//\n// It overrides the credentials set by method [Client.SetBasicAuth].\nfunc (r *Request) SetBasicAuth(username, password string) *Request {\n\tr.credentials = &credentials{Username: username, Password: password}\n\treturn r\n}\n\n// SetAuthToken method sets the auth token header(Default Scheme: Bearer) in the current HTTP request. Header example:\n//\n//\tAuthorization: Bearer <auth-token-value-comes-here>\n//\n// For Example: To set auth token BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F\n//\n//\tclient.R().SetAuthToken(\"BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F\")\n//\n// It overrides the Auth token set by method [Client.SetAuthToken].\nfunc (r *Request) SetAuthToken(authToken string) *Request {\n\tr.AuthToken = authToken\n\treturn r\n}\n\n// SetAuthScheme method sets the auth token scheme type in the HTTP request.\n//\n// Example Header value structure:\n//\n//\tAuthorization: <auth-scheme-value-set-here> <auth-token-value>\n//\n// For Example: To set the scheme to use OAuth\n//\n//\tclient.R().SetAuthScheme(\"OAuth\")\n//\n//\t// The outcome will be -\n//\tAuthorization: OAuth <auth-token-value>\n//\n// Information about Auth schemes can be found in [RFC 7235], IANA [HTTP Auth schemes]\n//\n// It overrides the `Authorization` scheme set by method [Client.SetAuthScheme].\n//\n// [RFC 7235]: https://tools.ietf.org/html/rfc7235\n// [HTTP Auth schemes]: https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes\nfunc (r *Request) SetAuthScheme(scheme string) *Request {\n\tr.AuthScheme = scheme\n\treturn r\n}\n\n// SetHeaderAuthorizationKey method sets the given HTTP header name for Authorization in the request.\n//\n// It overrides the `Authorization` header name set by method [Client.SetHeaderAuthorizationKey].\n//\n//\tclient.R().SetHeaderAuthorizationKey(\"X-Custom-Authorization\")\nfunc (r *Request) SetHeaderAuthorizationKey(k string) *Request {\n\tr.HeaderAuthorizationKey = k\n\treturn r\n}\n\n// SetResponseSaveFileName method sets the output file for the current HTTP request. The current\n// HTTP response will be saved in the given file. It is similar to the `curl -o` flag.\n//\n// Absolute path or relative path can be used.\n//\n// If it is a relative path, then the output file goes under the output directory, as mentioned\n// in the [Client.SetResponseSaveDirectory].\n//\n//\tclient.R().\n//\t\tSetResponseSaveFileName(\"/Users/jeeva/Downloads/ReplyWithHeader-v5.1-beta.zip\").\n//\t\tGet(\"http://bit.ly/1LouEKr\")\n//\n// NOTE: In this scenario\n//   - [Response.BodyBytes] might be nil.\n//   - [Response].Body might have been already read.\nfunc (r *Request) SetResponseSaveFileName(file string) *Request {\n\tr.ResponseSaveFileName = file\n\tr.SetResponseSaveToFile(true)\n\treturn r\n}\n\n// SetResponseSaveToFile method used to enable the save response option for the current requests\n//\n//\tclient.R().SetResponseSaveToFile(true)\n//\n// Resty determines the save filename in the following order -\n//   - [Request.SetResponseSaveFileName]\n//   - Content-Disposition header\n//   - Request URL using [path.Base]\n//   - Request URL hostname if path is empty or \"/\"\n//\n// It overrides the value set at the client instance level, see [Client.SetResponseSaveToFile]\nfunc (r *Request) SetResponseSaveToFile(save bool) *Request {\n\tr.IsResponseSaveToFile = save\n\treturn r\n}\n\n// SetCloseConnection method sets variable `Close` in HTTP request struct with the given\n// value. More info: https://golang.org/src/net/http/request.go\n//\n// It overrides the value set at the client instance level, see [Client.SetCloseConnection]\nfunc (r *Request) SetCloseConnection(close bool) *Request {\n\tr.IsCloseConnection = close\n\treturn r\n}\n\n// SetResponseDoNotParse method instructs Resty not to parse the response body automatically.\n//\n// Resty exposes the raw response body as [io.ReadCloser]. If you use it, do not\n// forget to close the body, otherwise, you might get into connection leaks, and connection\n// reuse may not happen.\n//\n// NOTE: The default [Response] middlewares are not executed when using this option. User\n// takes over the control of handling response body from Resty.\nfunc (r *Request) SetResponseDoNotParse(notParse bool) *Request {\n\tr.IsResponseDoNotParse = notParse\n\treturn r\n}\n\n// SetResponseBodyLimit method sets a maximum body size limit in bytes on response,\n// avoid reading too much data to memory.\n//\n// Client will return [resty.ErrResponseBodyTooLarge] if the body size of the body\n// in the uncompressed response is larger than the limit.\n// Body size limit will not be enforced in the following cases:\n//   - ResponseBodyLimit <= 0, which is the default behavior.\n//   - [Request.SetResponseSaveFileName] is called to save response data to the file.\n//   - \"DoNotParseResponse\" is set for client or request.\n//\n// It overrides the value set at the client instance level, see [Client.SetResponseBodyLimit]\nfunc (r *Request) SetResponseBodyLimit(v int64) *Request {\n\tr.ResponseBodyLimit = v\n\treturn r\n}\n\n// SetResponseBodyUnlimitedReads method is to turn on/off the response body in memory\n// that provides an ability to do unlimited reads.\n//\n// It overrides the value set at the client level; see [Client.SetResponseBodyUnlimitedReads]\n//\n// Unlimited reads are possible in a few scenarios, even without enabling it.\n//   - When debug mode is enabled\n//\n// NOTE: Use with care\n//   - Turning on this feature keeps the response body in memory, which might cause additional memory usage.\nfunc (r *Request) SetResponseBodyUnlimitedReads(b bool) *Request {\n\tr.IsResponseBodyUnlimitedReads = b\n\treturn r\n}\n\n// SetPathParam method sets a single URL path key-value pair in the\n// Resty current request instance.\n//\n//\tclient.R().SetPathParam(\"userId\", \"sample@sample.com\")\n//\n//\tResult:\n//\t   URL - /v1/users/{userId}/details\n//\t   Composed URL - /v1/users/sample@sample.com/details\n//\n//\tclient.R().SetPathParam(\"path\", \"groups/developers\")\n//\n//\tResult:\n//\t   URL - /v1/users/{path}/details\n//\t   Composed URL - /v1/users/groups%2Fdevelopers/details\n//\n// It replaces the value of the key while composing the request URL.\n// The values will be escaped using function [url.PathEscape].\n//\n// It overrides the path parameter set at the client instance level.\nfunc (r *Request) SetPathParam(param, value string) *Request {\n\tr.PathParams[param] = url.PathEscape(value)\n\treturn r\n}\n\n// SetPathParamAny method sets a single URL path key-value pair in the\n// current request instance.\n//\n// It is similar to [Request.SetPathParam] but accepts any type as the value and converts\n// it to a string using predefined formatting rules (integers, bools, time.Time, etc.).\n//\n//\tclient.R().SetPathParamAny(\"userId\", 12345)\n//\n//\tResult:\n//\t   URL - /v1/users/{userId}/details\n//\t   Composed URL - /v1/users/12345/details\n//\n// It replaces the value of the key while composing the request URL.\n// The value will be escaped using [url.PathEscape] function.\n//\n// It overrides the path parameter set at the client instance level.\n//\n// See [Client.SetPathParamAny].\nfunc (r *Request) SetPathParamAny(param string, value any) *Request {\n\tstrVal := formatAnyToString(value)\n\tr.PathParams[param] = url.PathEscape(strVal)\n\treturn r\n}\n\n// SetPathParams method sets multiple URL path key-value pairs at one go in the\n// Resty current request instance.\n//\n//\tclient.R().SetPathParams(map[string]string{\n//\t\t\"userId\":       \"sample@sample.com\",\n//\t\t\"subAccountId\": \"100002\",\n//\t\t\"path\":         \"groups/developers\",\n//\t})\n//\n//\tResult:\n//\t   URL - /v1/users/{userId}/{subAccountId}/{path}/details\n//\t   Composed URL - /v1/users/sample@sample.com/100002/groups%2Fdevelopers/details\n//\n// It replaces the value of the key while composing the request URL.\n// The values will be escaped using function [url.PathEscape].\n//\n// It overrides the path parameter set at the client instance level.\nfunc (r *Request) SetPathParams(params map[string]string) *Request {\n\tfor p, v := range params {\n\t\tr.SetPathParam(p, v)\n\t}\n\treturn r\n}\n\n// SetPathRawParam method sets a single URL path key-value pair in the\n// Resty current request instance without path escape.\n//\n//\tclient.R().SetPathRawParam(\"userId\", \"sample@sample.com\")\n//\n//\tResult:\n//\t   URL - /v1/users/{userId}/details\n//\t   Composed URL - /v1/users/sample@sample.com/details\n//\n//\tclient.R().SetPathRawParam(\"path\", \"groups/developers\")\n//\n//\tResult:\n//\t   URL - /v1/users/{path}/details\n//\t   Composed URL - /v1/users/groups/developers/details\n//\n// It replaces the value of the key while composing the request URL.\n// The value will be used as-is, no path escape applied.\n//\n// It overrides the raw path parameter set at the client instance level.\nfunc (r *Request) SetPathRawParam(param, value string) *Request {\n\tr.PathParams[param] = value\n\treturn r\n}\n\n// SetPathRawParamAny method sets a single URL path key-value pair in the\n// current request instance without path escape.\n//\n// It is similar to [Request.SetPathRawParam] but accepts any type as the value and converts\n// it to a string using predefined formatting rules (integers, bools, time.Time, etc.).\n//\n//\tclient.R().SetPathRawParamAny(\"userId\", 12345)\n//\n//\tResult:\n//\t   URL - /v1/users/{userId}/details\n//\t   Composed URL - /v1/users/12345/details\n//\n// It replaces the value of the key while composing the request URL.\n// The value will be used as-is, no path escape applied.\n//\n// It overrides the raw path parameter set at the client instance level.\n//\n// See [Client.SetPathRawParamAny].\nfunc (r *Request) SetPathRawParamAny(param string, value any) *Request {\n\tstrVal := formatAnyToString(value)\n\tr.PathParams[param] = strVal\n\treturn r\n}\n\n// SetPathRawParams method sets multiple URL path key-value pairs at one go in the\n// Resty current request instance without path escape.\n//\n//\tclient.R().SetPathParams(map[string]string{\n//\t\t\"userId\": \"sample@sample.com\",\n//\t\t\"subAccountId\": \"100002\",\n//\t\t\"path\":         \"groups/developers\",\n//\t})\n//\n//\tResult:\n//\t   URL - /v1/users/{userId}/{subAccountId}/{path}/details\n//\t   Composed URL - /v1/users/sample@sample.com/100002/groups/developers/details\n//\n// It replaces the value of the key while composing the request URL.\n// The value will be used as-is, no path escape applied.\n//\n// It overrides the raw path parameter set at the client instance level.\nfunc (r *Request) SetPathRawParams(params map[string]string) *Request {\n\tfor p, v := range params {\n\t\tr.SetPathRawParam(p, v)\n\t}\n\treturn r\n}\n\n// SetResponseExpectContentType method allows to provide fallback `Content-Type`\n// for automatic unmarshalling when the `Content-Type` response header is unavailable.\nfunc (r *Request) SetResponseExpectContentType(contentType string) *Request {\n\tr.ResponseExpectContentType = contentType\n\treturn r\n}\n\n// SetResponseForceContentType method provides a strong sense of response `Content-Type` for\n// automatic unmarshalling. Resty gives this a higher priority than the `Content-Type`\n// response header.\n//\n// This means that if both [Request.SetResponseForceContentType] is set and\n// the response `Content-Type` is available, `SetResponseForceContentType` value will win.\nfunc (r *Request) SetResponseForceContentType(contentType string) *Request {\n\tr.ResponseForceContentType = contentType\n\treturn r\n}\n\n// SetJSONEscapeHTML method enables or disables the HTML escape on JSON marshal.\n// By default, escape HTML is `true`.\n//\n// NOTE: This option only applies to the standard JSON Marshaller used by Resty.\n//\n// It overrides the value set at the client instance level, see [Client.SetJSONEscapeHTML]\nfunc (r *Request) SetJSONEscapeHTML(b bool) *Request {\n\tr.jsonEscapeHTML = b\n\treturn r\n}\n\n// SetCookie method appends a single cookie in the current request instance.\n//\n//\tclient.R().SetCookie(&http.Cookie{\n//\t\t\t\tName:\"go-resty\",\n//\t\t\t\tValue:\"This is cookie value\",\n//\t\t\t})\n//\n// NOTE: Method appends the Cookie value into existing Cookie even if its already existing.\nfunc (r *Request) SetCookie(hc *http.Cookie) *Request {\n\tr.Cookies = append(r.Cookies, hc)\n\treturn r\n}\n\n// SetCookies method sets an array of cookies in the current request instance.\n//\n//\tcookies := []*http.Cookie{\n//\t\t&http.Cookie{\n//\t\t\tName:\"go-resty-1\",\n//\t\t\tValue:\"This is cookie 1 value\",\n//\t\t},\n//\t\t&http.Cookie{\n//\t\t\tName:\"go-resty-2\",\n//\t\t\tValue:\"This is cookie 2 value\",\n//\t\t},\n//\t}\n//\n//\t// Setting a cookies into resty's current request\n//\tclient.R().SetCookies(cookies)\n//\n// NOTE: Method appends the Cookie value into existing Cookie even if its already existing.\nfunc (r *Request) SetCookies(rs []*http.Cookie) *Request {\n\tr.Cookies = append(r.Cookies, rs...)\n\treturn r\n}\n\n// SetTimeout method is used to set a timeout for the current request\n//\n//\tclient.R().SetTimeout(1 * time.Minute)\n//\n// It overrides the timeout set at the client instance level, See [Client.SetTimeout]\n//\n// NOTE: Resty uses [context.WithTimeout] on the request, it does not use [http.Client.Timeout]\nfunc (r *Request) SetTimeout(timeout time.Duration) *Request {\n\tr.Timeout = timeout\n\treturn r\n}\n\n// SetLogger method sets given writer for logging Resty request and response details.\n// By default, requests and responses inherit their logger from the client.\n//\n// Compliant to interface [resty.Logger].\n//\n// It overrides the logger value set at the client instance level.\nfunc (r *Request) SetLogger(l Logger) *Request {\n\tr.log = l\n\treturn r\n}\n\n// SetDebug method enables the debug mode on the current request. It logs\n// the details current request and response.\n//\n//\tclient.R().SetDebug(true)\n//\n// It overrides the debug value set at the client instance level.\n//   - For [Request], it logs information such as HTTP verb, Relative URL path,\n//     Host, Headers, and Body if it has one.\n//   - For [Response], it logs information such as Status, Response Time, Headers,\n//     and Body if it has one.\nfunc (r *Request) SetDebug(d bool) *Request {\n\tr.IsDebug = d\n\treturn r\n}\n\n// AddRetryConditions method adds one or more retry condition functions into the request.\n// These retry conditions are executed to determine if the request can be retried.\n// The request will retry if any functions return `true`, otherwise return `false`.\n//\n// NOTE:\n//   - Retry conditions are executed on each retry attempt.\n//   - Default retry conditions are executed first.\n//   - Client-level retry conditions are applied to all requests.\n//   - Request-level retry conditions are executed before client-level retry conditions.\n//     See [Client.AddRetryConditions], [Request.SetRetryConditions]\n//   - Once a retry condition returns true, the remaining retry conditions are not executed.\n//   - Retry conditions are executed in the order in which they are added.\nfunc (r *Request) AddRetryConditions(conditions ...RetryConditionFunc) *Request {\n\tr.retryConditions = append(r.retryConditions, conditions...)\n\treturn r\n}\n\n// SetRetryConditions method overwrites the retry conditions in the request.\n// These retry conditions are executed to determine if the request can be retried.\n// The request will retry if any function returns `true`, otherwise return `false`.\n//\n// NOTE:\n//   - It overwrites the existing retry conditions.\n//   - See [Request.AddRetryConditions] method for more details.\nfunc (r *Request) SetRetryConditions(conditions ...RetryConditionFunc) *Request {\n\tr.retryConditions = conditions\n\tr.isSetRetryConditions = true\n\treturn r\n}\n\n// AddRetryHooks method adds one or more side-effecting retry hooks in the request.\n//\n// NOTE:\n//   - Retry hooks are executed on each retry attempt.\n//   - The request-level retry hooks are executed first before client-level hooks.\n//     See [Client.AddRetryHooks]\n//   - Retry hooks are executed in the order in which they are added.\nfunc (r *Request) AddRetryHooks(hooks ...RetryHookFunc) *Request {\n\tr.retryHooks = append(r.retryHooks, hooks...)\n\treturn r\n}\n\n// SetRetryHooks method overwrites side-effecting retry hooks in the request.\n//\n// NOTE:\n//   - It overwrites the existing retry hooks.\n//   - See [Request.AddRetryHooks] method for more details.\nfunc (r *Request) SetRetryHooks(hooks ...RetryHookFunc) *Request {\n\tr.retryHooks = hooks\n\tr.isSetRetryHooks = true\n\treturn r\n}\n\n// SetRetryCount method enables retry on Resty client and allows you\n// to set no. of retry count.\n//\n//\tfirst attempt + retry count = total attempts\n//\n// See [Request.SetRetryDelayStrategy]\n//\n// NOTE:\n//   - By default, Resty only does retry on idempotent HTTP verb, [RFC 9110 Section 9.2.2], [RFC 9110 Section 18.2]\n//\n// [RFC 9110 Section 9.2.2]: https://datatracker.ietf.org/doc/html/rfc9110.html#name-idempotent-methods\n// [RFC 9110 Section 18.2]: https://datatracker.ietf.org/doc/html/rfc9110.html#name-method-registration\nfunc (r *Request) SetRetryCount(count int) *Request {\n\tr.RetryCount = count\n\treturn r\n}\n\n// SetRetryWaitTime method sets the default wait time for sleep before retrying\n//\n// Default is 100 milliseconds.\nfunc (r *Request) SetRetryWaitTime(waitTime time.Duration) *Request {\n\tr.RetryWaitTime = waitTime\n\treturn r\n}\n\n// SetRetryMaxWaitTime method sets the max wait time for sleep before retrying\n//\n// Default is 2 seconds.\nfunc (r *Request) SetRetryMaxWaitTime(maxWaitTime time.Duration) *Request {\n\tr.RetryMaxWaitTime = maxWaitTime\n\treturn r\n}\n\n// SetRetryDelayStrategy method used to set the custom Retry delay strategy on request,\n// it is used to get wait time before each retry. It overrides the retry delay\n// strategy set at the client instance level, see [Client.SetRetryDelayStrategy]\n//\n// By default, Resty employs the capped exponential backoff with a jitter delay strategy.\nfunc (r *Request) SetRetryDelayStrategy(rs RetryDelayStrategyFunc) *Request {\n\tr.RetryDelayStrategy = rs\n\treturn r\n}\n\n// SetRetryDefaultConditions method is used to enable/disable the Resty's default\n// retry conditions on request level\n//\n// It overrides value set at the client instance level, see [Client.SetRetryDefaultConditions]\nfunc (r *Request) SetRetryDefaultConditions(b bool) *Request {\n\tr.IsRetryDefaultConditions = b\n\treturn r\n}\n\n// SetRetryAllowNonIdempotent method is used to enable/disable non-idempotent HTTP\n// methods retry. By default, Resty only allows idempotent HTTP methods, see\n// [RFC 9110 Section 9.2.2], [RFC 9110 Section 18.2]\n//\n// It overrides value set at the client instance level, see [Client.SetRetryAllowNonIdempotent]\n//\n// [RFC 9110 Section 9.2.2]: https://datatracker.ietf.org/doc/html/rfc9110.html#name-idempotent-methods\n// [RFC 9110 Section 18.2]: https://datatracker.ietf.org/doc/html/rfc9110.html#name-method-registration\nfunc (r *Request) SetRetryAllowNonIdempotent(b bool) *Request {\n\tr.IsRetryAllowNonIdempotent = b\n\treturn r\n}\n\n// SetTrace method is used to turn on/off the trace capability at the request level.\n// It provides an insight into the request lifecycle using [httptrace.ClientTrace].\n//\n//\tclient := resty.New()\n//\tdefer client.Close()\n//\n//\tresp, err := client.R().\n//\t\tSetTrace(true).\n//\t\tGet(\"https://httpbin.org/get\")\n//\tfmt.Println(\"Error:\", err)\n//\tfmt.Println(\"Trace Info:\", resp.Request.TraceInfo())\n//\n// See [Client.SetTrace]\nfunc (r *Request) SetTrace(t bool) *Request {\n\tr.IsTrace = t\n\treturn r\n}\n\n// SetCurlCmdGenerate method is used to turn on/off the generate curl command for the current request.\n//\n// By default, Resty does not log the curl command in the debug log since it has the potential\n// to leak sensitive data unless explicitly enabled via [Request.SetCurlCmdDebugLog] or\n// [Client.SetCurlCmdDebugLog].\n//\n// It overrides the options set by the [Client.SetCurlCmdGenerate]\n//\n// NOTE: Use with care.\n//   - Potential to leak sensitive data from [Request] and [Response] in the debug log\n//     when the debug log option is enabled.\n//   - Additional memory usage since the request body was reread.\n//   - curl body is not generated for [io.Reader] and multipart request flow.\nfunc (r *Request) SetCurlCmdGenerate(b bool) *Request {\n\tr.isCurlCmdGenerate = b\n\treturn r\n}\n\n// SetCurlCmdDebugLog method enables the curl command to be logged in the debug log\n// for the current request.\n//\n// It can be overridden at the request level; see [Client.SetCurlCmdDebugLog]\nfunc (r *Request) SetCurlCmdDebugLog(b bool) *Request {\n\tr.isCurlCmdDebugLog = b\n\treturn r\n}\n\n// CurlCmd method generates the curl command for the request.\nfunc (r *Request) CurlCmd() string {\n\treturn r.generateCurlCommand()\n}\n\nfunc (r *Request) generateCurlCommand() string {\n\tif !r.isCurlCmdGenerate {\n\t\treturn \"\"\n\t}\n\tif len(r.curlCmdString) > 0 {\n\t\treturn r.curlCmdString\n\t}\n\tif r.RawRequest == nil {\n\t\tif err := r.client.executeRequestMiddlewares(r); err != nil {\n\t\t\tr.log.Errorf(\"%v\", err)\n\t\t\treturn \"\"\n\t\t}\n\t}\n\tr.curlCmdString = buildCurlCmd(r)\n\treturn r.curlCmdString\n}\n\n// SetQueryParamsUnescape method sets the choice of unescape query parameters for the request URL.\n// To prevent broken URL, Resty replaces space (\" \") with \"+\" in the query parameters.\n//\n// This method overrides the value set by [Client.SetQueryParamsUnescape]\n//\n// NOTE: Request failure is possible due to non-standard usage of Unescaped Query Parameters.\nfunc (r *Request) SetQueryParamsUnescape(unescape bool) *Request {\n\tr.unescapeQueryParams = unescape\n\treturn r\n}\n\n// SetMethodGetAllowPayload method allows the GET method with payload on the request level.\n// By default, Resty does not allow.\n//\n//\tclient.R().SetMethodGetAllowPayload(true)\n//\n// It overrides the option set by the [Client.SetMethodGetAllowPayload]\nfunc (r *Request) SetMethodGetAllowPayload(allow bool) *Request {\n\tr.IsMethodGetAllowPayload = allow\n\treturn r\n}\n\n// SetMethodDeleteAllowPayload method allows the DELETE method with payload on the request level.\n// By default, Resty does not allow.\n//\n//\tclient.R().SetMethodDeleteAllowPayload(true)\n//\n// More info, refer to GH#881\n//\n// It overrides the option set by the [Client.SetMethodDeleteAllowPayload]\nfunc (r *Request) SetMethodDeleteAllowPayload(allow bool) *Request {\n\tr.IsMethodDeleteAllowPayload = allow\n\treturn r\n}\n\n// TraceInfo method returns the trace info for the request.\n// If either the [Client.EnableTrace] or [Request.EnableTrace] function has not been called\n// before the request is made, an empty [resty.TraceInfo] object is returned.\nfunc (r *Request) TraceInfo() TraceInfo {\n\tct := r.trace\n\n\tif ct == nil {\n\t\treturn TraceInfo{}\n\t}\n\n\tct.lock.RLock()\n\tdefer ct.lock.RUnlock()\n\n\tti := TraceInfo{\n\t\tDNSLookup:      0,\n\t\tTCPConnTime:    0,\n\t\tServerTime:     0,\n\t\tIsConnReused:   ct.gotConnInfo.Reused,\n\t\tIsConnWasIdle:  ct.gotConnInfo.WasIdle,\n\t\tConnIdleTime:   ct.gotConnInfo.IdleTime,\n\t\tRequestAttempt: r.Attempt,\n\t}\n\n\tif !ct.dnsStart.IsZero() && !ct.dnsDone.IsZero() {\n\t\tti.DNSLookup = ct.dnsDone.Sub(ct.dnsStart)\n\t}\n\n\tif !ct.tlsHandshakeDone.IsZero() && !ct.tlsHandshakeStart.IsZero() {\n\t\tti.TLSHandshake = ct.tlsHandshakeDone.Sub(ct.tlsHandshakeStart)\n\t}\n\n\tif !ct.gotFirstResponseByte.IsZero() && !ct.gotConn.IsZero() {\n\t\tti.ServerTime = ct.gotFirstResponseByte.Sub(ct.gotConn)\n\t}\n\n\t// Calculate the total time accordingly when connection is reused,\n\t// and DNS start and get conn time may be zero if the request is invalid.\n\t// See issue #1016.\n\trequestStartTime := r.StartTime\n\tif ct.gotConnInfo.Reused && !ct.getConn.IsZero() {\n\t\trequestStartTime = ct.getConn\n\t} else if !ct.dnsStart.IsZero() {\n\t\trequestStartTime = ct.dnsStart\n\t}\n\tti.TotalTime = ct.endTime.Sub(requestStartTime)\n\n\t// Only calculate on successful connections\n\tif !ct.connectDone.IsZero() {\n\t\tti.TCPConnTime = ct.connectDone.Sub(ct.dnsDone)\n\t}\n\n\t// Only calculate on successful connections\n\tif !ct.gotConn.IsZero() {\n\t\tti.ConnTime = ct.gotConn.Sub(ct.getConn)\n\t}\n\n\t// Only calculate on successful connections\n\tif !ct.gotFirstResponseByte.IsZero() {\n\t\tti.ResponseTime = ct.endTime.Sub(ct.gotFirstResponseByte)\n\t}\n\n\t// Capture remote address info when connection is non-nil\n\tif ct.gotConnInfo.Conn != nil {\n\t\tti.RemoteAddr = ct.gotConnInfo.Conn.RemoteAddr().String()\n\t}\n\n\treturn ti\n}\n\n//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\n// HTTP verb method starts here\n//_______________________________________________________________________\n\n// Get method does GET HTTP request. It's defined in section 9.3.1 of [RFC 9110].\n//\n// [RFC 9110]: https://datatracker.ietf.org/doc/html/rfc9110.html#section-9.3.1\nfunc (r *Request) Get(url string) (*Response, error) {\n\treturn r.Execute(MethodGet, url)\n}\n\n// Head method does HEAD HTTP request. It's defined in section 9.3.2 of [RFC 9110].\n//\n// [RFC 9110]: https://datatracker.ietf.org/doc/html/rfc9110.html#section-9.3.2\nfunc (r *Request) Head(url string) (*Response, error) {\n\treturn r.Execute(MethodHead, url)\n}\n\n// Post method does POST HTTP request. It's defined in section 9.3.3 of [RFC 9110].\n//\n// [RFC 9110]: https://datatracker.ietf.org/doc/html/rfc9110.html#section-9.3.3\nfunc (r *Request) Post(url string) (*Response, error) {\n\treturn r.Execute(MethodPost, url)\n}\n\n// Put method does PUT HTTP request. It's defined in section 9.3.4 of [RFC 9110].\n//\n// [RFC 9110]: https://datatracker.ietf.org/doc/html/rfc9110.html#section-9.3.4\nfunc (r *Request) Put(url string) (*Response, error) {\n\treturn r.Execute(MethodPut, url)\n}\n\n// Patch method does PATCH HTTP request. It's defined in section 2 of [RFC 5789].\n//\n// [RFC 5789]: https://datatracker.ietf.org/doc/html/rfc5789.html#section-2\nfunc (r *Request) Patch(url string) (*Response, error) {\n\treturn r.Execute(MethodPatch, url)\n}\n\n// Delete method does DELETE HTTP request. It's defined in section 9.3.5 of [RFC 9110].\n//\n// [RFC 9110]: https://datatracker.ietf.org/doc/html/rfc9110.html#section-9.3.5\nfunc (r *Request) Delete(url string) (*Response, error) {\n\treturn r.Execute(MethodDelete, url)\n}\n\n// Options method does OPTIONS HTTP request. It's defined in section 9.3.7 of [RFC 9110].\n//\n// [RFC 9110]: https://datatracker.ietf.org/doc/html/rfc9110.html#section-9.3.7\nfunc (r *Request) Options(url string) (*Response, error) {\n\treturn r.Execute(MethodOptions, url)\n}\n\n// Trace method does TRACE HTTP request. It's defined in section 9.3.8 of [RFC 9110].\n//\n// [RFC 9110]: https://datatracker.ietf.org/doc/html/rfc9110.html#section-9.3.8\nfunc (r *Request) Trace(url string) (*Response, error) {\n\treturn r.Execute(MethodTrace, url)\n}\n\n// Send method performs the HTTP request using the method and URL already defined\n// for current [Request].\n//\n//\tres, err := client.R().\n//\t\tSetMethod(resty.MethodGet).\n//\t\tSetURL(\"http://httpbin.org/get\").\n//\t\tSend()\nfunc (r *Request) Send() (*Response, error) {\n\treturn r.Execute(r.Method, r.URL)\n}\n\n// Execute method performs the HTTP request with the given HTTP method and URL\n// for current [Request].\n//\n//\tresp, err := client.R().Execute(resty.MethodGet, \"http://httpbin.org/get\")\nfunc (r *Request) Execute(method, url string) (res *Response, err error) {\n\tdefer func() {\n\t\tif rec := recover(); rec != nil {\n\t\t\tif err, ok := rec.(error); ok {\n\t\t\t\tr.client.onPanicHooks(r, err)\n\t\t\t} else {\n\t\t\t\tr.client.onPanicHooks(r, fmt.Errorf(\"panic %v\", rec))\n\t\t\t}\n\t\t\tpanic(rec)\n\t\t}\n\t}()\n\n\tr.Method = method\n\n\tif r.RetryCount < 0 {\n\t\tr.RetryCount = 0 // default behavior is no retry\n\t}\n\n\tisIdempotent := r.isIdempotent()\n\tvar backoff *backoffWithJitter\n\tif r.RetryCount > 0 && isIdempotent {\n\t\tbackoff = newBackoffWithJitter(r.RetryWaitTime, r.RetryMaxWaitTime)\n\t\tr.SetCorrelationID(newGUID())\n\t}\n\n\tretryConditions := append(r.retryConditions, r.client.retryConditions...)\n\tif r.isSetRetryConditions {\n\t\tretryConditions = r.retryConditions\n\t}\n\n\tretryHooks := append(r.retryHooks, r.client.retryHooks...)\n\tif r.isSetRetryHooks {\n\t\tretryHooks = r.retryHooks\n\t}\n\n\tisInvalidRequestErr := false\n\t// first attempt + retry count = total attempts\n\tfor i := 0; i <= r.RetryCount; i++ {\n\t\tr.Attempt++\n\t\terr = nil\n\t\tr.URL = url\n\t\tres, err = r.client.execute(r)\n\t\tif err != nil {\n\t\t\tif irErr, ok := err.(*invalidRequestError); ok {\n\t\t\t\terr = irErr.Err\n\t\t\t\tisInvalidRequestErr = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif r.Context().Err() != nil {\n\t\t\t\tif r.ctxCancelFunc != nil {\n\t\t\t\t\tr.ctxCancelFunc()\n\t\t\t\t\tr.ctxCancelFunc = nil\n\t\t\t\t}\n\t\t\t\tif !errors.Is(err, context.DeadlineExceeded) {\n\t\t\t\t\terr = wrapErrors(r.Context().Err(), err)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// we have reached the maximum no. of requests\n\t\t// or request method is not an idempotent\n\t\tif r.Attempt-1 == r.RetryCount || !isIdempotent {\n\t\t\tbreak\n\t\t}\n\n\t\tif backoff != nil {\n\t\t\tneedsRetry, isCtxDone := false, false\n\n\t\t\t// apply default retry conditions\n\t\t\tif r.IsRetryDefaultConditions {\n\t\t\t\tneedsRetry = applyRetryDefaultConditions(res, err)\n\t\t\t}\n\n\t\t\t// apply user-defined retry conditions if default one\n\t\t\t// is still false\n\t\t\tif !needsRetry && res != nil {\n\t\t\t\t// run user-defined retry conditions\n\t\t\t\tfor _, retryCondition := range retryConditions {\n\t\t\t\t\tif needsRetry = retryCondition(res, err); needsRetry {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// retry not required stop here\n\t\t\tif !needsRetry {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\t// by default reset file readers\n\t\t\tif err = r.resetFileReaders(); err != nil {\n\t\t\t\t// if any error in reset readers, stop here\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\t// run user-defined retry hooks\n\t\t\tfor _, retryHookFunc := range retryHooks {\n\t\t\t\tretryHookFunc(res, err)\n\t\t\t}\n\n\t\t\t// let's drain the response body, before retry wait\n\t\t\tdrainBody(res)\n\n\t\t\twaitDuration, waitErr := backoff.NextWaitDuration(r.client, res, err, r.Attempt)\n\t\t\tif waitErr != nil {\n\t\t\t\t// if any error in retry strategy, stop here\n\t\t\t\terr = wrapErrors(waitErr, err)\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\ttimer := time.NewTimer(waitDuration)\n\t\t\tselect {\n\t\t\tcase <-r.Context().Done():\n\t\t\t\tisCtxDone = true\n\t\t\t\terr = wrapErrors(r.Context().Err(), err)\n\t\t\t\tbreak\n\t\t\tcase <-timer.C:\n\t\t\t}\n\t\t\ttimer.Stop()\n\t\t\tif isCtxDone {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif r.isMultiPart {\n\t\tfor _, mf := range r.multipartFields {\n\t\t\tmf.close()\n\t\t}\n\t}\n\n\tr.IsDone = true\n\n\tif isInvalidRequestErr {\n\t\tr.client.onInvalidHooks(r, err)\n\t} else {\n\t\tr.client.onErrorHooks(r, res, err)\n\t}\n\n\tr.sendLoadBalancerFeedback(res, err)\n\tbackToBufPool(r.bodyBuf)\n\treturn\n}\n\n// Clone returns a deep copy of r with its context changed to ctx.\n// It does clone appropriate fields, reset, and reinitialize, so\n// [Request] can be used again.\n//\n// The body is not copied, but it's a reference to the original body.\n//\n//\treq := client.R().\n//\t\tSetBody(\"body\").\n//\t\tSetHeader(\"header\", \"value\")\n//\tclonedRequest := req.Clone(context.Background())\nfunc (r *Request) Clone(ctx context.Context) *Request {\n\tif ctx == nil {\n\t\tpanic(\"resty: Request.Clone nil context\")\n\t}\n\trr := new(Request)\n\t*rr = *r\n\n\t// set new context\n\trr.ctx = ctx\n\n\t// RawRequest should not copied, since its created on request execution flow.\n\trr.RawRequest = nil\n\n\t// clone values\n\trr.Header = r.Header.Clone()\n\trr.FormData = cloneURLValues(r.FormData)\n\trr.QueryParams = cloneURLValues(r.QueryParams)\n\trr.PathParams = maps.Clone(r.PathParams)\n\n\t// reset content length if not set by user\n\tif !r.isContentLengthSet {\n\t\trr.contentLength = 0\n\t}\n\n\t// clone basic auth\n\tif r.credentials != nil {\n\t\trr.credentials = r.credentials.Clone()\n\t}\n\n\t// clone cookies\n\tif l := len(r.Cookies); l > 0 {\n\t\trr.Cookies = make([]*http.Cookie, l)\n\t\tfor _, cookie := range r.Cookies {\n\t\t\trr.Cookies = append(rr.Cookies, cloneCookie(cookie))\n\t\t}\n\t}\n\n\t// create new interface for result and error\n\trr.Result = newInterface(r.Result)\n\trr.ResultError = newInterface(r.ResultError)\n\n\t// clone multipart fields\n\tif l := len(r.multipartFields); l > 0 {\n\t\trr.multipartFields = make([]*MultipartField, l)\n\t\tfor i, mf := range r.multipartFields {\n\t\t\trr.multipartFields[i] = mf.Clone()\n\t\t}\n\t}\n\n\t// reset values\n\trr.StartTime = time.Time{}\n\trr.Attempt = 0\n\trr.initTraceIfEnabled()\n\tr.values = make(map[string]any)\n\tr.multipartErrChan = nil\n\tr.ctxCancelFunc = nil\n\n\t// copy bodyBuf\n\tif r.bodyBuf != nil {\n\t\trr.bodyBuf = acquireBuffer()\n\t\trr.bodyBuf.Write(r.bodyBuf.Bytes())\n\t}\n\n\treturn rr\n}\n\n// Funcs method gets executed on request composition that passes the\n// current request instance to provided [RequestFunc], which could be\n// used to apply common/reusable logic to the given request instance.\n//\n//\tfunc addRequestContentType(r *Request) *Request {\n//\t\treturn r.SetHeader(\"Content-Type\", \"application/json\").\n//\t\t\tSetHeader(\"Accept\", \"application/json\")\n//\t}\n//\n//\tfunc addRequestQueryParams(page, size int) func(r *Request) *Request {\n//\t\treturn func(r *Request) *Request {\n//\t\t\treturn r.SetQueryParam(\"page\", strconv.Itoa(page)).\n//\t\t\t\tSetQueryParam(\"size\", strconv.Itoa(size)).\n//\t\t\t\tSetQueryParam(\"request_no\", strconv.Itoa(int(time.Now().Unix())))\n//\t\t}\n//\t}\n//\n//\tclient.R().\n//\t\tFuncs(addRequestContentType, addRequestQueryParams(1, 100)).\n//\t\tGet(\"https://localhost:8080/foobar\")\nfunc (r *Request) Funcs(funcs ...RequestFunc) *Request {\n\tfor _, f := range funcs {\n\t\tr = f(r)\n\t}\n\treturn r\n}\n\nfunc (r *Request) fmtBodyString(sl int) (body string) {\n\tbody = \"***** NO CONTENT *****\"\n\tif !r.isPayloadSupported() {\n\t\treturn\n\t}\n\n\tif _, ok := r.Body.(io.Reader); ok {\n\t\tbody = \"***** BODY IS io.Reader *****\"\n\t\treturn\n\t}\n\n\t// multipart or form-data\n\tif r.isMultiPart || r.isFormData {\n\t\tbodySize := r.bodyBuf.Len()\n\t\tif bodySize > sl {\n\t\t\tbody = fmt.Sprintf(\"***** REQUEST TOO LARGE (size - %d) *****\", bodySize)\n\t\t\treturn\n\t\t}\n\t\tbody = r.bodyBuf.String()\n\t\treturn\n\t}\n\n\t// request body data\n\tif r.Body == nil {\n\t\treturn\n\t}\n\tvar prtBodyBytes []byte\n\tvar err error\n\n\tcontentType := r.Header.Get(hdrContentTypeKey)\n\tctKey := inferContentTypeMapKey(contentType)\n\n\tkind := inferKind(r.Body)\n\tif jsonKey == ctKey &&\n\t\t(kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) {\n\t\tbuf := acquireBuffer()\n\t\tdefer releaseBuffer(buf)\n\t\tif err = encodeJSONEscapeHTMLIndent(buf, &r.Body, false, \"   \"); err == nil {\n\t\t\tprtBodyBytes = buf.Bytes()\n\t\t}\n\t} else if xmlKey == ctKey && kind == reflect.Struct {\n\t\tprtBodyBytes, err = xml.MarshalIndent(&r.Body, \"\", \"   \")\n\t} else {\n\t\tswitch b := r.Body.(type) {\n\t\tcase string:\n\t\t\tprtBodyBytes = []byte(b)\n\t\t\tif jsonKey == ctKey {\n\t\t\t\tprtBodyBytes = jsonIndent(prtBodyBytes)\n\t\t\t}\n\t\tcase []byte:\n\t\t\tbody = fmt.Sprintf(\"***** BODY IS byte(s) (size - %d) *****\", len(b))\n\t\t\treturn\n\t\t}\n\t}\n\n\tbodySize := len(prtBodyBytes)\n\tif bodySize > sl {\n\t\tbody = fmt.Sprintf(\"***** REQUEST TOO LARGE (size - %d) *****\", bodySize)\n\t\treturn\n\t}\n\n\tif prtBodyBytes != nil && err == nil {\n\t\tbody = string(prtBodyBytes)\n\t}\n\n\treturn\n}\n\nfunc (r *Request) initValuesMap() {\n\tif r.values == nil {\n\t\tr.values = make(map[string]any)\n\t}\n}\n\nfunc (r *Request) initTraceIfEnabled() {\n\tif r.IsTrace {\n\t\tr.trace = new(clientTrace)\n\t\tr.ctx = r.trace.createContext(r.Context())\n\t}\n}\n\nfunc (r *Request) isHeaderExists(k string) bool {\n\t_, f := r.Header[k]\n\treturn f\n}\n\nfunc (r *Request) isPayloadSupported() bool {\n\tif r.Method == \"\" {\n\t\tr.Method = MethodGet\n\t}\n\n\tif r.Method == MethodGet && r.IsMethodGetAllowPayload {\n\t\treturn true\n\t}\n\n\t// More info, refer to GH#881\n\tif r.Method == MethodDelete && r.IsMethodDeleteAllowPayload {\n\t\treturn true\n\t}\n\n\tif r.Method == MethodPost || r.Method == MethodPut || r.Method == MethodPatch {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc (r *Request) sendLoadBalancerFeedback(res *Response, err error) {\n\tif r.client.LoadBalancer() == nil {\n\t\treturn\n\t}\n\n\tsuccess := true\n\n\t// load balancer feedback mainly focuses on connection\n\t// failures and status code >= 500\n\t// so that we can prevent sending the request to\n\t// that server which may fail\n\tif err != nil {\n\t\tvar noe *net.OpError\n\t\tif errors.As(err, &noe) {\n\t\t\tsuccess = !errors.Is(noe.Err, syscall.ECONNREFUSED) || noe.Timeout()\n\t\t}\n\t}\n\tif success && res != nil &&\n\t\t(res.StatusCode() >= 500 && res.StatusCode() != http.StatusNotImplemented) {\n\t\tsuccess = false\n\t}\n\n\tr.client.LoadBalancer().Feedback(&RequestFeedback{\n\t\tBaseURL: r.baseURL,\n\t\tSuccess: success,\n\t\tAttempt: r.Attempt,\n\t})\n}\n\nfunc (r *Request) resetFileReaders() error {\n\tfor _, f := range r.multipartFields {\n\t\tif err := f.resetReader(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\n// https://datatracker.ietf.org/doc/html/rfc9110.html#name-idempotent-methods\n// https://datatracker.ietf.org/doc/html/rfc9110.html#name-method-registration\nvar idempotentMethods = map[string]struct{}{\n\tMethodDelete:  {},\n\tMethodGet:     {},\n\tMethodHead:    {},\n\tMethodOptions: {},\n\tMethodPut:     {},\n\tMethodTrace:   {},\n}\n\nfunc (r *Request) isIdempotent() bool {\n\t_, found := idempotentMethods[r.Method]\n\treturn found || r.IsRetryAllowNonIdempotent\n}\n\nfunc (r *Request) withTimeout() *http.Request {\n\tif _, found := r.Context().Deadline(); found {\n\t\treturn r.RawRequest\n\t}\n\tif r.Timeout > 0 {\n\t\tctx, ctxCancelFunc := context.WithTimeout(r.Context(), r.Timeout)\n\t\tr.ctxCancelFunc = ctxCancelFunc\n\t\treturn r.RawRequest.WithContext(ctx)\n\t}\n\treturn r.RawRequest\n}\n\nfunc jsonIndent(v []byte) []byte {\n\tbuf := acquireBuffer()\n\tdefer releaseBuffer(buf)\n\tif err := json.Indent(buf, v, \"\", \"   \"); err != nil {\n\t\treturn v\n\t}\n\treturn buf.Bytes()\n}\n"
  },
  {
    "path": "request_test.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n)\n\ntype AuthSuccess struct {\n\tID      string `xml:\"Id\"`\n\tMessage string `xml:\"Message\"`\n}\n\ntype AuthError struct {\n\tID, Message string\n}\n\nfunc TestGet(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tresp, err := dcnl().R().\n\t\tSetQueryParam(\"request_no\", strconv.FormatInt(time.Now().Unix(), 10)).\n\t\tGet(ts.URL + \"/\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"HTTP/1.1\", resp.Proto())\n\tassertEqual(t, \"200 OK\", resp.Status())\n\tassertEqual(t, \"TestGet: text response\", resp.String())\n\n\tlogResponse(t, resp)\n}\n\nfunc TestGetGH524(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tresp, err := dcnl().R().\n\t\tSetPathParams((map[string]string{\n\t\t\t\"userId\":       \"sample@sample.com\",\n\t\t\t\"subAccountId\": \"100002\",\n\t\t\t\"path\":         \"groups/developers\",\n\t\t})).\n\t\tSetQueryParam(\"request_no\", strconv.FormatInt(time.Now().Unix(), 10)).\n\t\tSetDebug(true).\n\t\tGet(ts.URL + \"/v1/users/{userId}/{subAccountId}/{path}/details\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, resp.Request.Header.Get(\"Content-Type\"), \"\") //  unable to reproduce reported issue\n}\n\nfunc TestRequestNegativeRetryCount(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tresp, err := dcnl().SetRetryCount(-1).R().Get(ts.URL + \"/\")\n\n\tassertNil(t, err)\n\tassertNotNil(t, resp)\n\tassertEqual(t, \"TestGet: text response\", resp.String())\n}\n\nfunc TestGetCustomUserAgent(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tresp, err := dcnlr().\n\t\tSetHeader(hdrUserAgentKey, \"Test Custom User agent\").\n\t\tSetQueryParam(\"request_no\", strconv.FormatInt(time.Now().Unix(), 10)).\n\t\tGet(ts.URL + \"/\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"HTTP/1.1\", resp.Proto())\n\tassertEqual(t, \"200 OK\", resp.Status())\n\tassertEqual(t, \"TestGet: text response\", resp.String())\n\n\tlogResponse(t, resp)\n}\n\nfunc TestGetClientParamRequestParam(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetQueryParam(\"client_param\", \"true\").\n\t\tSetQueryParams(map[string]string{\"req_1\": \"jeeva\", \"req_3\": \"jeeva3\"}).\n\t\tSetDebug(true)\n\tc.outputLogTo(io.Discard)\n\n\tresp, err := c.R().\n\t\tSetQueryParams(map[string]string{\"req_1\": \"req 1 value\", \"req_2\": \"req 2 value\"}).\n\t\tSetQueryParam(\"request_no\", strconv.FormatInt(time.Now().Unix(), 10)).\n\t\tSetHeader(hdrUserAgentKey, \"Test Custom User agent\").\n\t\tGet(ts.URL + \"/\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"HTTP/1.1\", resp.Proto())\n\tassertEqual(t, \"200 OK\", resp.Status())\n\tassertEqual(t, \"TestGet: text response\", resp.String())\n\n\tlogResponse(t, resp)\n}\n\nfunc TestGetRelativePath(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetBaseURL(ts.URL)\n\n\tresp, err := c.R().Get(\"mypage2\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"TestGet: text response from mypage2\", resp.String())\n\n\tlogResponse(t, resp)\n}\n\nfunc TestGet400Error(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tresp, err := dcnlr().Get(ts.URL + \"/mypage\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusBadRequest, resp.StatusCode())\n\tassertEqual(t, \"\", resp.String())\n\n\tlogResponse(t, resp)\n}\n\nfunc TestPostJSONStringSuccess(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetHeader(hdrContentTypeKey, \"application/json; charset=utf-8\").\n\t\tSetHeaders(map[string]string{hdrUserAgentKey: \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) go-resty v0.1\", hdrAcceptKey: \"application/json; charset=utf-8\"})\n\n\tresp, err := c.R().\n\t\tSetBody(`{\"username\":\"testuser\", \"password\":\"testpass\"}`).\n\t\tPost(ts.URL + \"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\tlogResponse(t, resp)\n\n\t// PostJSONStringError\n\tresp, err = c.R().\n\t\tSetBody(`{\"username\":\"testuser\" \"password\":\"testpass\"}`).\n\t\tPost(ts.URL + \"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusBadRequest, resp.StatusCode())\n\n\tlogResponse(t, resp)\n}\n\nfunc TestPostJSONBytesSuccess(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetHeader(hdrContentTypeKey, \"application/json; charset=utf-8\").\n\t\tSetHeaders(map[string]string{hdrUserAgentKey: \"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5) go-resty v0.7\", hdrAcceptKey: \"application/json; charset=utf-8\"})\n\n\tresp, err := c.R().\n\t\tSetBody([]byte(`{\"username\":\"testuser\", \"password\":\"testpass\"}`)).\n\t\tPost(ts.URL + \"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\tlogResponse(t, resp)\n}\n\nfunc TestPostJSONBytesIoReader(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetHeader(hdrContentTypeKey, \"application/json; charset=utf-8\")\n\n\tbodyBytes := []byte(`{\"username\":\"testuser\", \"password\":\"testpass\"}`)\n\n\tresp, err := c.R().\n\t\tSetBody(bytes.NewReader(bodyBytes)).\n\t\tPost(ts.URL + \"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\tlogResponse(t, resp)\n}\n\nfunc TestPostJSONStructSuccess(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tuser := &credentials{Username: \"testuser\", Password: \"testpass\"}\n\tassertEqual(t, \"Username: **********, Password: **********\", user.String())\n\n\tc := dcnl().SetJSONEscapeHTML(false)\n\tr := c.R().\n\t\tSetHeader(hdrContentTypeKey, \"application/json; charset=utf-8\").\n\t\tSetBody(user).\n\t\tSetResult(&AuthSuccess{})\n\n\trr := r.WithContext(context.Background())\n\tresp, err := rr.Post(ts.URL + \"/login\")\n\n\t_ = rr.Clone(context.Background())\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, int64(50), resp.Size())\n\n\tt.Logf(\"Result Success: %q\", resp.Result().(*AuthSuccess))\n\n\tlogResponse(t, resp)\n}\n\nfunc TestPostJSONRPCStructSuccess(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tuser := &credentials{Username: \"testuser\", Password: \"testpass\"}\n\tassertEqual(t, \"Username: **********, Password: **********\", user.String())\n\n\tc := dcnl().SetJSONEscapeHTML(false)\n\tr := c.R().\n\t\tSetHeader(hdrContentTypeKey, \"application/json-rpc\").\n\t\tSetBody(user).\n\t\tSetResult(&AuthSuccess{}).\n\t\tSetQueryParam(\"ct\", \"rpc\")\n\n\trr := r.WithContext(context.Background())\n\tresp, err := rr.Post(ts.URL + \"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, int64(50), resp.Size())\n\n\tt.Logf(\"Result Success: %q\", resp.Result().(*AuthSuccess))\n\n\tlogResponse(t, resp)\n}\n\nfunc TestPostJSONStructInvalidLogin(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetDebug(false)\n\n\tresp, err := c.R().\n\t\tSetHeader(hdrContentTypeKey, \"application/json; charset=utf-8\").\n\t\tSetBody(credentials{Username: \"testuser\", Password: \"testpass1\"}).\n\t\tSetResultError(AuthError{}).\n\t\tSetJSONEscapeHTML(false).\n\t\tPost(ts.URL + \"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusUnauthorized, resp.StatusCode())\n\n\tauthError := resp.ResultError().(*AuthError)\n\tassertEqual(t, \"unauthorized\", authError.ID)\n\tassertEqual(t, \"Invalid credentials\", authError.Message)\n\tt.Logf(\"Result Error: %q\", resp.ResultError().(*AuthError))\n\n\tlogResponse(t, resp)\n}\n\nfunc TestPostJSONErrorRFC7807(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tresp, err := c.R().\n\t\tSetHeader(hdrContentTypeKey, \"application/json; charset=utf-8\").\n\t\tSetBody(credentials{Username: \"testuser\", Password: \"testpass1\"}).\n\t\tSetResultError(AuthError{}).\n\t\tPost(ts.URL + \"/login?ct=problem\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusUnauthorized, resp.StatusCode())\n\n\tauthError := resp.ResultError().(*AuthError)\n\tassertEqual(t, \"unauthorized\", authError.ID)\n\tassertEqual(t, \"Invalid credentials\", authError.Message)\n\tt.Logf(\"Result Error: %q\", resp.ResultError().(*AuthError))\n\n\tlogResponse(t, resp)\n}\n\nfunc TestPostJSONMapSuccess(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetDebug(false)\n\n\tresp, err := c.R().\n\t\tSetBody(map[string]any{\"username\": \"testuser\", \"password\": \"testpass\"}).\n\t\tSetResult(AuthSuccess{}).\n\t\tPost(ts.URL + \"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\tt.Logf(\"Result Success: %q\", resp.Result().(*AuthSuccess))\n\n\tlogResponse(t, resp)\n}\n\nfunc TestPostJSONMapInvalidResponseJson(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tresp, err := dcnldr().\n\t\tSetBody(map[string]any{\"username\": \"testuser\", \"password\": \"invalidjson\"}).\n\t\tSetResult(&AuthSuccess{}).\n\t\tPost(ts.URL + \"/login\")\n\n\tassertEqual(t, \"invalid character '}' looking for beginning of object key string\", err.Error())\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\tauthSuccess := resp.Result().(*AuthSuccess)\n\tassertEqual(t, \"\", authSuccess.ID)\n\tassertEqual(t, \"\", authSuccess.Message)\n\n\tt.Logf(\"Result Success: %q\", resp.Result().(*AuthSuccess))\n\n\tlogResponse(t, resp)\n}\n\ntype brokenMarshalJSON struct{}\n\nfunc (b brokenMarshalJSON) MarshalJSON() ([]byte, error) {\n\treturn nil, errors.New(\"b0rk3d\")\n}\n\nfunc TestPostJSONMarshalError(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tb := brokenMarshalJSON{}\n\texp := \"b0rk3d\"\n\n\t_, err := dcnldr().\n\t\tSetHeader(hdrContentTypeKey, \"application/json\").\n\t\tSetBody(b).\n\t\tPost(ts.URL + \"/login\")\n\tif err == nil {\n\t\tt.Fatalf(\"expected error but got %v\", err)\n\t}\n\n\tif !strings.Contains(err.Error(), exp) {\n\t\tt.Errorf(\"expected error string %q to contain %q\", err, exp)\n\t}\n}\n\nfunc TestForceContentTypeForGH276andGH240(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tretried := 0\n\tc := dcnl()\n\tc.SetDebug(false)\n\n\tresp, err := c.R().\n\t\tSetBody(map[string]any{\"username\": \"testuser\", \"password\": \"testpass\"}).\n\t\tSetResult(AuthSuccess{}).\n\t\tSetResponseForceContentType(\"application/json\").\n\t\tPost(ts.URL + \"/login-json-html\")\n\n\tassertNil(t, err) // JSON response comes with incorrect content-type, we correct it with ForceContentType\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, 0, retried)\n\tassertEqual(t, int64(50), resp.Size())\n\n\tt.Logf(\"Result Success: %q\", resp.Result().(*AuthSuccess))\n\n\tlogResponse(t, resp)\n}\n\nfunc TestPostXMLStringSuccess(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetDebug(false)\n\n\tresp, err := c.R().\n\t\tSetHeader(hdrContentTypeKey, \"application/xml\").\n\t\tSetBody(`<?xml version=\"1.0\" encoding=\"UTF-8\"?><User><Username>testuser</Username><Password>testpass</Password></User>`).\n\t\tSetQueryParam(\"request_no\", strconv.FormatInt(time.Now().Unix(), 10)).\n\t\tPost(ts.URL + \"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, int64(116), resp.Size())\n\n\tlogResponse(t, resp)\n}\n\ntype brokenMarshalXML struct{}\n\nfunc (b brokenMarshalXML) MarshalXML(e *xml.Encoder, start xml.StartElement) error {\n\treturn errors.New(\"b0rk3d\")\n}\n\nfunc TestPostXMLMarshalError(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tb := brokenMarshalXML{}\n\texp := \"b0rk3d\"\n\n\t_, err := dcnldr().\n\t\tSetHeader(hdrContentTypeKey, \"application/xml\").\n\t\tSetBody(b).\n\t\tPost(ts.URL + \"/login\")\n\tif err == nil {\n\t\tt.Fatalf(\"expected error but got %v\", err)\n\t}\n\n\tif !strings.Contains(err.Error(), exp) {\n\t\tt.Errorf(\"expected error string %q to contain %q\", err, exp)\n\t}\n}\n\nfunc TestPostXMLStringError(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tresp, err := dcnldr().\n\t\tSetHeader(hdrContentTypeKey, \"application/xml\").\n\t\tSetBody(`<?xml version=\"1.0\" encoding=\"UTF-8\"?><User><Username>testuser</Username>testpass</Password></User>`).\n\t\tPost(ts.URL + \"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusBadRequest, resp.StatusCode())\n\tassertEqual(t, `<?xml version=\"1.0\" encoding=\"UTF-8\"?><AuthError><Id>bad_request</Id><Message>Unable to read user info</Message></AuthError>`, resp.String())\n\n\tlogResponse(t, resp)\n}\n\nfunc TestPostXMLBytesSuccess(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetDebug(false)\n\n\tresp, err := c.R().\n\t\tSetHeader(hdrContentTypeKey, \"application/xml\").\n\t\tSetBody([]byte(`<?xml version=\"1.0\" encoding=\"UTF-8\"?><User><Username>testuser</Username><Password>testpass</Password></User>`)).\n\t\tSetQueryParam(\"request_no\", strconv.FormatInt(time.Now().Unix(), 10)).\n\t\tPost(ts.URL + \"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\tlogResponse(t, resp)\n}\n\nfunc TestPostXMLStructSuccess(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tresp, err := dcnldr().\n\t\tSetHeader(hdrContentTypeKey, \"application/xml\").\n\t\tSetBody(credentials{Username: \"testuser\", Password: \"testpass\"}).\n\t\tSetResult(&AuthSuccess{}).\n\t\tPost(ts.URL + \"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\tt.Logf(\"Result Success: %q\", resp.Result().(*AuthSuccess))\n\n\tlogResponse(t, resp)\n}\n\nfunc TestPostXMLStructInvalidLogin(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetResultError(&AuthError{})\n\n\tresp, err := c.R().\n\t\tSetHeader(hdrContentTypeKey, \"application/xml\").\n\t\tSetBody(credentials{Username: \"testuser\", Password: \"testpass1\"}).\n\t\tPost(ts.URL + \"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusUnauthorized, resp.StatusCode())\n\tassertEqual(t, resp.Header().Get(\"Www-Authenticate\"), \"Protected Realm\")\n\n\tt.Logf(\"Result Error: %q\", resp.ResultError().(*AuthError))\n\n\tlogResponse(t, resp)\n}\n\nfunc TestPostXMLStructInvalidResponseXml(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tresp, err := dcnldr().\n\t\tSetHeader(hdrContentTypeKey, \"application/xml\").\n\t\tSetBody(credentials{Username: \"testuser\", Password: \"invalidxml\"}).\n\t\tSetResult(&AuthSuccess{}).\n\t\tPost(ts.URL + \"/login\")\n\n\tassertEqual(t, \"XML syntax error on line 1: element <Message> closed by </AuthSuccess>\", err.Error())\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\tt.Logf(\"Result Success: %q\", resp.Result().(*AuthSuccess))\n\n\tlogResponse(t, resp)\n}\n\nfunc TestPostXMLMapNotSupported(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\t_, err := dcnldr().\n\t\tSetHeader(hdrContentTypeKey, \"application/xml\").\n\t\tSetBody(map[string]any{\"Username\": \"testuser\", \"Password\": \"testpass\"}).\n\t\tPost(ts.URL + \"/login\")\n\n\tassertErrorIs(t, ErrUnsupportedRequestBodyKind, err)\n}\n\nfunc TestRequestBasicAuth(t *testing.T) {\n\tts := createAuthServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetBaseURL(ts.URL).\n\t\tSetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})\n\n\tresp, err := c.R().\n\t\tSetBasicAuth(\"myuser\", \"basicauth\").\n\t\tSetResult(&AuthSuccess{}).\n\t\tPost(\"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\tt.Logf(\"Result Success: %q\", resp.Result().(*AuthSuccess))\n\tlogResponse(t, resp)\n}\n\nfunc TestRequestBasicAuthWithBody(t *testing.T) {\n\tts := createAuthServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetBaseURL(ts.URL).\n\t\tSetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})\n\n\tresp, err := c.R().\n\t\tSetBasicAuth(\"myuser\", \"basicauth\").\n\t\tSetBody([]string{strings.Repeat(\"hello\", 25)}).\n\t\tSetResult(&AuthSuccess{}).\n\t\tPost(\"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\tt.Logf(\"Result Success: %q\", resp.Result().(*AuthSuccess))\n\tlogResponse(t, resp)\n}\n\nfunc TestRequestInsecureBasicAuth(t *testing.T) {\n\tts := createAuthServerTLSOptional(t, false)\n\tdefer ts.Close()\n\n\tvar logBuf bytes.Buffer\n\tlogger := createLogger()\n\tlogger.l.SetOutput(&logBuf)\n\n\tc := dcnl()\n\tc.SetBaseURL(ts.URL)\n\n\tresp, err := c.R().\n\t\tSetBasicAuth(\"myuser\", \"basicauth\").\n\t\tSetResult(&AuthSuccess{}).\n\t\tSetLogger(logger).\n\t\tPost(\"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertTrue(t, strings.Contains(logBuf.String(),\n\t\t\"WARN RESTY Using sensitive credentials in HTTP mode is not secure. Use HTTPS\"))\n\n\tt.Logf(\"Result Success: %q\", resp.Result().(*AuthSuccess))\n\tlogResponse(t, resp)\n\tt.Logf(\"captured request-level logs: %s\", logBuf.String())\n}\n\nfunc TestRequestBasicAuthFail(t *testing.T) {\n\tts := createAuthServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).\n\t\tSetResultError(AuthError{})\n\n\tresp, err := c.R().\n\t\tSetBasicAuth(\"myuser\", \"basicauth1\").\n\t\tPost(ts.URL + \"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusUnauthorized, resp.StatusCode())\n\n\tt.Logf(\"Result Error: %q\", resp.ResultError().(*AuthError))\n\tlogResponse(t, resp)\n}\n\nfunc TestRequestAuthToken(t *testing.T) {\n\tts := createAuthServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).\n\t\tSetAuthToken(\"004DDB79-6801-4587-B976-F093E6AC44FF\")\n\n\tresp, err := c.R().\n\t\tSetAuthToken(\"004DDB79-6801-4587-B976-F093E6AC44FF-Request\").\n\t\tGet(ts.URL + \"/profile\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n}\n\nfunc TestRequestAuthScheme(t *testing.T) {\n\tts := createAuthServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).\n\t\tSetAuthScheme(\"OAuth\").\n\t\tSetAuthToken(\"004DDB79-6801-4587-B976-F093E6AC44FF\")\n\n\tt.Run(\"override auth scheme\", func(t *testing.T) {\n\t\tresp, err := c.R().\n\t\t\tSetAuthScheme(\"Bearer\").\n\t\t\tSetAuthToken(\"004DDB79-6801-4587-B976-F093E6AC44FF-Request\").\n\t\t\tGet(ts.URL + \"/profile\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\t})\n\n\tt.Run(\"empty auth scheme at client level GH954\", func(t *testing.T) {\n\t\ttokenValue := \"004DDB79-6801-4587-B976-F093E6AC44FF\"\n\n\t\t// set client level\n\t\tc.SetAuthScheme(\"\").\n\t\t\tSetAuthToken(tokenValue)\n\n\t\tresp, err := c.R().\n\t\t\tGet(ts.URL + \"/profile\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\t\tassertEqual(t, \"\", resp.Request.Header.Get(hdrAuthorizationKey))\n\t\tassertEqual(t, tokenValue, resp.Request.RawRequest.Header.Get(hdrAuthorizationKey))\n\t})\n\n\tt.Run(\"empty auth scheme at request level GH954\", func(t *testing.T) {\n\t\ttokenValue := \"004DDB79-6801-4587-B976-F093E6AC44FF\"\n\n\t\t// set client level\n\t\tc := dcnl().\n\t\t\tSetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).\n\t\t\tSetAuthToken(tokenValue)\n\n\t\tresp, err := c.R().\n\t\t\tSetAuthScheme(\"\").\n\t\t\tGet(ts.URL + \"/profile\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, \"\", resp.Request.Header.Get(hdrAuthorizationKey))\n\t\tassertEqual(t, tokenValue, resp.Request.RawRequest.Header.Get(hdrAuthorizationKey))\n\t})\n\n\tt.Run(\"only client level auth token GH959\", func(t *testing.T) {\n\t\ttokenValue := \"004DDB79-6801-4587-B976-F093E6AC44FF\"\n\n\t\tc := dcnl().\n\t\t\tSetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).\n\t\t\tSetAuthToken(tokenValue)\n\n\t\tresp, err := c.R().\n\t\t\tGet(ts.URL + \"/profile\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\t\tassertEqual(t, \"\", resp.Request.Header.Get(hdrAuthorizationKey))\n\t\tassertEqual(t, \"Bearer \"+tokenValue, resp.Request.RawRequest.Header.Get(hdrAuthorizationKey))\n\t})\n}\n\nfunc TestFormData(t *testing.T) {\n\tts := createFormPostServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetFormData(map[string]string{\"zip_code\": \"00000\", \"city\": \"Los Angeles\"}).\n\t\tSetDebug(true)\n\tc.outputLogTo(io.Discard)\n\n\tresp, err := c.R().\n\t\tSetFormData(map[string]string{\"first_name\": \"Jeevanandam\", \"last_name\": \"M\", \"zip_code\": \"00001\"}).\n\t\tSetBasicAuth(\"myuser\", \"mypass\").\n\t\tPost(ts.URL + \"/profile\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"Success\", resp.String())\n}\n\nfunc TestMultiValueFormData(t *testing.T) {\n\tts := createFormPostServer(t)\n\tdefer ts.Close()\n\n\tv := url.Values{\n\t\t\"search_criteria\": []string{\"book\", \"glass\", \"pencil\"},\n\t}\n\n\tc := dcnl()\n\tc.SetDebug(true)\n\tc.outputLogTo(io.Discard)\n\n\tresp, err := c.R().\n\t\tSetQueryParamsFromValues(v).\n\t\tPost(ts.URL + \"/search\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"Success\", resp.String())\n}\n\nfunc TestFormDataDisableWarn(t *testing.T) {\n\tts := createFormPostServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetFormData(map[string]string{\"zip_code\": \"00000\", \"city\": \"Los Angeles\"}).\n\t\tSetLoggerWarnLevel(true)\n\tc.outputLogTo(io.Discard)\n\n\tresp, err := c.R().\n\t\tSetDebug(true).\n\t\tSetFormData(map[string]string{\"first_name\": \"Jeevanandam\", \"last_name\": \"M\", \"zip_code\": \"00001\"}).\n\t\tSetBasicAuth(\"myuser\", \"mypass\").\n\t\tPost(ts.URL + \"/profile\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"Success\", resp.String())\n}\n\nfunc TestGetWithCookie(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetBaseURL(ts.URL)\n\tc.SetCookie(&http.Cookie{\n\t\tName:  \"go-resty-1\",\n\t\tValue: \"This is cookie 1 value\",\n\t})\n\n\tr := c.R().\n\t\tSetCookie(&http.Cookie{\n\t\t\tName:  \"go-resty-2\",\n\t\t\tValue: \"This is cookie 2 value\",\n\t\t}).\n\t\tSetCookies([]*http.Cookie{\n\t\t\t{\n\t\t\t\tName:  \"go-resty-1\",\n\t\t\t\tValue: \"This is cookie 1 value additional append\",\n\t\t\t},\n\t\t})\n\tresp, err := r.Get(\"mypage2\")\n\n\t_ = r.Clone(context.Background())\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"TestGet: text response from mypage2\", resp.String())\n\n\tlogResponse(t, resp)\n}\n\nfunc TestGetWithCookies(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetBaseURL(ts.URL).SetDebug(true)\n\n\ttu, _ := url.Parse(ts.URL)\n\tc.Client().Jar.SetCookies(tu, []*http.Cookie{\n\t\t{\n\t\t\tName:  \"jar-go-resty-1\",\n\t\t\tValue: \"From Jar - This is cookie 1 value\",\n\t\t},\n\t\t{\n\t\t\tName:  \"jar-go-resty-2\",\n\t\t\tValue: \"From Jar - This is cookie 2 value\",\n\t\t},\n\t})\n\n\tresp, err := c.R().SetHeader(hdrCookieKey, \"\").Get(\"mypage2\")\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\t// Client cookies\n\tc.SetCookies([]*http.Cookie{\n\t\t{\n\t\t\tName:  \"go-resty-1\",\n\t\t\tValue: \"This is cookie 1 value\",\n\t\t},\n\t\t{\n\t\t\tName:  \"go-resty-2\",\n\t\t\tValue: \"This is cookie 2 value\",\n\t\t},\n\t})\n\n\tr := c.R().\n\t\tSetCookie(&http.Cookie{\n\t\t\tName:  \"req-go-resty-1\",\n\t\t\tValue: \"This is request cookie 1 value additional append\",\n\t\t})\n\tresp, err = r.Get(\"mypage2\")\n\n\t_ = r.Clone(context.Background())\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"TestGet: text response from mypage2\", resp.String())\n\n\tlogResponse(t, resp)\n}\n\nfunc TestPutPlainString(t *testing.T) {\n\tts := createGenericServer(t)\n\tdefer ts.Close()\n\n\tresp, err := dcnl().R().\n\t\tSetBody(\"This is plain text body to server\").\n\t\tPut(ts.URL + \"/plaintext\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"TestPut: plain text response\", resp.String())\n}\n\nfunc TestPutJSONString(t *testing.T) {\n\tts := createGenericServer(t)\n\tdefer ts.Close()\n\n\tclient := dcnl()\n\n\tclient.AddRequestMiddleware(func(c *Client, r *Request) error {\n\t\tr.SetHeader(\"X-Custom-Request-Middleware\", \"Request middleware\")\n\t\treturn nil\n\t})\n\tclient.AddRequestMiddleware(func(c *Client, r *Request) error {\n\t\tr.SetHeader(\"X-ContentLength\", \"Request middleware ContentLength set\")\n\t\treturn nil\n\t})\n\n\tclient.SetDebug(true)\n\tclient.outputLogTo(io.Discard)\n\n\tresp, err := client.R().\n\t\tSetHeaders(map[string]string{hdrContentTypeKey: \"application/json; charset=utf-8\", hdrAcceptKey: \"application/json; charset=utf-8\"}).\n\t\tSetBody(`{\"content\":\"json content sending to server\"}`).\n\t\tPut(ts.URL + \"/json\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, `{\"response\":\"json response\"}`, resp.String())\n}\n\nfunc TestPutXMLString(t *testing.T) {\n\tts := createGenericServer(t)\n\tdefer ts.Close()\n\n\tresp, err := dcnl().R().\n\t\tSetHeaders(map[string]string{hdrContentTypeKey: \"application/xml\", hdrAcceptKey: \"application/xml\"}).\n\t\tSetBody(`<?xml version=\"1.0\" encoding=\"UTF-8\"?><Request>XML Content sending to server</Request>`).\n\t\tPut(ts.URL + \"/xml\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, `<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response>XML response</Response>`, resp.String())\n}\n\nfunc TestRequestMiddleware(t *testing.T) {\n\tts := createGenericServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\n\tc.AddRequestMiddleware(func(c *Client, r *Request) error {\n\t\tr.SetHeader(\"X-Custom-Request-Middleware\", \"Request middleware\")\n\t\treturn nil\n\t})\n\tc.AddRequestMiddleware(func(c *Client, r *Request) error {\n\t\tr.SetHeader(\"X-ContentLength\", \"Request middleware ContentLength set\")\n\t\treturn nil\n\t})\n\n\tresp, err := c.R().\n\t\tSetBody(\"RequestMiddleware: This is plain text body to server\").\n\t\tPut(ts.URL + \"/plaintext\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"TestPut: plain text response\", resp.String())\n}\n\nfunc TestHTTPAutoRedirectUpTo10(t *testing.T) {\n\tts := createRedirectServer(t)\n\tdefer ts.Close()\n\n\tres, err := dcnl().R().Get(ts.URL + \"/redirect-1\")\n\tredirects := res.RedirectHistory()\n\tassertEqual(t, 10, len(redirects))\n\n\tfinalReq := redirects[0]\n\tassertEqual(t, 307, finalReq.StatusCode)\n\tassertEqual(t, ts.URL+\"/redirect-10\", finalReq.URL)\n\n\tassertTrue(t, (err.Error() == \"Get /redirect-11: stopped after 10 redirects\" ||\n\t\terr.Error() == \"Get \\\"/redirect-11\\\": stopped after 10 redirects\"))\n}\n\nfunc TestHostCheckRedirectPolicy(t *testing.T) {\n\tts := createRedirectServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl().\n\t\tSetRedirectPolicy(RedirectDomainCheckPolicy(\"127.0.0.1\"))\n\n\t_, err := c.R().Get(ts.URL + \"/redirect-host-check-1\")\n\n\tassertNotNil(t, err)\n\tassertTrue(t, strings.Contains(err.Error(), \"redirect is not allowed as per DomainCheckRedirectPolicy\"))\n}\n\nfunc TestHttpMethods(t *testing.T) {\n\tts := createGenericServer(t)\n\tdefer ts.Close()\n\n\tt.Run(\"head method\", func(t *testing.T) {\n\t\tresp, err := dcnldr().Head(ts.URL + \"/\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\t})\n\n\tt.Run(\"options method\", func(t *testing.T) {\n\t\tresp, err := dcnldr().Options(ts.URL + \"/options\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\t\tassertEqual(t, resp.Header().Get(\"Access-Control-Expose-Headers\"), \"x-go-resty-id\")\n\t})\n\n\tt.Run(\"patch method\", func(t *testing.T) {\n\t\tresp, err := dcnldr().Patch(ts.URL + \"/patch\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\t\tassertEqual(t, \"\", resp.String())\n\t})\n\n\tt.Run(\"trace method\", func(t *testing.T) {\n\t\tresp, err := dcnldr().Trace(ts.URL + \"/trace\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\t\tassertEqual(t, \"\", resp.String())\n\t})\n}\n\nfunc TestSendMethod(t *testing.T) {\n\tts := createGenericServer(t)\n\tdefer ts.Close()\n\n\tt.Run(\"send-get-implicit\", func(t *testing.T) {\n\t\treq := dcnldr()\n\t\treq.URL = ts.URL + \"/gzip-test\"\n\n\t\tresp, err := req.Send()\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\t\tassertEqual(t, \"This is Gzip response testing\", resp.String())\n\t})\n\n\tt.Run(\"send-get\", func(t *testing.T) {\n\t\treq := dcnldr()\n\t\treq.SetMethod(MethodGet)\n\t\treq.URL = ts.URL + \"/gzip-test\"\n\n\t\tresp, err := req.Send()\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\t\tassertEqual(t, \"This is Gzip response testing\", resp.String())\n\t})\n\n\tt.Run(\"send-options\", func(t *testing.T) {\n\t\treq := dcnldr()\n\t\treq.SetMethod(MethodOptions)\n\t\treq.URL = ts.URL + \"/options\"\n\n\t\tresp, err := req.Send()\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\t\tassertEqual(t, \"\", resp.String())\n\t\tassertEqual(t, \"x-go-resty-id\", resp.Header().Get(\"Access-Control-Expose-Headers\"))\n\t})\n\n\tt.Run(\"send-patch\", func(t *testing.T) {\n\t\treq := dcnldr()\n\t\treq.SetMethod(MethodPatch)\n\t\treq.URL = ts.URL + \"/patch\"\n\n\t\tresp, err := req.Send()\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\t\tassertEqual(t, \"\", resp.String())\n\t})\n\n\tt.Run(\"send-put\", func(t *testing.T) {\n\t\treq := dcnldr()\n\t\treq.SetMethod(MethodPut)\n\t\treq.URL = ts.URL + \"/plaintext\"\n\n\t\tresp, err := req.Send()\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\t\tassertEqual(t, \"TestPut: plain text response\", resp.String())\n\t})\n}\n\nfunc TestRawFileUploadByBody(t *testing.T) {\n\tts := createFormPostServer(t)\n\tdefer ts.Close()\n\n\tfileBytes, err := os.ReadFile(filepath.Join(getTestDataPath(), \"test-img.png\"))\n\tassertNil(t, err)\n\n\tresp, err := dcnldr().\n\t\tSetBody(fileBytes).\n\t\tSetAuthToken(\"004DDB79-6801-4587-B976-F093E6AC44FF\").\n\t\tPut(ts.URL + \"/raw-upload\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"image/png\", resp.Request.Header.Get(hdrContentTypeKey))\n}\n\nfunc TestProxySetting(t *testing.T) {\n\tc := dcnl()\n\n\ttransport, err := c.HTTPTransport()\n\n\tassertNil(t, err)\n\n\tassertFalse(t, c.IsProxySet())\n\tassertNotNil(t, transport.Proxy)\n\n\tc.SetProxy(\"http://sampleproxy:8888\")\n\tassertTrue(t, c.IsProxySet())\n\tassertNotNil(t, transport.Proxy)\n\n\tc.SetProxy(\"//not.a.user@%66%6f%6f.com:8888\")\n\tassertTrue(t, c.IsProxySet())\n\tassertNotNil(t, transport.Proxy)\n\n\tc.SetProxy(\"http://sampleproxy:8888\")\n\tassertTrue(t, c.IsProxySet())\n\tc.RemoveProxy()\n\tassertNil(t, c.ProxyURL())\n\tassertNil(t, transport.Proxy)\n}\n\nfunc TestGetClient(t *testing.T) {\n\tclient := New()\n\tcustom := New()\n\tcustomClient := custom.Client()\n\n\tassertNotNil(t, customClient)\n\tassertNotEqual(t, client, http.DefaultClient)\n\tassertNotEqual(t, customClient, http.DefaultClient)\n\tassertNotEqual(t, client, customClient)\n}\n\nfunc TestIncorrectURL(t *testing.T) {\n\tc := dcnl()\n\t_, err := c.R().Get(\"//not.a.user@%66%6f%6f.com/just/a/path/also\")\n\tassertTrue(t, (strings.Contains(err.Error(), \"parse //not.a.user@%66%6f%6f.com/just/a/path/also\") ||\n\t\tstrings.Contains(err.Error(), \"parse \\\"//not.a.user@%66%6f%6f.com/just/a/path/also\\\"\")))\n\n\tc.SetBaseURL(\"//not.a.user@%66%6f%6f.com\")\n\t_, err1 := c.R().Get(\"/just/a/path/also\")\n\tassertTrue(t, (strings.Contains(err1.Error(), \"parse //not.a.user@%66%6f%6f.com/just/a/path/also\") ||\n\t\tstrings.Contains(err1.Error(), \"parse \\\"//not.a.user@%66%6f%6f.com/just/a/path/also\\\"\")))\n}\n\nfunc TestDetectContentTypeForPointer(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tuser := &credentials{Username: \"testuser\", Password: \"testpass\"}\n\n\tresp, err := dcnldr().\n\t\tSetBody(user).\n\t\tSetResult(AuthSuccess{}).\n\t\tPost(ts.URL + \"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\n\tt.Logf(\"Result Success: %q\", resp.Result().(*AuthSuccess))\n\n\tlogResponse(t, resp)\n}\n\ntype ExampleUser struct {\n\tFirstName string `json:\"first_name\"`\n\tLastName  string `json:\"last_name\"`\n\tZipCode   string `json:\"zip_code\"`\n}\n\nfunc TestDetectContentTypeForPointerWithSlice(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tusers := &[]ExampleUser{\n\t\t{FirstName: \"firstname1\", LastName: \"lastname1\", ZipCode: \"10001\"},\n\t\t{FirstName: \"firstname2\", LastName: \"lastname3\", ZipCode: \"10002\"},\n\t\t{FirstName: \"firstname3\", LastName: \"lastname3\", ZipCode: \"10003\"},\n\t}\n\n\tresp, err := dcnldr().\n\t\tSetBody(users).\n\t\tPost(ts.URL + \"/users\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusAccepted, resp.StatusCode())\n\n\tt.Logf(\"Result Success: %q\", resp)\n\n\tlogResponse(t, resp)\n}\n\nfunc TestDetectContentTypeForPointerWithSliceMap(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tusersmap := map[string]any{\n\t\t\"user1\": ExampleUser{FirstName: \"firstname1\", LastName: \"lastname1\", ZipCode: \"10001\"},\n\t\t\"user2\": &ExampleUser{FirstName: \"firstname2\", LastName: \"lastname3\", ZipCode: \"10002\"},\n\t\t\"user3\": ExampleUser{FirstName: \"firstname3\", LastName: \"lastname3\", ZipCode: \"10003\"},\n\t}\n\n\tvar users []map[string]any\n\tusers = append(users, usersmap)\n\n\tresp, err := dcnldr().\n\t\tSetBody(&users).\n\t\tPost(ts.URL + \"/usersmap\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusAccepted, resp.StatusCode())\n\n\tt.Logf(\"Result Success: %q\", resp)\n\n\tlogResponse(t, resp)\n}\n\nfunc TestDetectContentTypeForSlice(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tusers := []ExampleUser{\n\t\t{FirstName: \"firstname1\", LastName: \"lastname1\", ZipCode: \"10001\"},\n\t\t{FirstName: \"firstname2\", LastName: \"lastname3\", ZipCode: \"10002\"},\n\t\t{FirstName: \"firstname3\", LastName: \"lastname3\", ZipCode: \"10003\"},\n\t}\n\n\tresp, err := dcnldr().\n\t\tSetBody(users).\n\t\tPost(ts.URL + \"/users\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusAccepted, resp.StatusCode())\n\n\tt.Logf(\"Result Success: %q\", resp)\n\n\tlogResponse(t, resp)\n}\n\nfunc TestMultiParamsQueryString(t *testing.T) {\n\tts1 := createGetServer(t)\n\tdefer ts1.Close()\n\n\tclient := dcnl()\n\treq1 := client.R()\n\n\tclient.SetQueryParam(\"status\", \"open\")\n\n\t_, _ = req1.SetQueryParam(\"status\", \"pending\").\n\t\tGet(ts1.URL)\n\n\tassertTrue(t, strings.Contains(req1.URL, \"status=pending\"))\n\t// pending overrides open\n\tassertFalse(t, strings.Contains(req1.URL, \"status=open\"))\n\n\t_, _ = req1.SetQueryParam(\"status\", \"approved\").\n\t\tGet(ts1.URL)\n\n\tassertTrue(t, strings.Contains(req1.URL, \"status=approved\"))\n\t// approved overrides pending\n\tassertFalse(t, strings.Contains(req1.URL, \"status=pending\"))\n\n\tts2 := createGetServer(t)\n\tdefer ts2.Close()\n\n\treq2 := client.R()\n\n\tv := url.Values{\n\t\t\"status\": []string{\"pending\", \"approved\", \"reject\"},\n\t}\n\n\t_, _ = req2.SetQueryParamsFromValues(v).Get(ts2.URL)\n\n\tassertTrue(t, strings.Contains(req2.URL, \"status=pending\"))\n\tassertTrue(t, strings.Contains(req2.URL, \"status=approved\"))\n\tassertTrue(t, strings.Contains(req2.URL, \"status=reject\"))\n\n\t// because it's removed by key\n\tassertFalse(t, strings.Contains(req2.URL, \"status=open\"))\n}\n\nfunc TestSetQueryStringTypical(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tresp, err := dcnldr().\n\t\tSetQueryString(\"productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more\").\n\t\tGet(ts.URL)\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"200 OK\", resp.Status())\n\tassertEqual(t, \"TestGet: text response\", resp.String())\n\n\tresp, err = dcnldr().\n\t\tSetQueryString(\"&%%amp;\").\n\t\tGet(ts.URL)\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"200 OK\", resp.Status())\n\tassertEqual(t, \"TestGet: text response\", resp.String())\n}\n\nfunc TestSetHeaderVerbatim(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tr := dcnldr().\n\t\tSetHeaderVerbatim(\"header-lowercase\", \"value_lowercase\").\n\t\tSetHeader(\"header-lowercase\", \"value_standard\")\n\n\t//lint:ignore SA1008 valid one ignore this!\n\tassertEqual(t, \"value_lowercase\", strings.Join(r.Header[\"header-lowercase\"], \"\"))\n\tassertEqual(t, \"value_standard\", r.Header.Get(\"Header-Lowercase\"))\n}\n\nfunc TestSetHeaderMultipleValue(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tr := dcnldr().\n\t\tSetHeaderMultiValues(map[string][]string{\n\t\t\t\"Content\":       {\"text/*\", \"text/html\", \"*\"},\n\t\t\t\"Authorization\": {\"Bearer xyz\"},\n\t\t})\n\tassertEqual(t, \"text/*, text/html, *\", r.Header.Get(\"content\"))\n\tassertEqual(t, \"Bearer xyz\", r.Header.Get(\"authorization\"))\n}\n\nfunc TestRequestSetHeaderAny(t *testing.T) {\n\tr := dcnldr().\n\t\tSetHeaderAny(\"X-Int-Value\", 42).\n\t\tSetHeaderAny(\"X-String-Value\", \"hello\")\n\n\tassertEqual(t, \"42\", r.Header.Get(\"X-Int-Value\"))\n\tassertEqual(t, \"hello\", r.Header.Get(\"X-String-Value\"))\n}\n\nfunc TestRequestSetHeaderVerbatimAny(t *testing.T) {\n\tr := dcnldr().\n\t\tSetHeaderVerbatimAny(\"header-lowercase\", 123)\n\n\t//lint:ignore SA1008 valid one ignore this!\n\tassertEqual(t, \"123\", strings.Join(r.Header[\"header-lowercase\"], \"\"))\n}\n\nfunc TestRequestSetQueryParamAny(t *testing.T) {\n\tr := dcnldr().\n\t\tSetQueryParamAny(\"page\", 5).\n\t\tSetQueryParamAny(\"active\", true)\n\n\tassertEqual(t, \"5\", r.QueryParams.Get(\"page\"))\n\tassertEqual(t, \"true\", r.QueryParams.Get(\"active\"))\n}\n\nfunc TestRequestSetPathParamAny(t *testing.T) {\n\tr := dcnldr().\n\t\tSetPathParamAny(\"userId\", 42).\n\t\tSetPathParamAny(\"name\", \"john doe\")\n\n\tassertEqual(t, \"42\", r.PathParams[\"userId\"])\n\tassertEqual(t, \"john%20doe\", r.PathParams[\"name\"])\n}\n\nfunc TestRequestSetRawPathParamAny(t *testing.T) {\n\tr := dcnldr().\n\t\tSetPathRawParamAny(\"userId\", 42).\n\t\tSetPathRawParamAny(\"name\", \"john doe\")\n\n\tassertEqual(t, \"42\", r.PathParams[\"userId\"])\n\tassertEqual(t, \"john doe\", r.PathParams[\"name\"])\n}\n\nfunc TestOutputFileWithBaseDirAndRelativePath(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\tdefer cleanupFiles(\".testdata/dir-sample\")\n\n\tbaseOutputDir := filepath.Join(getTestDataPath(), \"dir-sample\")\n\tclient := dcnl().\n\t\tSetRedirectPolicy(RedirectFlexiblePolicy(10)).\n\t\tSetResponseSaveDirectory(baseOutputDir).\n\t\tSetDebug(true)\n\n\toutputFilePath := \"go-resty/test-img-success.png\"\n\tresp, err := client.R().\n\t\tSetResponseSaveFileName(outputFilePath).\n\t\tGet(ts.URL + \"/my-image.png\")\n\n\tassertError(t, err)\n\tassertTrue(t, resp.Size() != 0)\n\tassertTrue(t, resp.Duration() > 0)\n\n\tf, err1 := os.Open(filepath.Join(baseOutputDir, outputFilePath))\n\tdefer closeq(f)\n\tassertError(t, err1)\n}\n\nfunc TestOutputFileWithBaseDirError(t *testing.T) {\n\tc := dcnl().SetRedirectPolicy(RedirectFlexiblePolicy(10)).\n\t\tSetResponseSaveDirectory(filepath.Join(getTestDataPath(), `go-resty\\0`))\n\n\t_ = c\n}\n\nfunc TestOutputPathDirNotExists(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\tdefer cleanupFiles(filepath.Join(\".testdata\", \"not-exists-dir\"))\n\n\tclient := dcnl().\n\t\tSetRedirectPolicy(RedirectFlexiblePolicy(10)).\n\t\tSetResponseSaveDirectory(filepath.Join(getTestDataPath(), \"not-exists-dir\"))\n\n\tresp, err := client.R().\n\t\tSetResponseSaveFileName(\"test-img-success.png\").\n\t\tGet(ts.URL + \"/my-image.png\")\n\n\tassertError(t, err)\n\tassertTrue(t, resp.Size() != 0)\n\tassertTrue(t, resp.Duration() > 0)\n}\n\nfunc TestOutputFileAbsPath(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\tdefer cleanupFiles(filepath.Join(\".testdata\", \"go-resty\"))\n\n\toutputFile := filepath.Join(getTestDataPath(), \"go-resty\", \"test-img-success-2.png\")\n\n\tres, err := dcnlr().\n\t\tSetResponseSaveFileName(outputFile).\n\t\tGet(ts.URL + \"/my-image.png\")\n\n\tassertError(t, err)\n\tassertEqual(t, int64(2579468), res.Size())\n\n\t_, err = os.Stat(outputFile)\n\tassertNil(t, err)\n}\n\nfunc TestRequestSaveResponse(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\tdefer cleanupFiles(filepath.Join(\".testdata\", \"go-resty\"))\n\n\tc := dcnl().\n\t\tSetResponseSaveToFile(true).\n\t\tSetResponseSaveDirectory(filepath.Join(getTestDataPath(), \"go-resty\"))\n\n\tassertTrue(t, c.IsResponseSaveToFile())\n\n\tt.Run(\"content-disposition save response request\", func(t *testing.T) {\n\t\toutputFile := filepath.Join(getTestDataPath(), \"go-resty\", \"test-img-success-2.png\")\n\t\tc.SetResponseSaveToFile(false)\n\t\tassertFalse(t, c.IsResponseSaveToFile())\n\n\t\tres, err := c.R().\n\t\t\tSetResponseSaveToFile(true).\n\t\t\tGet(ts.URL + \"/my-image.png?content-disposition=true&filename=test-img-success-2.png\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, int64(2579468), res.Size())\n\n\t\t_, err = os.Stat(outputFile)\n\t\tassertNil(t, err)\n\t})\n\n\tt.Run(\"use filename from path\", func(t *testing.T) {\n\t\toutputFile := filepath.Join(getTestDataPath(), \"go-resty\", \"my-image.png\")\n\t\tc.SetResponseSaveToFile(false)\n\t\tassertFalse(t, c.IsResponseSaveToFile())\n\n\t\tres, err := c.R().\n\t\t\tSetResponseSaveToFile(true).\n\t\t\tGet(ts.URL + \"/my-image.png\")\n\n\t\tassertError(t, err)\n\t\tassertEqual(t, int64(2579468), res.Size())\n\n\t\t_, err = os.Stat(outputFile)\n\t\tassertNil(t, err)\n\t})\n\n\tt.Run(\"empty path\", func(t *testing.T) {\n\t\t_, err := c.R().\n\t\t\tSetResponseSaveToFile(true).\n\t\t\tGet(ts.URL)\n\t\tassertError(t, err)\n\t})\n\n}\n\nfunc TestContextInternal(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tr := dcnl().R().\n\t\tSetQueryParam(\"request_no\", strconv.FormatInt(time.Now().Unix(), 10))\n\n\tresp, err := r.Get(ts.URL + \"/\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n}\n\nfunc TestRequestDoNotParseResponse(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tt.Run(\"do not parse response 1\", func(t *testing.T) {\n\t\tclient := dcnl().SetResponseDoNotParse(true)\n\t\tresp, err := client.R().\n\t\t\tSetQueryParam(\"request_no\", strconv.FormatInt(time.Now().Unix(), 10)).\n\t\t\tGet(ts.URL + \"/\")\n\n\t\tassertError(t, err)\n\n\t\tb, err := io.ReadAll(resp.Body)\n\t\t_ = resp.Body.Close()\n\t\tassertError(t, err)\n\t\tassertEqual(t, \"TestGet: text response\", string(b))\n\t})\n\n\tt.Run(\"manual reset raw response - do not parse response 2\", func(t *testing.T) {\n\t\tresp, err := dcnl().R().\n\t\t\tSetResponseDoNotParse(true).\n\t\t\tGet(ts.URL + \"/\")\n\n\t\tassertError(t, err)\n\n\t\tresp.RawResponse = nil\n\t\tassertEqual(t, 0, resp.StatusCode())\n\t\tassertEqual(t, \"\", resp.String())\n\t})\n}\n\nfunc TestRequestDoNotParseResponseDebugLog(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tt.Run(\"do not parse response debug log client level\", func(t *testing.T) {\n\t\tc := dcnl().\n\t\t\tSetResponseDoNotParse(true).\n\t\t\tSetDebug(true)\n\n\t\tvar lgr bytes.Buffer\n\t\tc.outputLogTo(&lgr)\n\n\t\t_, err := c.R().\n\t\t\tSetQueryParam(\"request_no\", strconv.FormatInt(time.Now().Unix(), 10)).\n\t\t\tGet(ts.URL + \"/\")\n\n\t\tassertError(t, err)\n\t\tassertTrue(t, strings.Contains(lgr.String(), \"***** DO NOT PARSE RESPONSE - Enabled *****\"))\n\t})\n\n\tt.Run(\"do not parse response debug log request level\", func(t *testing.T) {\n\t\tc := dcnl()\n\n\t\tvar lgr bytes.Buffer\n\t\tc.outputLogTo(&lgr)\n\n\t\t_, err := c.R().\n\t\t\tSetDebug(true).\n\t\t\tSetResponseDoNotParse(true).\n\t\t\tSetQueryParam(\"request_no\", strconv.FormatInt(time.Now().Unix(), 10)).\n\t\t\tGet(ts.URL + \"/\")\n\n\t\tassertError(t, err)\n\t\tassertTrue(t, strings.Contains(lgr.String(), \"***** DO NOT PARSE RESPONSE - Enabled *****\"))\n\t})\n}\n\ntype noCtTest struct {\n\tResponse string `json:\"response\"`\n}\n\nfunc TestRequestExpectContentTypeTest(t *testing.T) {\n\tts := createGenericServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tresp, err := c.R().\n\t\tSetResult(noCtTest{}).\n\t\tSetResponseExpectContentType(\"application/json\").\n\t\tGet(ts.URL + \"/json-no-set\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertNotNil(t, resp.Result())\n\tassertEqual(t, \"json response no content type set\", resp.Result().(*noCtTest).Response)\n\n\tassertEqual(t, \"\", firstNonEmpty(\"\", \"\"))\n}\n\nfunc TestGetPathParamAndPathParams(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl().\n\t\tSetBaseURL(ts.URL).\n\t\tSetPathParam(\"userId\", \"sample@sample.com\")\n\n\tassertEqual(t, \"sample@sample.com\", c.PathParams()[\"userId\"])\n\n\tresp, err := c.R().SetPathParam(\"subAccountId\", \"100002\").\n\t\tGet(\"/v1/users/{userId}/{subAccountId}/details\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertTrue(t, strings.Contains(resp.String(), \"TestGetPathParams: text response\"))\n\tassertTrue(t, strings.Contains(resp.String(), \"/v1/users/sample@sample.com/100002/details\"))\n\n\tlogResponse(t, resp)\n}\n\nfunc TestReportMethodSupportsPayload(t *testing.T) {\n\tts := createGenericServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tresp, err := c.R().\n\t\tSetBody(\"body\").\n\t\tExecute(\"REPORT\", ts.URL+\"/report\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n}\n\nfunc TestRequestQueryStringOrder(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tresp, err := New().R().\n\t\tSetQueryString(\"productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more\").\n\t\tGet(ts.URL + \"/?UniqueId=ead1d0ed-XXX-XXX-XXX-abb7612b3146&Translate=false&tempauth=eyJ0eXAiOiJKV1QiLC...HZEhwVnJ1d0NSUGVLaUpSaVNLRG5scz0&ApiVersion=2.0\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"200 OK\", resp.Status())\n\tassertEqual(t, \"TestGet: text response\", resp.String())\n\n\tlogResponse(t, resp)\n}\n\nfunc TestRequestOverridesClientAuthorizationHeader(t *testing.T) {\n\tts := createAuthServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tc.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).\n\t\tSetHeader(\"Authorization\", \"some token\").\n\t\tSetBaseURL(ts.URL + \"/\")\n\n\tresp, err := c.R().\n\t\tSetHeader(\"Authorization\", \"Bearer 004DDB79-6801-4587-B976-F093E6AC44FF\").\n\t\tGet(\"/profile\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n}\n\nfunc TestRequestFileUploadAsReader(t *testing.T) {\n\tts := createFileUploadServer(t)\n\tdefer ts.Close()\n\n\tfile, _ := os.Open(filepath.Join(getTestDataPath(), \"test-img.png\"))\n\tdefer file.Close()\n\tfi, _ := file.Stat()\n\n\tc := dcnl()\n\tc.SetRequestMiddlewares(\n\t\tMiddlewareRequestCreate,\n\t\tfunc(c *Client, r *Request) error {\n\t\t\t// validate content length values\n\t\t\tassertTrue(t, r.isContentLengthSet)\n\t\t\tassertTrue(t, r.contentLength == fi.Size())\n\t\t\tassertTrue(t, r.RawRequest.ContentLength == fi.Size())\n\t\t\tassertEqual(t, r.contentLength, r.RawRequest.ContentLength)\n\t\t\treturn nil\n\t\t},\n\t)\n\n\tresp, err := c.R().\n\t\tSetBody(file).\n\t\tSetContentType(\"image/png\").\n\t\tSetContentLength(fi.Size()).\n\t\tPost(ts.URL + \"/upload\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertTrue(t, strings.Contains(resp.String(), \"File Uploaded successfully\"))\n\n\tfile, _ = os.Open(filepath.Join(getTestDataPath(), \"test-img.png\"))\n\tdefer file.Close()\n\n\tresp, err = dcnldr().\n\t\tSetBody(file).\n\t\tSetContentType(\"image/png\").\n\t\tPost(ts.URL + \"/upload\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertTrue(t, strings.Contains(resp.String(), \"File Uploaded successfully\"))\n}\n\nfunc TestHostHeaderOverride(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tresp, err := dcnl().R().\n\t\tSetHeader(\"Host\", \"myhostname\").\n\t\tGet(ts.URL + \"/host-header\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"200 OK\", resp.Status())\n\tassertEqual(t, \"myhostname\", resp.String())\n\n\tlogResponse(t, resp)\n}\n\ntype HTTPErrorResponse struct {\n\tError string `json:\"error,omitempty\"`\n}\n\nfunc TestNotFoundWithError(t *testing.T) {\n\tvar httpError HTTPErrorResponse\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tresp, err := dcnl().R().\n\t\tSetHeader(hdrContentTypeKey, \"application/json\").\n\t\tSetResultError(&httpError).\n\t\tGet(ts.URL + \"/not-found-with-error\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusNotFound, resp.StatusCode())\n\tassertEqual(t, \"404 Not Found\", resp.Status())\n\tassertNotNil(t, httpError)\n\tassertEqual(t, \"Not found\", httpError.Error)\n\n\tlogResponse(t, resp)\n}\n\nfunc TestNotFoundWithoutError(t *testing.T) {\n\tvar httpError HTTPErrorResponse\n\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl().outputLogTo(os.Stdout)\n\tresp, err := c.R().\n\t\tSetResultError(&httpError).\n\t\tSetHeader(hdrContentTypeKey, \"application/json\").\n\t\tGet(ts.URL + \"/not-found-no-error\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusNotFound, resp.StatusCode())\n\tassertEqual(t, \"404 Not Found\", resp.Status())\n\tassertNotNil(t, httpError)\n\tassertEqual(t, \"\", httpError.Error)\n\n\tlogResponse(t, resp)\n}\n\nfunc TestPathParamURLInput(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl().\n\t\tSetBaseURL(ts.URL).\n\t\tSetPathParams(map[string]string{\n\t\t\t\"userId\": \"sample@sample.com\",\n\t\t\t\"path\":   \"users/developers\",\n\t\t})\n\n\tresp, err := c.R().\n\t\tSetDebug(true).\n\t\tSetPathParams(map[string]string{\n\t\t\t\"subAccountId\": \"100002\",\n\t\t\t\"website\":      \"https://example.com\",\n\t\t}).Get(\"/v1/users/{userId}/{subAccountId}/{path}/{website}\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertTrue(t, strings.Contains(resp.String(), \"TestPathParamURLInput: text response\"))\n\tassertTrue(t, strings.Contains(resp.String(), \"/v1/users/sample@sample.com/100002/users%2Fdevelopers/https:%2F%2Fexample.com\"))\n\n\tlogResponse(t, resp)\n}\n\nfunc TestRawPathParamURLInput(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl().\n\t\tSetBaseURL(ts.URL).\n\t\tSetPathRawParams(map[string]string{\n\t\t\t\"userId\": \"sample@sample.com\",\n\t\t\t\"path\":   \"users/developers\",\n\t\t})\n\n\tassertEqual(t, \"sample@sample.com\", c.PathParams()[\"userId\"])\n\tassertEqual(t, \"users/developers\", c.PathParams()[\"path\"])\n\n\tresp, err := c.R().SetDebug(true).\n\t\tSetPathRawParams(map[string]string{\n\t\t\t\"subAccountId\": \"100002\",\n\t\t\t\"website\":      \"https://example.com\",\n\t\t}).Get(\"/v1/users/{userId}/{subAccountId}/{path}/{website}\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertTrue(t, strings.Contains(resp.String(), \"TestPathParamURLInput: text response\"))\n\tassertTrue(t, strings.Contains(resp.String(), \"/v1/users/sample@sample.com/100002/users/developers/https://example.com\"))\n\n\tlogResponse(t, resp)\n}\n\n// This test case is kind of pass always\nfunc TestTraceInfo(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tserverAddr := ts.URL[strings.LastIndex(ts.URL, \"/\")+1:]\n\n\tclient := dcnl()\n\n\tt.Run(\"enable trace on client\", func(t *testing.T) {\n\t\tclient.SetBaseURL(ts.URL).SetTrace(true)\n\t\tfor _, u := range []string{\"/\", \"/json\", \"/long-text\", \"/long-json\"} {\n\t\t\tresp, err := client.R().Get(u)\n\t\t\tassertNil(t, err)\n\t\t\tassertNotNil(t, resp)\n\n\t\t\ttr := resp.Request.TraceInfo()\n\t\t\tassertTrue(t, tr.DNSLookup >= 0)\n\t\t\tassertTrue(t, tr.ConnTime >= 0)\n\t\t\tassertTrue(t, tr.TLSHandshake >= 0)\n\t\t\tassertTrue(t, tr.ServerTime >= 0)\n\t\t\tassertTrue(t, tr.ResponseTime >= 0)\n\t\t\tassertTrue(t, tr.TotalTime >= 0)\n\t\t\tassertTrue(t, tr.TotalTime < time.Hour)\n\t\t\tassertTrue(t, tr.TotalTime == resp.Duration())\n\t\t\tassertEqual(t, tr.RemoteAddr, serverAddr)\n\n\t\t\tassertNotNil(t, tr.Clone())\n\t\t}\n\n\t\tclient.SetTrace(false)\n\t})\n\n\tt.Run(\"enable trace on request\", func(t *testing.T) {\n\t\tfor _, u := range []string{\"/\", \"/json\", \"/long-text\", \"/long-json\"} {\n\t\t\tresp, err := client.R().SetTrace(true).Get(u)\n\t\t\tassertNil(t, err)\n\t\t\tassertNotNil(t, resp)\n\n\t\t\ttr := resp.Request.TraceInfo()\n\t\t\tassertTrue(t, tr.DNSLookup >= 0)\n\t\t\tassertTrue(t, tr.ConnTime >= 0)\n\t\t\tassertTrue(t, tr.TLSHandshake >= 0)\n\t\t\tassertTrue(t, tr.ServerTime >= 0)\n\t\t\tassertTrue(t, tr.ResponseTime >= 0)\n\t\t\tassertTrue(t, tr.TotalTime >= 0)\n\t\t\tassertTrue(t, tr.TotalTime == resp.Duration())\n\t\t\tassertEqual(t, tr.RemoteAddr, serverAddr)\n\t\t}\n\n\t})\n\n\tt.Run(\"enable trace on invalid request, issue #1016\", func(t *testing.T) {\n\t\tresp, err := client.R().SetTrace(true).Get(\"unknown://url.com\")\n\t\tassertNotNil(t, err)\n\t\ttr := resp.Request.TraceInfo()\n\t\tassertTrue(t, tr.DNSLookup == 0)\n\t\tassertTrue(t, tr.ConnTime == 0)\n\t\tassertTrue(t, tr.TLSHandshake == 0)\n\t\tassertTrue(t, tr.ServerTime == 0)\n\t\tassertTrue(t, tr.ResponseTime == 0)\n\t\tassertTrue(t, tr.TotalTime > 0 && tr.TotalTime < time.Second)\n\t})\n\n\tt.Run(\"enable trace and debug on request\", func(t *testing.T) {\n\t\tc, logBuf := dcldb()\n\t\tc.SetBaseURL(ts.URL)\n\n\t\trequestURLs := []string{\"/\", \"/json\", \"/long-text\", \"/long-json\"}\n\t\tfor _, u := range requestURLs {\n\t\t\tresp, err := c.R().SetTrace(true).SetDebug(true).Get(u)\n\t\t\tassertNil(t, err)\n\t\t\tassertNotNil(t, resp)\n\n\t\t\tjsonStr := resp.Request.TraceInfo().JSON()\n\t\t\tassertTrue(t, strings.Contains(jsonStr, serverAddr))\n\t\t}\n\n\t\tlogContent := logBuf.String()\n\t\tregexTraceInfoHeader := regexp.MustCompile(\"TRACE INFO:\")\n\t\tmatches := regexTraceInfoHeader.FindAllStringIndex(logContent, -1)\n\t\tassertEqual(t, len(requestURLs), len(matches))\n\t})\n\n\tt.Run(\"enable trace and debug on request json formatter\", func(t *testing.T) {\n\t\tc, logBuf := dcldb()\n\t\tc.SetBaseURL(ts.URL)\n\t\tc.SetDebugLogFormatter(DebugLogJSONFormatter)\n\n\t\trequestURLs := []string{\"/\", \"/json\", \"/long-text\", \"/long-json\"}\n\t\tfor _, u := range requestURLs {\n\t\t\tresp, err := c.R().SetTrace(true).SetDebug(true).Get(u)\n\t\t\tassertNil(t, err)\n\t\t\tassertNotNil(t, resp)\n\t\t}\n\n\t\tlogContent := logBuf.String()\n\t\tregexTraceInfoHeader := regexp.MustCompile(`\"trace_info\":{\"`)\n\t\tmatches := regexTraceInfoHeader.FindAllStringIndex(logContent, -1)\n\t\tassertEqual(t, len(requestURLs), len(matches))\n\t})\n\n\t// for sake of hook funcs\n\t_, _ = client.R().SetTrace(true).Get(\"https://httpbin.org/get\")\n}\n\nfunc TestTraceInfoWithoutEnableTrace(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tclient := dcnl()\n\tclient.SetBaseURL(ts.URL)\n\tfor _, u := range []string{\"/\", \"/json\", \"/long-text\", \"/long-json\"} {\n\t\tresp, err := client.R().Get(u)\n\t\tassertNil(t, err)\n\t\tassertNotNil(t, resp)\n\n\t\ttr := resp.Request.TraceInfo()\n\t\tassertTrue(t, tr.DNSLookup == 0)\n\t\tassertTrue(t, tr.ConnTime == 0)\n\t\tassertTrue(t, tr.TLSHandshake == 0)\n\t\tassertTrue(t, tr.ServerTime == 0)\n\t\tassertTrue(t, tr.ResponseTime == 0)\n\t\tassertTrue(t, tr.TotalTime == 0)\n\t}\n}\n\nfunc TestTraceInfoOnTimeout(t *testing.T) {\n\tclient := NewWithTransportSettings(&TransportSettings{\n\t\tDialerTimeout: 100 * time.Millisecond,\n\t}).\n\t\tSetBaseURL(\"http://resty-nowhere.local\").\n\t\tSetTrace(true)\n\n\tresp, err := client.R().Get(\"/\")\n\tassertNotNil(t, err)\n\tassertNotNil(t, resp)\n\n\ttr := resp.Request.TraceInfo()\n\tassertTrue(t, tr.DNSLookup >= 0)\n\tassertTrue(t, tr.ConnTime == 0)\n\tassertTrue(t, tr.TLSHandshake == 0)\n\tassertTrue(t, tr.TCPConnTime == 0)\n\tassertTrue(t, tr.ServerTime == 0)\n\tassertTrue(t, tr.ResponseTime == 0)\n\tassertTrue(t, tr.TotalTime > 0)\n\tassertTrue(t, tr.TotalTime == resp.Duration())\n}\n\nfunc TestTraceInfoOnTimeoutWithSetTimeout(t *testing.T) {\n\tt.Run(\"timeout with very short timeout\", func(t *testing.T) {\n\t\tclient := New().\n\t\t\tSetTimeout(1 * time.Millisecond).\n\t\t\tSetBaseURL(\"http://resty-nowhere.local\").\n\t\t\tSetTrace(true)\n\n\t\tresp, err := client.R().Get(\"/\")\n\t\tassertNotNil(t, err)\n\t\tassertNotNil(t, resp)\n\n\t\ttr := resp.Request.TraceInfo()\n\n\t\tassertTrue(t, tr.DNSLookup == 0)\n\t\tassertTrue(t, tr.ConnTime == 0)\n\t\tassertTrue(t, tr.TLSHandshake == 0)\n\t\tassertTrue(t, tr.TCPConnTime == 0)\n\t\tassertTrue(t, tr.ServerTime == 0)\n\t\tassertTrue(t, tr.ResponseTime == 0)\n\t\tassertTrue(t, tr.TotalTime > 0)\n\t\tassertTrue(t, tr.TotalTime == resp.Duration())\n\t})\n\n\tt.Run(\"successful request with SetTimeout\", func(t *testing.T) {\n\t\tts := createGetServer(t)\n\t\tdefer ts.Close()\n\n\t\tclient := New().\n\t\t\tSetTimeout(5 * time.Second).\n\t\t\tSetBaseURL(ts.URL).\n\t\t\tSetTrace(true)\n\n\t\tresp, err := client.R().Get(\"/\")\n\t\tassertNil(t, err)\n\t\tassertNotNil(t, resp)\n\n\t\ttr := resp.Request.TraceInfo()\n\n\t\tassertTrue(t, tr.DNSLookup >= 0)\n\t\tassertTrue(t, tr.ConnTime >= 0)\n\t\tassertTrue(t, tr.TLSHandshake >= 0)\n\t\tassertTrue(t, tr.TCPConnTime >= 0)\n\t\tassertTrue(t, tr.ServerTime >= 0)\n\t\tassertTrue(t, tr.ResponseTime >= 0)\n\t\tassertTrue(t, tr.TotalTime > 0)\n\t\tassertTrue(t, tr.TotalTime == resp.Duration())\n\t})\n\n\tt.Run(\"HTTPS request with TLS handshake\", func(t *testing.T) {\n\t\tts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t_, _ = w.Write([]byte(\"OK\"))\n\t\t}))\n\t\tdefer ts.Close()\n\n\t\tclient := New().\n\t\t\tSetTimeout(5 * time.Second).\n\t\t\tSetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}).\n\t\t\tSetTrace(true)\n\n\t\tresp, err := client.R().Get(ts.URL)\n\t\tassertNil(t, err)\n\t\tassertNotNil(t, resp)\n\n\t\ttr := resp.Request.TraceInfo()\n\n\t\tassertTrue(t, tr.TLSHandshake > 0)\n\t\tassertTrue(t, tr.DNSLookup >= 0)\n\t\tassertTrue(t, tr.ConnTime >= 0)\n\t\tassertTrue(t, tr.TCPConnTime >= 0)\n\t\tassertTrue(t, tr.ServerTime >= 0)\n\t\tassertTrue(t, tr.ResponseTime >= 0)\n\t\tassertTrue(t, tr.TotalTime > 0)\n\t\tassertTrue(t, tr.TotalTime == resp.Duration())\n\t})\n}\n\nfunc TestDebugLoggerRequestBodyTooLarge(t *testing.T) {\n\tformTs := createFormPostServer(t)\n\tdefer formTs.Close()\n\n\tdebugBodySizeLimit := 512\n\n\tt.Run(\"post form with more than 512 bytes data\", func(t *testing.T) {\n\t\toutput := bytes.NewBufferString(\"\")\n\t\tresp, err := New().SetDebug(true).outputLogTo(output).SetDebugBodyLimit(debugBodySizeLimit).R().\n\t\t\tSetFormData(map[string]string{\n\t\t\t\t\"first_name\": \"Alex\",\n\t\t\t\t\"last_name\":  strings.Repeat(\"C\", int(debugBodySizeLimit)),\n\t\t\t\t\"zip_code\":   \"00001\",\n\t\t\t}).\n\t\t\tSetBasicAuth(\"myuser\", \"mypass\").\n\t\t\tPost(formTs.URL + \"/profile\")\n\t\tassertNil(t, err)\n\t\tassertNotNil(t, resp)\n\t\tassertTrue(t, strings.Contains(output.String(), \"REQUEST TOO LARGE\"))\n\t})\n\n\tt.Run(\"post form with no more than 512 bytes data\", func(t *testing.T) {\n\t\toutput := bytes.NewBufferString(\"\")\n\t\tresp, err := New().outputLogTo(output).SetDebugBodyLimit(debugBodySizeLimit).R().\n\t\t\tSetDebug(true).\n\t\t\tSetFormData(map[string]string{\n\t\t\t\t\"first_name\": \"Alex\",\n\t\t\t\t\"last_name\":  \"C\",\n\t\t\t\t\"zip_code\":   \"00001\",\n\t\t\t}).\n\t\t\tSetBasicAuth(\"myuser\", \"mypass\").\n\t\t\tPost(formTs.URL + \"/profile\")\n\t\tassertNil(t, err)\n\t\tassertNotNil(t, resp)\n\t\tassertTrue(t, strings.Contains(output.String(), \"Alex\"))\n\t})\n\n\tt.Run(\"post string with more than 512 bytes data\", func(t *testing.T) {\n\t\toutput := bytes.NewBufferString(\"\")\n\t\tresp, err := New().SetDebug(true).outputLogTo(output).SetDebugBodyLimit(debugBodySizeLimit).R().\n\t\t\tSetBody(`{\n\t\t\t\"first_name\": \"Alex\",\n\t\t\t\"last_name\": \"`+strings.Repeat(\"C\", int(debugBodySizeLimit))+`C\",\n\t\t\t\"zip_code\": \"00001\"}`).\n\t\t\tSetBasicAuth(\"myuser\", \"mypass\").\n\t\t\tPost(formTs.URL + \"/profile\")\n\t\tassertNil(t, err)\n\t\tassertNotNil(t, resp)\n\t\tassertTrue(t, strings.Contains(output.String(), \"REQUEST TOO LARGE\"))\n\t})\n\n\tt.Run(\"post string slice with more than 512 bytes data\", func(t *testing.T) {\n\t\toutput := bytes.NewBufferString(\"\")\n\t\tresp, err := New().outputLogTo(output).SetDebugBodyLimit(debugBodySizeLimit).R().\n\t\t\tSetDebug(true).\n\t\t\tSetBody([]string{strings.Repeat(\"hello\", debugBodySizeLimit)}).\n\t\t\tSetBasicAuth(\"myuser\", \"mypass\").\n\t\t\tPost(formTs.URL + \"/profile\")\n\t\tassertNil(t, err)\n\t\tassertNotNil(t, resp)\n\t\tassertTrue(t, strings.Contains(output.String(), \"REQUEST TOO LARGE\"))\n\t})\n\n}\n\nfunc TestPostMapTemporaryRedirect(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tresp, err := c.R().SetBody(map[string]string{\"username\": \"testuser\", \"password\": \"testpass\"}).\n\t\tPost(ts.URL + \"/redirect\")\n\n\tassertNil(t, err)\n\tassertNotNil(t, resp)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n}\n\nfunc TestPostWith204Response(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tresp, err := c.R().SetBody(map[string]string{\"username\": \"testuser\", \"password\": \"testpass\"}).\n\t\tPost(ts.URL + \"/204-response\")\n\n\tassertNil(t, err)\n\tassertNotNil(t, resp)\n\tassertEqual(t, http.StatusNoContent, resp.StatusCode())\n}\n\ntype brokenReadCloser struct{}\n\nfunc (b brokenReadCloser) Read(p []byte) (n int, err error) {\n\treturn 0, errors.New(\"read error\")\n}\n\nfunc (b brokenReadCloser) Close() error {\n\treturn nil\n}\n\nfunc TestPostBodyError(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tresp, err := c.R().SetBody(brokenReadCloser{}).Post(ts.URL + \"/redirect\")\n\tassertNotNil(t, err)\n\tassertEqual(t, \"read error\", errors.Unwrap(err).Error())\n\tassertNotNil(t, resp)\n}\n\nfunc TestSetResultMustNotPanicOnNil(t *testing.T) {\n\tdefer func() {\n\t\tif r := recover(); r != nil {\n\t\t\tt.Errorf(\"must not panic\")\n\t\t}\n\t}()\n\tdcnl().R().SetResult(nil)\n}\n\nfunc TestRequestClone(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\tparent := c.R()\n\n\t// set an non-interface value\n\tparent.URL = ts.URL\n\tparent.SetPathParam(\"name\", \"parent\")\n\tparent.SetPathRawParam(\"name\", \"parent\")\n\t// set http header\n\tparent.SetHeader(\"X-Header\", \"parent\")\n\t// set an interface value\n\tparent.SetBasicAuth(\"parent\", \"\")\n\tparent.bodyBuf = acquireBuffer()\n\tparent.bodyBuf.WriteString(\"parent\")\n\tparent.RawRequest = &http.Request{}\n\n\tclone := parent.Clone(context.Background())\n\n\t// assume parent request is used\n\t_, _ = parent.Get(ts.URL)\n\n\t// update value of non-interface type - change will only happen on clone\n\tclone.URL = \"http://localhost.clone\"\n\tclone.PathParams[\"name\"] = \"clone\"\n\t// update value of http header - change will only happen on clone\n\tclone.SetHeader(\"X-Header\", \"clone\")\n\t// update value of interface type - change will only happen on clone\n\tclone.credentials.Username = \"clone\"\n\tclone.bodyBuf.Reset()\n\tclone.bodyBuf.WriteString(\"clone\")\n\n\t// assert non-interface type\n\tassertEqual(t, \"http://localhost.clone\", clone.URL)\n\tassertEqual(t, ts.URL, parent.URL)\n\tassertEqual(t, \"clone\", clone.PathParams[\"name\"])\n\tassertEqual(t, \"parent\", parent.PathParams[\"name\"])\n\t// assert http header\n\tassertEqual(t, \"parent\", parent.Header.Get(\"X-Header\"))\n\tassertEqual(t, \"clone\", clone.Header.Get(\"X-Header\"))\n\t// assert interface type\n\tassertEqual(t, \"parent\", parent.credentials.Username)\n\tassertEqual(t, \"clone\", clone.credentials.Username)\n\tassertEqual(t, \"\", parent.bodyBuf.String())\n\tassertEqual(t, \"clone\", clone.bodyBuf.String())\n\n\t// parent request should have raw request while clone should not\n\tassertNil(t, clone.RawRequest)\n\tassertNotNil(t, parent.RawRequest)\n\tassertNotEqual(t, parent.RawRequest, clone.RawRequest)\n}\n\nfunc TestResponseBodyUnlimitedReads(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tuser := &credentials{Username: \"testuser\", Password: \"testpass\"}\n\n\tc := dcnl().\n\t\tSetJSONEscapeHTML(false).\n\t\tSetResponseBodyUnlimitedReads(true)\n\n\tassertTrue(t, c.ResponseBodyUnlimitedReads())\n\n\tresp, err := c.R().\n\t\tSetHeader(hdrContentTypeKey, \"application/json; charset=utf-8\").\n\t\tSetBody(user).\n\t\tSetResult(&AuthSuccess{}).\n\t\tPost(ts.URL + \"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, int64(50), resp.Size())\n\n\tt.Logf(\"Result Success: %q\", resp.Result().(*AuthSuccess))\n\n\tfor i := 1; i <= 5; i++ {\n\t\tb, err := io.ReadAll(resp.Body)\n\t\tassertNil(t, err)\n\t\tassertEqual(t, `{ \"id\": \"success\", \"message\": \"login successful\" }`, string(b))\n\t}\n\n\tlogResponse(t, resp)\n}\n\nfunc TestRequestAllowPayload(t *testing.T) {\n\tc := dcnl()\n\n\tt.Run(\"default method is GET\", func(t *testing.T) {\n\t\tr := c.R()\n\t\tresult1 := r.isPayloadSupported()\n\t\tassertFalse(t, result1)\n\n\t\tr.SetMethodGetAllowPayload(true)\n\t\tresult2 := r.isPayloadSupported()\n\t\tassertTrue(t, result2)\n\t})\n\n\tt.Run(\"method GET\", func(t *testing.T) {\n\t\tr := c.R().\n\t\t\tSetMethod(MethodGet)\n\n\t\tresult1 := r.isPayloadSupported()\n\t\tassertFalse(t, result1)\n\n\t\tr.SetMethodGetAllowPayload(true)\n\t\tresult2 := r.isPayloadSupported()\n\t\tassertTrue(t, result2)\n\t})\n\n\tt.Run(\"method POST\", func(t *testing.T) {\n\t\tr := c.R().\n\t\t\tSetMethod(MethodPost)\n\t\tresult1 := r.isPayloadSupported()\n\t\tassertTrue(t, result1)\n\t})\n\n\tt.Run(\"method PUT\", func(t *testing.T) {\n\t\tr := c.R().\n\t\t\tSetMethod(MethodPut)\n\t\tresult1 := r.isPayloadSupported()\n\t\tassertTrue(t, result1)\n\t})\n\n\tt.Run(\"method PATCH\", func(t *testing.T) {\n\t\tr := c.R().\n\t\t\tSetMethod(MethodPatch)\n\t\tresult1 := r.isPayloadSupported()\n\t\tassertTrue(t, result1)\n\t})\n\n\tt.Run(\"method DELETE\", func(t *testing.T) {\n\t\tr := c.R().\n\t\t\tSetMethod(MethodDelete)\n\n\t\tresult1 := r.isPayloadSupported()\n\t\tassertFalse(t, result1)\n\n\t\tr.SetMethodDeleteAllowPayload(true)\n\t\tresult2 := r.isPayloadSupported()\n\t\tassertTrue(t, result2)\n\t})\n\n\tt.Run(\"method HEAD\", func(t *testing.T) {\n\t\tr := c.R().\n\t\t\tSetMethod(MethodHead)\n\t\tresult1 := r.isPayloadSupported()\n\t\tassertFalse(t, result1)\n\t})\n\n\tt.Run(\"method OPTIONS\", func(t *testing.T) {\n\t\tr := c.R().\n\t\t\tSetMethod(MethodOptions)\n\t\tresult1 := r.isPayloadSupported()\n\t\tassertFalse(t, result1)\n\t})\n\n\tt.Run(\"method TRACE\", func(t *testing.T) {\n\t\tr := c.R().\n\t\t\tSetMethod(MethodTrace)\n\t\tresult1 := r.isPayloadSupported()\n\t\tassertFalse(t, result1)\n\t})\n\n}\n\nfunc TestRequestNoRetryOnNonIdempotentMethod(t *testing.T) {\n\tts := createFileUploadServer(t)\n\tdefer ts.Close()\n\n\tstr := \"test\"\n\tbuf := []byte(str)\n\n\tbufReader := bytes.NewReader(buf)\n\tbufCpy := make([]byte, len(buf))\n\n\tc := dcnl().\n\t\tSetTimeout(time.Second * 3).\n\t\tAddRetryHooks(\n\t\t\tfunc(response *Response, _ error) {\n\t\t\t\tread, err := bufReader.Read(bufCpy)\n\n\t\t\t\tassertNil(t, err)\n\t\t\t\tassertEqual(t, len(buf), read)\n\t\t\t\tassertEqual(t, str, string(bufCpy))\n\t\t\t},\n\t\t)\n\n\treq := c.R().\n\t\tSetRetryCount(3).\n\t\tSetFileReader(\"name\", \"filename\", bufReader)\n\tresp, err := req.Post(ts.URL + \"/set-reset-multipart-readers-test\")\n\n\tassertNil(t, err)\n\tassertEqual(t, 1, resp.Request.Attempt)\n\tassertEqual(t, 500, resp.StatusCode())\n}\n\nfunc TestRequestContextTimeout(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tt.Run(\"use client set timeout\", func(t *testing.T) {\n\t\tc := dcnl().SetTimeout(200 * time.Millisecond)\n\t\tassertTrue(t, c.Timeout() > 0)\n\n\t\treq := c.R()\n\t\tassertTrue(t, req.Timeout > 0)\n\n\t\t_, err := req.Get(ts.URL + \"/set-timeout-test\")\n\n\t\tassertTrue(t, errors.Is(err, context.DeadlineExceeded))\n\t})\n\n\tt.Run(\"use request set timeout\", func(t *testing.T) {\n\t\tc := dcnl()\n\t\tassertTrue(t, c.Timeout() == 0)\n\n\t\t_, err := c.R().\n\t\t\tSetTimeout(200 * time.Millisecond).\n\t\t\tGet(ts.URL + \"/set-timeout-test\")\n\n\t\tassertTrue(t, errors.Is(err, context.DeadlineExceeded))\n\t})\n\n\tt.Run(\"use external context for timeout\", func(t *testing.T) {\n\t\tctx, ctxCancelFunc := context.WithTimeout(context.Background(), 200*time.Millisecond)\n\t\tdefer ctxCancelFunc()\n\n\t\tc := dcnl()\n\t\t_, err := c.R().\n\t\t\tSetContext(ctx).\n\t\t\tGet(ts.URL + \"/set-timeout-test\")\n\n\t\tassertTrue(t, errors.Is(err, context.DeadlineExceeded))\n\t})\n\n}\n\nfunc TestRequestPanicContext(t *testing.T) {\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\tt.Errorf(\"The code did not panic\")\n\t\t}\n\t}()\n\n\tc := dcnl()\n\n\t//lint:ignore SA1012 test case nil check\n\t_ = c.R().WithContext(nil)\n}\n\nfunc TestRequestSetResultAndSetOutputFile(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\toutputFile := filepath.Join(getTestDataPath(), \"login-success.txt\")\n\tdefer cleanupFiles(outputFile)\n\n\tc := dcnl().SetBaseURL(ts.URL)\n\n\tres, err := c.R().\n\t\tSetHeader(hdrContentTypeKey, \"application/json; charset=utf-8\").\n\t\tSetBody(&credentials{Username: \"testuser\", Password: \"testpass\"}).\n\t\tSetResponseBodyUnlimitedReads(true).\n\t\tSetResult(&AuthSuccess{}).\n\t\tSetResponseSaveFileName(outputFile).\n\t\tPost(\"/login\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, res.StatusCode())\n\tassertEqual(t, int64(50), res.Size())\n\n\tloginResult := res.Result().(*AuthSuccess)\n\tassertEqual(t, \"success\", loginResult.ID)\n\tassertEqual(t, \"login successful\", loginResult.Message)\n\n\tfileContent, _ := os.ReadFile(outputFile)\n\tassertEqual(t, `{ \"id\": \"success\", \"message\": \"login successful\" }`, string(fileContent))\n}\n\nfunc TestRequestBodyContentLengthValidation(t *testing.T) {\n\tts := createGenericServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl().SetBaseURL(ts.URL)\n\n\tc.SetRequestMiddlewares(\n\t\tMiddlewareRequestCreate,\n\t\tfunc(c *Client, r *Request) error {\n\t\t\t// validate content length\n\t\t\tassertTrue(t, r.contentLength > 0)\n\t\t\tassertTrue(t, r.RawRequest.ContentLength > 0)\n\t\t\tassertEqual(t, r.contentLength, r.RawRequest.ContentLength)\n\t\t\treturn nil\n\t\t},\n\t)\n\n\tbuf := bytes.NewBuffer([]byte(`{\"content\":\"json content sending to server\"}`))\n\tres, err := c.R().\n\t\tSetHeader(hdrContentTypeKey, \"application/json\").\n\t\tSetBody(buf).\n\t\tPut(\"/json\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, res.StatusCode())\n\tassertEqual(t, `{\"response\":\"json response\"}`, res.String())\n\tassertEqual(t, int64(44), res.Request.RawRequest.ContentLength)\n}\n\nfunc TestRequestFuncs(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl().\n\t\tSetQueryParam(\"client_param\", \"true\").\n\t\tSetQueryParams(map[string]string{\"req_1\": \"value1\", \"req_3\": \"value3\"}).\n\t\tSetDebug(true)\n\n\taddRequestQueryParams := func(page, size int) func(r *Request) *Request {\n\t\treturn func(r *Request) *Request {\n\t\t\treturn r.SetQueryParam(\"page\", strconv.Itoa(page)).\n\t\t\t\tSetQueryParam(\"size\", strconv.Itoa(size)).\n\t\t\t\tSetQueryParam(\"request_no\", strconv.Itoa(int(time.Now().Unix())))\n\t\t}\n\t}\n\n\taddRequestHeaders := func(r *Request) *Request {\n\t\treturn r.SetHeader(hdrAcceptKey, \"application/json\").\n\t\t\tSetHeader(hdrUserAgentKey, \"my-client/v1.0\")\n\t}\n\n\tresp, err := c.R().\n\t\tFuncs(addRequestQueryParams(1, 100), addRequestHeaders).\n\t\tSetHeader(hdrUserAgentKey, \"Test Custom User agent\").\n\t\tGet(ts.URL + \"/\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"HTTP/1.1\", resp.Proto())\n\tassertEqual(t, \"200 OK\", resp.Status())\n\tassertEqual(t, \"TestGet: text response\", resp.String())\n}\n\nfunc TestHTTPWarnGH970(t *testing.T) {\n\tlookupText := \"Using sensitive credentials in HTTP mode is not secure. Use HTTPS\"\n\n\tt.Run(\"SSL used\", func(t *testing.T) {\n\t\tts := createAuthServerTLSOptional(t, true)\n\t\tdefer ts.Close()\n\n\t\tc, lb := dcldb()\n\t\tc.SetBaseURL(ts.URL).\n\t\t\tSetTLSClientConfig(&tls.Config{InsecureSkipVerify: true})\n\n\t\tres, err := c.R().\n\t\t\tSetAuthToken(\"004DDB79-6801-4587-B976-F093E6AC44FF\").\n\t\t\tGet(\"/profile\")\n\n\t\tassertNil(t, err)\n\t\tassertTrue(t, strings.Contains(res.String(), \"profile fetch successful\"))\n\t\tassertFalse(t, strings.Contains(lb.String(), lookupText))\n\t})\n\n\tt.Run(\"non-SSL used\", func(t *testing.T) {\n\t\tts := createAuthServerTLSOptional(t, false)\n\t\tdefer ts.Close()\n\n\t\tc, lb := dcldb()\n\t\tc.SetBaseURL(ts.URL)\n\n\t\tres, err := c.R().\n\t\t\tSetAuthToken(\"004DDB79-6801-4587-B976-F093E6AC44FF\").\n\t\t\tGet(\"/profile\")\n\n\t\tassertNil(t, err)\n\t\tassertTrue(t, strings.Contains(res.String(), \"profile fetch successful\"))\n\t\tassertTrue(t, strings.Contains(lb.String(), lookupText))\n\t})\n}\n\n// This test methods exist for test coverage purpose\n// to validate the getter and setter\nfunc TestRequestSettingsCoverage(t *testing.T) {\n\tc := dcnl()\n\n\tr1 := c.R()\n\tassertFalse(t, r1.IsCloseConnection)\n\tr1.SetCloseConnection(true)\n\tassertTrue(t, r1.IsCloseConnection)\n\n\tr2 := c.R()\n\tassertFalse(t, r2.IsTrace)\n\tr2.SetTrace(true)\n\tassertTrue(t, r2.IsTrace)\n\tr2.SetTrace(false)\n\tassertFalse(t, r2.IsTrace)\n\n\tr3 := c.R()\n\tassertFalse(t, r3.IsResponseBodyUnlimitedReads)\n\tr3.SetResponseBodyUnlimitedReads(true)\n\tassertTrue(t, r3.IsResponseBodyUnlimitedReads)\n\tr3.SetResponseBodyUnlimitedReads(false)\n\tassertFalse(t, r3.IsResponseBodyUnlimitedReads)\n\n\tr4 := c.R()\n\tassertFalse(t, r4.IsDebug)\n\tr4.SetDebug(true)\n\tassertTrue(t, r4.IsDebug)\n\tr4.SetDebug(false)\n\tassertFalse(t, r4.IsDebug)\n\n\tr5 := c.R()\n\tassertTrue(t, r5.IsRetryDefaultConditions)\n\tr5.SetRetryDefaultConditions(false)\n\tassertFalse(t, r5.IsRetryDefaultConditions)\n\tr5.SetRetryDefaultConditions(true)\n\tassertTrue(t, r5.IsRetryDefaultConditions)\n\n\tr6 := c.R()\n\tcustomAuthHeader := \"X-Custom-Authorization\"\n\tr6.SetHeaderAuthorizationKey(customAuthHeader)\n\tassertEqual(t, customAuthHeader, r6.HeaderAuthorizationKey)\n\n\tinvalidJsonBytes := []byte(`{\\\" \\\": \"value here\"}`)\n\tresult := jsonIndent(invalidJsonBytes)\n\tassertEqual(t, string(invalidJsonBytes), string(result))\n\n\tres := &Response{}\n\tassertNil(t, res.RedirectHistory())\n\n\tdefer func() {\n\t\tif rec := recover(); rec != nil {\n\t\t\tif err, ok := rec.(error); ok {\n\t\t\t\tassertTrue(t, strings.Contains(err.Error(), \"resty: Request.Clone nil context\"))\n\t\t\t}\n\t\t}\n\t}()\n\trc := c.R()\n\t//lint:ignore SA1012 test case nil check\n\trc2 := rc.Clone(nil)\n\tassertEqual(t, nil, rc2.ctx)\n}\n\nfunc TestRequestDataRace(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tusersmap := map[string]any{\n\t\t\"user1\": ExampleUser{FirstName: \"firstname1\", LastName: \"lastname1\", ZipCode: \"10001\"},\n\t\t\"user2\": &ExampleUser{FirstName: \"firstname2\", LastName: \"lastname3\", ZipCode: \"10002\"},\n\t\t\"user3\": ExampleUser{FirstName: \"firstname3\", LastName: \"lastname3\", ZipCode: \"10003\"},\n\t}\n\n\tvar users []map[string]any\n\tusers = append(users, usersmap)\n\n\tc := dcnl().SetBaseURL(ts.URL)\n\n\ttotalRequests := 4000\n\twg := sync.WaitGroup{}\n\twg.Add(totalRequests)\n\tfor i := 0; i < totalRequests; i++ {\n\t\tif i%100 == 0 {\n\t\t\ttime.Sleep(20 * time.Millisecond) // to prevent test server socket exhaustion\n\t\t}\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tres, err := c.R().SetContext(context.Background()).SetBody(users).Post(\"/usersmap\")\n\t\t\tassertError(t, err)\n\t\t\tassertEqual(t, http.StatusAccepted, res.StatusCode())\n\t\t}()\n\t}\n\twg.Wait()\n}\n"
  },
  {
    "path": "response.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n)\n\n//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\n// Response struct and methods\n//_______________________________________________________________________\n\n// Response struct holds response values of executed requests.\ntype Response struct {\n\tRequest     *Request\n\tBody        io.ReadCloser\n\tRawResponse *http.Response\n\tIsRead      bool\n\n\t// CascadeError field used to cascade the response processing and\n\t// middleware execution errors\n\tCascadeError error\n\n\tbodyBytes  []byte\n\tsize       int64\n\treceivedAt time.Time\n}\n\n// Status method returns the HTTP status string for the executed request.\n//\n//\tExample: 200 OK\nfunc (r *Response) Status() string {\n\tif r.RawResponse == nil {\n\t\treturn \"\"\n\t}\n\treturn r.RawResponse.Status\n}\n\n// StatusCode method returns the HTTP status code for the executed request.\n//\n//\tExample: 200\nfunc (r *Response) StatusCode() int {\n\tif r.RawResponse == nil {\n\t\treturn 0\n\t}\n\treturn r.RawResponse.StatusCode\n}\n\n// Proto method returns the HTTP response protocol used for the request.\nfunc (r *Response) Proto() string {\n\tif r.RawResponse == nil {\n\t\treturn \"\"\n\t}\n\treturn r.RawResponse.Proto\n}\n\n// Result method returns the unmarshalled result response object if it exists,\n// otherwise nil.\n//\n//\tclient := resty.New()\n//\tdefer client.Close()\n//\n//\tres, err := client.R().\n//\t   SetBody(User{\n//\t     Username: \"testuser\",\n//\t     Password: \"testpass\",\n//\t   }).\n//\t   SetResult(&LoginResponse{}).      // or SetResult(LoginResponse{}).\n//\t   SetResultError(&LoginErrorResponse{}).  // or SetResultError(LoginErrorResponse{}).\n//\t   Post(\"https://myapp.com/login\")\n//\n//\tfmt.Println(err, res)\n//\tfmt.Println(res.Result().(*LoginResponse))\n//\tfmt.Println(res.ResultError().(*LoginErrorResponse))\n//\n// See [Request.SetResult]\nfunc (r *Response) Result() any {\n\treturn r.Request.Result\n}\n\n// ResultError method returns the unmarshalled result error object if it exists,\n// otherwise nil.\n//\n//\tclient := resty.New()\n//\tdefer client.Close()\n//\n//\tres, err := client.R().\n//\t   SetBody(User{\n//\t     Username: \"testuser\",\n//\t     Password: \"testpass\",\n//\t   }).\n//\t   SetResult(&LoginResponse{}).            // or SetResult(LoginResponse{}).\n//\t   SetResultError(&LoginErrorResponse{}).  // or SetResultError(LoginErrorResponse{}).\n//\t   Post(\"https://myapp.com/login\")\n//\n//\tfmt.Println(err, res)\n//\tfmt.Println(res.Result().(*LoginResponse))\n//\tfmt.Println(res.ResultError().(*LoginErrorResponse))\n//\n// See [Request.SetResultError], [Client.SetResultError]\nfunc (r *Response) ResultError() any {\n\treturn r.Request.ResultError\n}\n\n// Header method returns the response headers\nfunc (r *Response) Header() http.Header {\n\tif r.RawResponse == nil {\n\t\treturn http.Header{}\n\t}\n\treturn r.RawResponse.Header\n}\n\n// Cookies method to returns all the response cookies\nfunc (r *Response) Cookies() []*http.Cookie {\n\tif r.RawResponse == nil {\n\t\treturn make([]*http.Cookie, 0)\n\t}\n\treturn r.RawResponse.Cookies()\n}\n\n// String method returns the body of the HTTP response as a `string`.\n// It returns an empty string if it is nil or the body is zero length.\n//\n// NOTE:\n//   - Returns an empty string on auto-unmarshal scenarios, unless\n//     [Client.SetResponseBodyUnlimitedReads] or [Request.SetResponseBodyUnlimitedReads] set.\n//   - Returns an empty string when [Client.SetResponseDoNotParse] or [Request.SetResponseDoNotParse] set.\nfunc (r *Response) String() string {\n\tr.readIfRequired()\n\treturn strings.TrimSpace(string(r.bodyBytes))\n}\n\n// Bytes method returns the body of the HTTP response as a byte slice.\n// It returns an empty byte slice if it is nil or the body is zero length.\n//\n// NOTE:\n//   - Returns an empty byte slice on auto-unmarshal scenarios, unless\n//     [Client.SetResponseBodyUnlimitedReads] or [Request.SetResponseBodyUnlimitedReads] set.\n//   - Returns an empty byte slice when [Client.SetResponseDoNotParse] or [Request.SetResponseDoNotParse] set.\nfunc (r *Response) Bytes() []byte {\n\tr.readIfRequired()\n\treturn r.bodyBytes\n}\n\n// Duration method returns the duration of HTTP response time from the request we sent\n// and received a request.\n//\n// See [Response.ReceivedAt] to know when the client received a response and see\n// `Response.Request.Time` to know when the client sent a request.\nfunc (r *Response) Duration() time.Duration {\n\tif r.Request.trace != nil {\n\t\treturn r.Request.TraceInfo().TotalTime\n\t}\n\treturn r.receivedAt.Sub(r.Request.StartTime)\n}\n\n// ReceivedAt method returns the time we received a response from the server for the request.\nfunc (r *Response) ReceivedAt() time.Time {\n\treturn r.receivedAt\n}\n\n// Size method returns the HTTP response size in bytes. Yeah, you can rely on HTTP `Content-Length`\n// header, however it won't be available for chucked transfer/compressed response.\n// Since Resty captures response size details when processing the response body\n// when possible. So that users get the actual size of response bytes.\nfunc (r *Response) Size() int64 {\n\tr.readIfRequired()\n\treturn r.size\n}\n\n// IsStatusSuccess method returns true if HTTP status `code >= 200 and <= 299` otherwise false.\n//\n// Example: 200, 201, 204, etc.\nfunc (r *Response) IsStatusSuccess() bool {\n\treturn r.StatusCode() > 199 && r.StatusCode() < 300\n}\n\n// IsStatusFailure method returns true if HTTP status `code >= 400` otherwise false.\n//\n// Example: 400, 500, etc.\nfunc (r *Response) IsStatusFailure() bool {\n\treturn r.StatusCode() > 399\n}\n\n// RedirectHistory method returns a redirect history slice with the URL and status code\nfunc (r *Response) RedirectHistory() []*RedirectInfo {\n\tif r.RawResponse == nil {\n\t\treturn nil\n\t}\n\n\tredirects := make([]*RedirectInfo, 0)\n\tres := r.RawResponse\n\tfor res != nil {\n\t\treq := res.Request\n\t\tredirects = append(redirects, &RedirectInfo{\n\t\t\tStatusCode: res.StatusCode,\n\t\t\tURL:        req.URL.String(),\n\t\t})\n\t\tres = req.Response\n\t}\n\n\treturn redirects\n}\n\nfunc (r *Response) setReceivedAt() {\n\tr.receivedAt = time.Now()\n\tif r.Request.trace != nil {\n\t\tr.Request.trace.endTime = r.receivedAt\n\t}\n}\n\nfunc (r *Response) fmtBodyString(sl int) string {\n\tif r.Request.IsResponseDoNotParse {\n\t\treturn \"***** DO NOT PARSE RESPONSE - Enabled *****\"\n\t}\n\n\tif r.Request.IsResponseSaveToFile {\n\t\treturn \"***** RESPONSE WRITTEN INTO FILE *****\"\n\t}\n\n\tbl := len(r.bodyBytes)\n\tif r.IsRead && bl == 0 {\n\t\treturn \"***** RESPONSE BODY IS ALREADY READ - see Response.{Result()/Error()} *****\"\n\t}\n\n\tif bl > 0 {\n\t\tif bl > sl {\n\t\t\treturn fmt.Sprintf(\"***** RESPONSE TOO LARGE (size - %d) *****\", bl)\n\t\t}\n\n\t\tct := r.Header().Get(hdrContentTypeKey)\n\t\tctKey := inferContentTypeMapKey(ct)\n\t\tif jsonKey == ctKey {\n\t\t\tout := acquireBuffer()\n\t\t\tdefer releaseBuffer(out)\n\t\t\terr := json.Indent(out, r.bodyBytes, \"\", \"   \")\n\t\t\tif err != nil {\n\t\t\t\tr.Request.log.Errorf(\"DebugLog: Response.fmtBodyString: %v\", err)\n\t\t\t\treturn \"\"\n\t\t\t}\n\t\t\treturn out.String()\n\t\t}\n\t\treturn r.String()\n\t}\n\n\treturn \"***** NO CONTENT *****\"\n}\n\nfunc (r *Response) readIfRequired() {\n\tif len(r.bodyBytes) == 0 && !r.Request.IsResponseDoNotParse {\n\t\t_ = r.readAll()\n\t}\n}\n\nvar ioReadAll = io.ReadAll\n\n// auto-unmarshal didn't happen, so fallback to\n// old behavior of reading response as body bytes\nfunc (r *Response) readAll() (err error) {\n\tif r.Body == nil || r.IsRead {\n\t\treturn nil\n\t}\n\n\tif _, ok := r.Body.(*copyReadCloser); ok {\n\t\t_, err = ioReadAll(r.Body)\n\t} else {\n\t\tr.bodyBytes, err = ioReadAll(r.Body)\n\t\tcloseq(r.Body)\n\t\tr.Body = &nopReadCloser{r: bytes.NewReader(r.bodyBytes), resetOnEOF: true}\n\t}\n\tif err == io.ErrUnexpectedEOF {\n\t\t// content-encoding scenario's - empty/no response body from server\n\t\terr = nil\n\t}\n\n\tr.IsRead = true\n\treturn\n}\n\nfunc (r *Response) wrapLimitReadCloser() {\n\tr.Body = &limitReadCloser{\n\t\tr: r.Body,\n\t\tl: r.Request.ResponseBodyLimit,\n\t\tf: func(s int64) {\n\t\t\tr.size = s\n\t\t},\n\t}\n}\n\nfunc (r *Response) wrapCopyReadCloser() {\n\tr.Body = &copyReadCloser{\n\t\ts: r.Body,\n\t\tt: acquireBuffer(),\n\t\tf: func(b *bytes.Buffer) {\n\t\t\tr.bodyBytes = append([]byte{}, b.Bytes()...)\n\t\t\tcloseq(r.Body)\n\t\t\tr.Body = &nopReadCloser{r: bytes.NewReader(r.bodyBytes), resetOnEOF: true}\n\t\t\treleaseBuffer(b)\n\t\t},\n\t}\n}\n\nfunc (r *Response) wrapContentDecompresser() error {\n\tce := r.Header().Get(hdrContentEncodingKey)\n\tif isStringEmpty(ce) {\n\t\treturn nil\n\t}\n\n\tif decFunc, f := r.Request.client.ContentDecompressers()[strings.ToLower(ce)]; f {\n\t\tdec, err := decFunc(r.Body)\n\t\tif err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\t// empty/no response body from server\n\t\t\t\terr = nil\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\tr.Body = dec\n\t\tr.Header().Del(hdrContentEncodingKey)\n\t\tr.Header().Del(hdrContentLengthKey)\n\t\tr.RawResponse.ContentLength = -1\n\t} else {\n\t\treturn ErrContentDecompresserNotFound\n\t}\n\n\treturn nil\n}\n\nfunc (r *Response) wrapError(err error, preserve bool) error {\n\tr.CascadeError = wrapErrors(err, r.CascadeError)\n\tif preserve {\n\t\treturn nil\n\t}\n\te := r.CascadeError\n\tr.CascadeError = nil\n\treturn e\n}\n"
  },
  {
    "path": "resty.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\n// Package resty provides Simple HTTP, REST, and SSE client library for Go.\npackage resty // import \"resty.dev/v3\"\n\nimport (\n\t\"math\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\t\"net/url\"\n\t\"runtime\"\n\t\"sync\"\n\t\"time\"\n\n\t\"golang.org/x/net/publicsuffix\"\n)\n\n// Version # of resty\nconst Version = \"3.0.0-beta.6\"\n\n// New method creates a new Resty client.\nfunc New() *Client {\n\treturn NewWithTransportSettings(nil)\n}\n\n// NewWithTransportSettings method creates a new Resty client with provided\n// timeout values.\nfunc NewWithTransportSettings(transportSettings *TransportSettings) *Client {\n\treturn NewWithDialerAndTransportSettings(nil, transportSettings)\n}\n\n// NewWithClient method creates a new Resty client with given [http.Client].\nfunc NewWithClient(hc *http.Client) *Client {\n\treturn createClient(hc)\n}\n\n// NewWithDialer method creates a new Resty client with given Local Address\n// to dial from.\nfunc NewWithDialer(dialer *net.Dialer) *Client {\n\treturn NewWithDialerAndTransportSettings(dialer, nil)\n}\n\n// NewWithLocalAddr method creates a new Resty client with the given Local Address.\nfunc NewWithLocalAddr(localAddr net.Addr) *Client {\n\treturn NewWithDialerAndTransportSettings(\n\t\t&net.Dialer{LocalAddr: localAddr},\n\t\tnil,\n\t)\n}\n\n// NewWithDialerAndTransportSettings method creates a new Resty client with given Local Address\n// to dial from.\nfunc NewWithDialerAndTransportSettings(dialer *net.Dialer, transportSettings *TransportSettings) *Client {\n\treturn createClient(&http.Client{\n\t\tJar:       createCookieJar(),\n\t\tTransport: createTransport(dialer, transportSettings),\n\t})\n}\n\n//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\n// Unexported methods\n//_______________________________________________________________________\n\nfunc createTransport(dialer *net.Dialer, transportSettings *TransportSettings) *http.Transport {\n\tif transportSettings == nil {\n\t\ttransportSettings = &TransportSettings{}\n\t}\n\n\t// Dialer\n\n\tif dialer == nil {\n\t\tdialer = &net.Dialer{}\n\t}\n\n\tif transportSettings.DialerTimeout > 0 {\n\t\tdialer.Timeout = transportSettings.DialerTimeout\n\t} else {\n\t\tdialer.Timeout = 30 * time.Second\n\t}\n\n\tif transportSettings.DialerKeepAlive > 0 {\n\t\tdialer.KeepAlive = transportSettings.DialerKeepAlive\n\t} else {\n\t\tdialer.KeepAlive = 30 * time.Second\n\t}\n\n\t// Transport\n\tt := &http.Transport{\n\t\tProxy:              http.ProxyFromEnvironment,\n\t\tDialContext:        transportDialContext(dialer),\n\t\tDisableKeepAlives:  transportSettings.DisableKeepAlives,\n\t\tDisableCompression: true, // Resty handles it, see [Client.AddContentDecoder]\n\t\tForceAttemptHTTP2:  true,\n\t}\n\n\tif transportSettings.IdleConnTimeout > 0 {\n\t\tt.IdleConnTimeout = transportSettings.IdleConnTimeout\n\t} else {\n\t\tt.IdleConnTimeout = 90 * time.Second\n\t}\n\n\tif transportSettings.TLSHandshakeTimeout > 0 {\n\t\tt.TLSHandshakeTimeout = transportSettings.TLSHandshakeTimeout\n\t} else {\n\t\tt.TLSHandshakeTimeout = 10 * time.Second\n\t}\n\n\tif transportSettings.ExpectContinueTimeout > 0 {\n\t\tt.ExpectContinueTimeout = transportSettings.ExpectContinueTimeout\n\t} else {\n\t\tt.ExpectContinueTimeout = 1 * time.Second\n\t}\n\n\tif transportSettings.MaxIdleConns > 0 {\n\t\tt.MaxIdleConns = transportSettings.MaxIdleConns\n\t} else {\n\t\tt.MaxIdleConns = 100\n\t}\n\n\tif transportSettings.MaxIdleConnsPerHost > 0 {\n\t\tt.MaxIdleConnsPerHost = transportSettings.MaxIdleConnsPerHost\n\t} else {\n\t\tt.MaxIdleConnsPerHost = runtime.GOMAXPROCS(0) + 1\n\t}\n\n\tif transportSettings.MaxConnsPerHost > 0 {\n\t\tt.MaxConnsPerHost = transportSettings.MaxConnsPerHost\n\t}\n\n\t//\n\t// No default value in Resty for following settings, added to\n\t// provide ability to set value otherwise the Go HTTP client\n\t// default value applies.\n\t//\n\n\tif transportSettings.ResponseHeaderTimeout > 0 {\n\t\tt.ResponseHeaderTimeout = transportSettings.ResponseHeaderTimeout\n\t}\n\n\tif transportSettings.MaxResponseHeaderBytes > 0 {\n\t\tt.MaxResponseHeaderBytes = transportSettings.MaxResponseHeaderBytes\n\t}\n\n\tif transportSettings.WriteBufferSize > 0 {\n\t\tt.WriteBufferSize = transportSettings.WriteBufferSize\n\t}\n\n\tif transportSettings.ReadBufferSize > 0 {\n\t\tt.ReadBufferSize = transportSettings.ReadBufferSize\n\t}\n\n\treturn t\n}\n\nfunc createCookieJar() *cookiejar.Jar {\n\tcookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})\n\treturn cookieJar\n}\n\nfunc createClient(hc *http.Client) *Client {\n\tc := &Client{ // not setting language default values\n\t\tlock:                     &sync.RWMutex{},\n\t\tqueryParams:              url.Values{},\n\t\tformData:                 url.Values{},\n\t\theader:                   http.Header{},\n\t\tauthScheme:               defaultAuthScheme,\n\t\tcookies:                  make([]*http.Cookie, 0),\n\t\tretryWaitTime:            defaultWaitTime,\n\t\tretryMaxWaitTime:         defaultMaxWaitTime,\n\t\tisRetryDefaultConditions: true,\n\t\tpathParams:               make(map[string]string),\n\t\theaderAuthorizationKey:   hdrAuthorizationKey,\n\t\tjsonEscapeHTML:           true,\n\t\thttpClient:               hc,\n\t\tdebugBodyLimit:           math.MaxInt32,\n\t\tcontentTypeEncoders:      make(map[string]ContentTypeEncoder),\n\t\tcontentTypeDecoders:      make(map[string]ContentTypeDecoder),\n\t\tcontentDecompresserKeys:  make([]string, 0),\n\t\tcontentDecompressers:     make(map[string]ContentDecompresser),\n\t\tcertWatcherStopChan:      make(chan bool),\n\t}\n\n\t// Logger\n\tc.SetLogger(createLogger())\n\tc.SetDebugLogFormatter(DebugLogFormatter)\n\n\tc.AddContentTypeEncoder(jsonKey, encodeJSON)\n\tc.AddContentTypeEncoder(xmlKey, encodeXML)\n\n\tc.AddContentTypeDecoder(jsonKey, decodeJSON)\n\tc.AddContentTypeDecoder(xmlKey, decodeXML)\n\n\t// Order matter, giving priority to gzip\n\tc.AddContentDecompresser(\"deflate\", decompressDeflate)\n\tc.AddContentDecompresser(\"gzip\", decompressGzip)\n\n\t// request middlewares\n\tc.SetRequestMiddlewares(\n\t\tMiddlewareRequestCreate,\n\t)\n\n\t// response middlewares\n\tc.SetResponseMiddlewares(\n\t\tMiddlewareResponseAutoParse,\n\t\tMiddlewareResponseSaveToFile,\n\t)\n\n\treturn c\n}\n"
  },
  {
    "path": "resty_test.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bytes\"\n\t\"compress/flate\"\n\t\"compress/gzip\"\n\t\"compress/lzw\"\n\t\"crypto/tls\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n)\n\nvar (\n\thdrLocationKey = http.CanonicalHeaderKey(\"Location\")\n)\n\n//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\n// Testing Unexported methods\n//___________________________________\n\nfunc getTestDataPath() string {\n\tpwd, _ := os.Getwd()\n\treturn filepath.Join(pwd, \".testdata\")\n}\n\nfunc createGetServer(t *testing.T) *httptest.Server {\n\tvar attempt int32\n\tvar sequence int32\n\tvar lastRequest time.Time\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tt.Logf(\"Method: %v\", r.Method)\n\t\tt.Logf(\"Path: %v\", r.URL.Path)\n\n\t\tif r.Method == MethodGet {\n\t\t\tswitch r.URL.Path {\n\t\t\tcase \"/\":\n\t\t\t\t_, _ = w.Write([]byte(\"TestGet: text response\"))\n\t\t\tcase \"/no-content\":\n\t\t\t\t_, _ = w.Write([]byte(\"\"))\n\t\t\tcase \"/json\":\n\t\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t\t_, _ = w.Write([]byte(`{\"TestGet\": \"JSON response\"}`))\n\t\t\tcase \"/json-invalid\":\n\t\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t\t_, _ = w.Write([]byte(\"TestGet: Invalid JSON\"))\n\t\t\tcase \"/long-text\":\n\t\t\t\t_, _ = w.Write([]byte(\"TestGet: text response with size > 30\"))\n\t\t\tcase \"/long-json\":\n\t\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t\t_, _ = w.Write([]byte(`{\"TestGet\": \"JSON response with size > 30\"}`))\n\t\t\tcase \"/mypage\":\n\t\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\tcase \"/mypage2\":\n\t\t\t\t_, _ = w.Write([]byte(\"TestGet: text response from mypage2\"))\n\t\t\tcase \"/set-retrycount-test\":\n\t\t\t\tattp := atomic.AddInt32(&attempt, 1)\n\t\t\t\tif attp <= 4 {\n\t\t\t\t\ttime.Sleep(time.Millisecond * 150)\n\t\t\t\t}\n\t\t\t\t_, _ = w.Write([]byte(\"TestClientRetry page\"))\n\t\t\tcase \"/set-retrywaittime-test\":\n\t\t\t\t// Returns time.Duration since last request here\n\t\t\t\t// or 0 for the very first request\n\t\t\t\tif atomic.LoadInt32(&attempt) == 0 {\n\t\t\t\t\tlastRequest = time.Now()\n\t\t\t\t\t_, _ = fmt.Fprint(w, \"0\")\n\t\t\t\t} else {\n\t\t\t\t\tnow := time.Now()\n\t\t\t\t\tsinceLastRequest := now.Sub(lastRequest)\n\t\t\t\t\tlastRequest = now\n\t\t\t\t\t_, _ = fmt.Fprintf(w, \"%d\", uint64(sinceLastRequest))\n\t\t\t\t}\n\t\t\t\tatomic.AddInt32(&attempt, 1)\n\n\t\t\tcase \"/set-retry-error-recover\":\n\t\t\t\tw.Header().Set(hdrContentTypeKey, \"application/json; charset=utf-8\")\n\t\t\t\tif atomic.LoadInt32(&attempt) == 0 {\n\t\t\t\t\tw.WriteHeader(http.StatusTooManyRequests)\n\t\t\t\t\t_, _ = w.Write([]byte(`{ \"message\": \"too many\" }`))\n\t\t\t\t} else {\n\t\t\t\t\t_, _ = w.Write([]byte(`{ \"message\": \"hello\" }`))\n\t\t\t\t}\n\t\t\t\tatomic.AddInt32(&attempt, 1)\n\t\t\tcase \"/set-timeout-test-with-sequence\":\n\t\t\t\tseq := atomic.AddInt32(&sequence, 1)\n\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\t\t_, _ = fmt.Fprintf(w, \"%d\", seq)\n\t\t\tcase \"/set-timeout-test\":\n\t\t\t\ttime.Sleep(400 * time.Millisecond)\n\t\t\t\t_, _ = w.Write([]byte(\"TestClientTimeout page\"))\n\t\t\tcase \"/my-image.png\":\n\t\t\t\tfileBytes, _ := os.ReadFile(filepath.Join(getTestDataPath(), \"test-img.png\"))\n\t\t\t\tw.Header().Set(\"Content-Type\", \"image/png\")\n\t\t\t\tw.Header().Set(\"Content-Length\", strconv.Itoa(len(fileBytes)))\n\t\t\t\tif r.URL.Query().Get(\"content-disposition\") == \"true\" {\n\t\t\t\t\tfilename := r.URL.Query().Get(\"filename\")\n\t\t\t\t\tw.Header().Set(hdrContentDisposition, \"inline; filename=\\\"\"+filename+\"\\\"\")\n\t\t\t\t}\n\t\t\t\t_, _ = w.Write(fileBytes)\n\t\t\tcase \"/get-method-payload-test\":\n\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\tif err != nil {\n\t\t\t\t\tt.Errorf(\"Error: could not read get body: %s\", err.Error())\n\t\t\t\t}\n\t\t\t\t_, _ = w.Write(body)\n\t\t\tcase \"/host-header\":\n\t\t\t\t_, _ = w.Write([]byte(r.Host))\n\t\t\tcase \"/not-found-with-error\":\n\t\t\t\tw.Header().Set(hdrContentTypeKey, \"application/json\")\n\t\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\t\t_, _ = w.Write([]byte(`{\"error\": \"Not found\"}`))\n\t\t\tcase \"/not-found-no-error\":\n\t\t\t\tw.Header().Set(hdrContentTypeKey, \"application/json\")\n\t\t\t\tw.WriteHeader(http.StatusNotFound)\n\t\t\tcase \"/retry-after-delay\":\n\t\t\t\tw.Header().Set(hdrContentTypeKey, \"application/json; charset=utf-8\")\n\t\t\t\tif atomic.LoadInt32(&attempt) == 0 {\n\t\t\t\t\tw.Header().Set(hdrRetryAfterKey, \"1\")\n\t\t\t\t\tw.WriteHeader(http.StatusTooManyRequests)\n\t\t\t\t\t_, _ = w.Write([]byte(`{ \"message\": \"too many\" }`))\n\t\t\t\t} else {\n\t\t\t\t\t_, _ = w.Write([]byte(`{ \"message\": \"hello\" }`))\n\t\t\t\t}\n\t\t\t\tatomic.AddInt32(&attempt, 1)\n\t\t\tcase \"/unescape-query-params\":\n\t\t\t\tinitOne := r.URL.Query().Get(\"initone\")\n\t\t\t\tfromClient := r.URL.Query().Get(\"fromclient\")\n\t\t\t\tregistry := r.URL.Query().Get(\"registry\")\n\t\t\t\tassertEqual(t, \"cáfe\", initOne)\n\t\t\t\tassertEqual(t, \"hey unescape\", fromClient)\n\t\t\t\tassertEqual(t, \"nacos://test:6801\", registry)\n\t\t\t\t_, _ = w.Write([]byte(`query params looks good`))\n\t\t\t}\n\n\t\t\tswitch {\n\t\t\tcase strings.HasPrefix(r.URL.Path, \"/v1/users/sample@sample.com/100002\"):\n\t\t\t\tif strings.HasSuffix(r.URL.Path, \"details\") {\n\t\t\t\t\t_, _ = w.Write([]byte(\"TestGetPathParams: text response: \" + r.URL.String()))\n\t\t\t\t} else {\n\t\t\t\t\t_, _ = w.Write([]byte(\"TestPathParamURLInput: text response: \" + r.URL.String()))\n\t\t\t\t}\n\t\t\t}\n\n\t\t}\n\t})\n\n\treturn ts\n}\n\nfunc handleLoginEndpoint(t *testing.T, w http.ResponseWriter, r *http.Request) {\n\tif r.URL.Path == \"/login\" {\n\t\tuser := &credentials{}\n\n\t\t// JSON\n\t\tif isJSONContentType(r.Header.Get(hdrContentTypeKey)) {\n\t\t\tjd := json.NewDecoder(r.Body)\n\t\t\terr := jd.Decode(user)\n\t\t\tif r.URL.Query().Get(\"ct\") == \"problem\" {\n\t\t\t\tw.Header().Set(hdrContentTypeKey, \"application/problem+json; charset=utf-8\")\n\t\t\t} else if r.URL.Query().Get(\"ct\") == \"rpc\" {\n\t\t\t\tw.Header().Set(hdrContentTypeKey, \"application/json-rpc\")\n\t\t\t} else {\n\t\t\t\tw.Header().Set(hdrContentTypeKey, \"AppLicAtioN/jsON\")\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tt.Logf(\"Error: %#v\", err)\n\t\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\t\t_, _ = w.Write([]byte(`{ \"id\": \"bad_request\", \"message\": \"Unable to read user info\" }`))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif user.Username == \"testuser\" && user.Password == \"testpass\" {\n\t\t\t\t_, _ = w.Write([]byte(`{ \"id\": \"success\", \"message\": \"login successful\" }`))\n\t\t\t} else if user.Username == \"testuser\" && user.Password == \"invalidjson\" {\n\t\t\t\t_, _ = w.Write([]byte(`{ \"id\": \"success\", \"message\": \"login successful\", }`))\n\t\t\t} else {\n\t\t\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\t\t\t_, _ = w.Write([]byte(`{ \"id\": \"unauthorized\", \"message\": \"Invalid credentials\" }`))\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\n\t\t// XML\n\t\tif isXMLContentType(r.Header.Get(hdrContentTypeKey)) {\n\t\t\txd := xml.NewDecoder(r.Body)\n\t\t\terr := xd.Decode(user)\n\n\t\t\tw.Header().Set(hdrContentTypeKey, \"application/xml\")\n\t\t\tif err != nil {\n\t\t\t\tt.Logf(\"Error: %v\", err)\n\t\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\t\t_, _ = w.Write([]byte(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>`))\n\t\t\t\t_, _ = w.Write([]byte(`<AuthError><Id>bad_request</Id><Message>Unable to read user info</Message></AuthError>`))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif user.Username == \"testuser\" && user.Password == \"testpass\" {\n\t\t\t\t_, _ = w.Write([]byte(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>`))\n\t\t\t\t_, _ = w.Write([]byte(`<AuthSuccess><Id>success</Id><Message>login successful</Message></AuthSuccess>`))\n\t\t\t} else if user.Username == \"testuser\" && user.Password == \"invalidxml\" {\n\t\t\t\t_, _ = w.Write([]byte(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>`))\n\t\t\t\t_, _ = w.Write([]byte(`<AuthSuccess><Id>success</Id><Message>login successful</AuthSuccess>`))\n\t\t\t} else {\n\t\t\t\tw.Header().Set(\"Www-Authenticate\", \"Protected Realm\")\n\t\t\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\t\t\t_, _ = w.Write([]byte(`<?xml version=\"1.0\" encoding=\"UTF-8\"?>`))\n\t\t\t\t_, _ = w.Write([]byte(`<AuthError><Id>unauthorized</Id><Message>Invalid credentials</Message></AuthError>`))\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc handleUsersEndpoint(t *testing.T, w http.ResponseWriter, r *http.Request) {\n\tif r.URL.Path == \"/users\" {\n\t\t// JSON\n\t\tif isJSONContentType(r.Header.Get(hdrContentTypeKey)) {\n\t\t\tvar users []ExampleUser\n\t\t\tjd := json.NewDecoder(r.Body)\n\t\t\terr := jd.Decode(&users)\n\t\t\tw.Header().Set(hdrContentTypeKey, \"application/json\")\n\t\t\tif err != nil {\n\t\t\t\tt.Logf(\"Error: %v\", err)\n\t\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\t\t_, _ = w.Write([]byte(`{ \"id\": \"bad_request\", \"message\": \"Unable to read user info\" }`))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// logic check, since we are excepting to reach 3 records\n\t\t\tif len(users) != 3 {\n\t\t\t\tt.Log(\"Error: Excepted count of 3 records\")\n\t\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\t\t_, _ = w.Write([]byte(`{ \"id\": \"bad_request\", \"message\": \"Expected record count doesn't match\" }`))\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\teu := users[2]\n\t\t\tif eu.FirstName == \"firstname3\" && eu.ZipCode == \"10003\" {\n\t\t\t\tw.WriteHeader(http.StatusAccepted)\n\t\t\t\t_, _ = w.Write([]byte(`{ \"message\": \"Accepted\" }`))\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc createPostServer(t *testing.T) *httptest.Server {\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tif r.Method == MethodPost {\n\t\t\thandleLoginEndpoint(t, w, r)\n\n\t\t\thandleUsersEndpoint(t, w, r)\n\t\t\tswitch r.URL.Path {\n\t\t\tcase \"/login-json-html\":\n\t\t\t\tw.Header().Set(hdrContentTypeKey, \"text/html\")\n\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t_, _ = w.Write([]byte(`{ \"id\": \"success\", \"message\": \"login successful\" }`))\n\t\t\t\treturn\n\t\t\tcase \"/usersmap\":\n\t\t\t\t// JSON\n\t\t\t\tif isJSONContentType(r.Header.Get(hdrContentTypeKey)) {\n\t\t\t\t\tif r.URL.Query().Get(\"status\") == \"500\" {\n\t\t\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tt.Errorf(\"Error: could not read post body: %s\", err.Error())\n\t\t\t\t\t\t}\n\t\t\t\t\t\tt.Logf(\"Got query param: status=500 so we're returning the post body as response and a 500 status code. body: %s\", string(body))\n\t\t\t\t\t\tw.Header().Set(hdrContentTypeKey, \"application/json; charset=utf-8\")\n\t\t\t\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\t\t\t\t_, _ = w.Write(body)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tvar users []map[string]any\n\t\t\t\t\tjd := json.NewDecoder(r.Body)\n\t\t\t\t\terr := jd.Decode(&users)\n\t\t\t\t\tw.Header().Set(hdrContentTypeKey, \"application/json; charset=utf-8\")\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tt.Logf(\"Error: %v\", err)\n\t\t\t\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\t\t\t\t_, _ = w.Write([]byte(`{ \"id\": \"bad_request\", \"message\": \"Unable to read user info\" }`))\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\t// logic check, since we are excepting to reach 1 map records\n\t\t\t\t\tif len(users) != 1 {\n\t\t\t\t\t\tt.Log(\"Error: Excepted count of 1 map records\")\n\t\t\t\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\t\t\t\t_, _ = w.Write([]byte(`{ \"id\": \"bad_request\", \"message\": \"Expected record count doesn't match\" }`))\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tw.WriteHeader(http.StatusAccepted)\n\t\t\t\t\t_, _ = w.Write([]byte(`{ \"message\": \"Accepted\" }`))\n\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\tcase \"/redirect\":\n\t\t\t\tw.Header().Set(hdrLocationKey, \"/login\")\n\t\t\t\tw.WriteHeader(http.StatusTemporaryRedirect)\n\t\t\tcase \"/redirect-with-body\":\n\t\t\t\tbody, _ := io.ReadAll(r.Body)\n\t\t\t\tquery := url.Values{}\n\t\t\t\tquery.Add(\"body\", string(body))\n\t\t\t\tw.Header().Set(hdrLocationKey, \"/redirected-with-body?\"+query.Encode())\n\t\t\t\tw.WriteHeader(http.StatusTemporaryRedirect)\n\t\t\tcase \"/redirected-with-body\":\n\t\t\t\tbody, _ := io.ReadAll(r.Body)\n\t\t\t\tassertEqual(t, r.URL.Query().Get(\"body\"), string(body))\n\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tcase \"/curl-cmd-post\":\n\t\t\t\tcookie := http.Cookie{\n\t\t\t\t\tName:    \"testserver\",\n\t\t\t\t\tDomain:  \"localhost\",\n\t\t\t\t\tPath:    \"/\",\n\t\t\t\t\tExpires: time.Now().AddDate(0, 0, 1),\n\t\t\t\t\tValue:   \"yes\",\n\t\t\t\t}\n\t\t\t\thttp.SetCookie(w, &cookie)\n\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\tcase \"/204-response\":\n\t\t\t\tw.WriteHeader(http.StatusNoContent)\n\t\t\t}\n\t\t}\n\t})\n\n\treturn ts\n}\n\nfunc createFormPostServer(t *testing.T) *httptest.Server {\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tt.Logf(\"Content-Type: %v\", r.Header.Get(hdrConnectionKey))\n\n\t\tif r.Method == MethodPost {\n\t\t\t_ = r.ParseMultipartForm(10e6)\n\n\t\t\tif r.URL.Path == \"/profile\" {\n\t\t\t\tif r.MultipartForm == nil {\n\t\t\t\t\tvalues := r.Form\n\t\t\t\t\tt.Log(values)\n\t\t\t\t} else {\n\t\t\t\t\tvalues := r.MultipartForm.Value\n\t\t\t\t\tt.Log(values)\n\t\t\t\t}\n\n\t\t\t\t_, _ = w.Write([]byte(\"Success\"))\n\t\t\t\treturn\n\t\t\t} else if r.URL.Path == \"/search\" {\n\t\t\t\tformEncodedData := r.Form.Encode()\n\t\t\t\tt.Logf(\"Received Form Encoded values: %v\", formEncodedData)\n\n\t\t\t\tassertTrue(t, strings.Contains(formEncodedData, \"search_criteria=pencil\"), \"expected search_criteria=pencil\")\n\t\t\t\tassertTrue(t, strings.Contains(formEncodedData, \"search_criteria=glass\"), \"expected search_criteria=glass\")\n\n\t\t\t\t_, _ = w.Write([]byte(\"Success\"))\n\t\t\t\treturn\n\t\t\t} else if r.URL.Path == \"/upload\" {\n\t\t\t\tt.Logf(\"FirstName: %v\", r.FormValue(\"first_name\"))\n\t\t\t\tt.Logf(\"LastName: %v\", r.FormValue(\"last_name\"))\n\n\t\t\t\ttargetPath := filepath.Join(getTestDataPath(), \"upload\")\n\t\t\t\t_ = os.MkdirAll(targetPath, 0700)\n\n\t\t\t\tvalues := r.MultipartForm.Value\n\t\t\t\tt.Logf(\"%v\", values)\n\n\t\t\t\tfor _, fhdrs := range r.MultipartForm.File {\n\t\t\t\t\tfor _, hdr := range fhdrs {\n\t\t\t\t\t\tt.Logf(\"Name: %v\", hdr.Filename)\n\t\t\t\t\t\tt.Logf(\"Header: %v\", hdr.Header)\n\t\t\t\t\t\tdotPos := strings.LastIndex(hdr.Filename, \".\")\n\n\t\t\t\t\t\tfname := fmt.Sprintf(\"%s-%v%s\", hdr.Filename[:dotPos], time.Now().Unix(), hdr.Filename[dotPos:])\n\t\t\t\t\t\tt.Logf(\"Write name: %v\", fname)\n\n\t\t\t\t\t\tinfile, _ := hdr.Open()\n\t\t\t\t\t\tf, err := os.OpenFile(filepath.Join(targetPath, fname), os.O_WRONLY|os.O_CREATE, 0666)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tt.Logf(\"Error: %v\", err)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdefer func() {\n\t\t\t\t\t\t\t_ = f.Close()\n\t\t\t\t\t\t}()\n\t\t\t\t\t\tsize, _ := io.Copy(f, infile)\n\n\t\t\t\t\t\t_, _ = w.Write([]byte(fmt.Sprintf(\"File: %v, uploaded as: %v, size: %v\\n\", hdr.Filename, fname, size)))\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tif r.Method == MethodPut {\n\n\t\t\tif r.URL.Path == \"/raw-upload\" {\n\t\t\t\tbody, _ := io.ReadAll(r.Body)\n\t\t\t\tbl, _ := strconv.Atoi(r.Header.Get(\"Content-Length\"))\n\t\t\t\tassertEqual(t, len(body), bl)\n\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t}\n\n\t\t}\n\t})\n\n\treturn ts\n}\n\nfunc createFormPatchServer(t *testing.T) *httptest.Server {\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tt.Logf(\"Method: %v\", r.Method)\n\t\tt.Logf(\"Path: %v\", r.URL.Path)\n\t\tt.Logf(\"Content-Type: %v\", r.Header.Get(hdrContentTypeKey))\n\n\t\tif r.Method == MethodPatch {\n\t\t\t_ = r.ParseMultipartForm(10e6)\n\n\t\t\tif r.URL.Path == \"/upload\" {\n\t\t\t\tt.Logf(\"FirstName: %v\", r.FormValue(\"first_name\"))\n\t\t\t\tt.Logf(\"LastName: %v\", r.FormValue(\"last_name\"))\n\n\t\t\t\ttargetPath := filepath.Join(getTestDataPath(), \"upload\")\n\t\t\t\t_ = os.MkdirAll(targetPath, 0700)\n\n\t\t\t\tfor _, fhdrs := range r.MultipartForm.File {\n\t\t\t\t\tfor _, hdr := range fhdrs {\n\t\t\t\t\t\tt.Logf(\"Name: %v\", hdr.Filename)\n\t\t\t\t\t\tt.Logf(\"Header: %v\", hdr.Header)\n\t\t\t\t\t\tdotPos := strings.LastIndex(hdr.Filename, \".\")\n\n\t\t\t\t\t\tfname := fmt.Sprintf(\"%s-%v%s\", hdr.Filename[:dotPos], time.Now().Unix(), hdr.Filename[dotPos:])\n\t\t\t\t\t\tt.Logf(\"Write name: %v\", fname)\n\n\t\t\t\t\t\tinfile, _ := hdr.Open()\n\t\t\t\t\t\tf, err := os.OpenFile(filepath.Join(targetPath, fname), os.O_WRONLY|os.O_CREATE, 0666)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tt.Logf(\"Error: %v\", err)\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\tdefer func() {\n\t\t\t\t\t\t\t_ = f.Close()\n\t\t\t\t\t\t}()\n\t\t\t\t\t\t_, _ = io.Copy(f, infile)\n\n\t\t\t\t\t\t_, _ = w.Write([]byte(fmt.Sprintf(\"File: %v, uploaded as: %v\\n\", hdr.Filename, fname)))\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t})\n\n\treturn ts\n}\n\nfunc createFileUploadServer(t *testing.T) *httptest.Server {\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tt.Logf(\"Method: %v\", r.Method)\n\t\tt.Logf(\"Path: %v\", r.URL.Path)\n\t\tt.Logf(\"Content-Type: %v\", r.Header.Get(hdrContentTypeKey))\n\n\t\tif r.Method != MethodPost && r.Method != MethodPut {\n\t\t\tt.Log(\"createFileUploadServer:: Not a POST or PUT request\")\n\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\tfmt.Fprint(w, http.StatusText(http.StatusBadRequest))\n\t\t\treturn\n\t\t}\n\n\t\ttargetPath := filepath.Join(getTestDataPath(), \"upload-large\")\n\t\t_ = os.MkdirAll(targetPath, 0700)\n\t\tdefer cleanupFiles(targetPath)\n\n\t\tswitch r.URL.Path {\n\t\tcase \"/upload\":\n\t\t\tf, err := os.OpenFile(filepath.Join(targetPath, \"large-file.png\"),\n\t\t\t\tos.O_WRONLY|os.O_CREATE, 0666)\n\t\t\tif err != nil {\n\t\t\t\tt.Logf(\"Error: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tdefer func() {\n\t\t\t\t_ = f.Close()\n\t\t\t}()\n\t\t\tsize, _ := io.Copy(f, r.Body)\n\n\t\t\tfmt.Fprintf(w, \"File Uploaded successfully, file size: %v\", size)\n\t\tcase \"/set-reset-multipart-readers-test\":\n\t\t\tw.Header().Set(hdrContentTypeKey, \"application/json; charset=utf-8\")\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\t_, _ = fmt.Fprintf(w, `{ \"message\": \"error\" }`)\n\t\t}\n\t})\n\n\treturn ts\n}\n\nfunc createAuthServer(t *testing.T) *httptest.Server {\n\treturn createAuthServerTLSOptional(t, true)\n}\n\nfunc createAuthServerTLSOptional(t *testing.T, useTLS bool) *httptest.Server {\n\thandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tt.Logf(`createAuthServerTLSOptional: Method: %v, Path: %v, Content-Type: %v`,\n\t\t\tr.Method, r.URL.Path, r.Header.Get(hdrContentTypeKey))\n\n\t\tif r.Method == MethodGet {\n\t\t\tif r.URL.Path == \"/profile\" {\n\t\t\t\t// 004DDB79-6801-4587-B976-F093E6AC44FF\n\t\t\t\tauth := r.Header.Get(\"Authorization\")\n\t\t\t\tt.Logf(\"Bearer Auth: %v\", auth)\n\n\t\t\t\tw.Header().Set(hdrContentTypeKey, \"application/json; charset=utf-8\")\n\n\t\t\t\tif strings.HasPrefix(auth, \"Basic \") {\n\t\t\t\t\tw.Header().Set(\"Www-Authenticate\", \"Protected Realm\")\n\t\t\t\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\t\t\t\t_, _ = w.Write([]byte(`{ \"id\": \"unauthorized\", \"message\": \"Invalid credentials\" }`))\n\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif strings.Contains(auth, \"004DDB79-6801-4587-B976-F093E6AC44FF\") {\n\t\t\t\t\t_, _ = w.Write([]byte(`{ \"username\": \"auth_test\", \"message\": \"profile fetch successful\" }`))\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\n\t\tif r.Method == MethodPost {\n\t\t\tif r.URL.Path == \"/login\" {\n\t\t\t\tauth := r.Header.Get(\"Authorization\")\n\t\t\t\tt.Logf(\"Basic Auth: %v\", auth)\n\n\t\t\t\t_, _ = io.ReadAll(r.Body)\n\n\t\t\t\tw.Header().Set(hdrContentTypeKey, \"application/json; charset=utf-8\")\n\n\t\t\t\tpassword, err := base64.StdEncoding.DecodeString(auth[6:])\n\t\t\t\tif err != nil || string(password) != \"myuser:basicauth\" {\n\t\t\t\t\tw.Header().Set(\"Www-Authenticate\", \"Protected Realm\")\n\t\t\t\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\t\t\t\t_, _ = w.Write([]byte(`{ \"id\": \"unauthorized\", \"message\": \"Invalid credentials\" }`))\n\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t_, _ = w.Write([]byte(`{ \"id\": \"success\", \"message\": \"login successful\" }`))\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\t})\n\tif useTLS {\n\t\treturn httptest.NewTLSServer(handler)\n\t}\n\treturn httptest.NewServer(handler)\n}\n\nfunc createGenericServer(t *testing.T) *httptest.Server {\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tt.Logf(\"Method: %v\", r.Method)\n\t\tt.Logf(\"Path: %v\", r.URL.Path)\n\n\t\tif r.Method == MethodGet {\n\t\t\tswitch r.URL.Path {\n\t\t\tcase \"/json-no-set\":\n\t\t\t\t// Set empty header value for testing, since Go server sets to\n\t\t\t\t// text/plain; charset=utf-8\n\t\t\t\tw.Header().Set(hdrContentTypeKey, \"\")\n\t\t\t\t_, _ = w.Write([]byte(`{\"response\":\"json response no content type set\"}`))\n\n\t\t\t// Gzip\n\t\t\tcase \"/gzip-test\":\n\t\t\t\tw.Header().Set(hdrContentTypeKey, plainTextType)\n\t\t\t\tw.Header().Set(hdrContentEncodingKey, \"gzip\")\n\t\t\t\tzw := gzip.NewWriter(w)\n\t\t\t\t_, _ = zw.Write([]byte(\"This is Gzip response testing\"))\n\t\t\t\tzw.Close()\n\t\t\tcase \"/gzip-test-gziped-empty-body\":\n\t\t\t\tw.Header().Set(hdrContentTypeKey, plainTextType)\n\t\t\t\tw.Header().Set(hdrContentEncodingKey, \"gzip\")\n\t\t\t\tzw := gzip.NewWriter(w)\n\t\t\t\t// write gziped empty body\n\t\t\t\t_, _ = zw.Write([]byte(\"\"))\n\t\t\t\tzw.Close()\n\t\t\tcase \"/gzip-test-no-gziped-body\":\n\t\t\t\tw.Header().Set(hdrContentTypeKey, plainTextType)\n\t\t\t\tw.Header().Set(hdrContentEncodingKey, \"gzip\")\n\t\t\t\t// don't write body\n\n\t\t\t// Deflate\n\t\t\tcase \"/deflate-test\":\n\t\t\t\tw.Header().Set(hdrContentTypeKey, plainTextType)\n\t\t\t\tw.Header().Set(hdrContentEncodingKey, \"deflate\")\n\t\t\t\tzw, _ := flate.NewWriter(w, flate.BestSpeed)\n\t\t\t\t_, _ = zw.Write([]byte(\"This is Deflate response testing\"))\n\t\t\t\tzw.Close()\n\t\t\tcase \"/deflate-test-empty-body\":\n\t\t\t\tw.Header().Set(hdrContentTypeKey, plainTextType)\n\t\t\t\tw.Header().Set(hdrContentEncodingKey, \"deflate\")\n\t\t\t\tzw, _ := flate.NewWriter(w, flate.BestSpeed)\n\t\t\t\t// write deflate empty body\n\t\t\t\t_, _ = zw.Write([]byte(\"\"))\n\t\t\t\tzw.Close()\n\t\t\tcase \"/deflate-test-no-body\":\n\t\t\t\tw.Header().Set(hdrContentTypeKey, plainTextType)\n\t\t\t\tw.Header().Set(hdrContentEncodingKey, \"deflate\")\n\t\t\t\t// don't write body\n\n\t\t\t// LZW\n\t\t\tcase \"/lzw-test\":\n\t\t\t\tw.Header().Set(hdrContentTypeKey, plainTextType)\n\t\t\t\tw.Header().Set(hdrContentEncodingKey, \"coMpReSs\")\n\t\t\t\tzw := lzw.NewWriter(w, lzw.LSB, 8)\n\t\t\t\t_, _ = zw.Write([]byte(\"This is LZW response testing\"))\n\t\t\t\tzw.Close()\n\t\t\tcase \"/lzw-test-empty-body\":\n\t\t\t\tw.Header().Set(hdrContentTypeKey, plainTextType)\n\t\t\t\tw.Header().Set(hdrContentEncodingKey, \"compress\")\n\t\t\t\tzw := lzw.NewWriter(w, lzw.LSB, 8)\n\t\t\t\t// write lzw empty body\n\t\t\t\t_, _ = zw.Write([]byte(\"\"))\n\t\t\t\tzw.Close()\n\t\t\tcase \"/lzw-test-no-body\":\n\t\t\t\tw.Header().Set(hdrContentTypeKey, plainTextType)\n\t\t\t\tw.Header().Set(hdrContentEncodingKey, \"compress\")\n\t\t\t\t// don't write body\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\n\t\tif r.Method == MethodPut {\n\t\t\tif r.URL.Path == \"/plaintext\" {\n\t\t\t\t_, _ = w.Write([]byte(\"TestPut: plain text response\"))\n\t\t\t} else if r.URL.Path == \"/json\" {\n\t\t\t\tw.Header().Set(hdrContentTypeKey, \"application/json; charset=utf-8\")\n\t\t\t\t_, _ = w.Write([]byte(`{\"response\":\"json response\"}`))\n\t\t\t} else if r.URL.Path == \"/xml\" {\n\t\t\t\tw.Header().Set(hdrContentTypeKey, \"application/xml\")\n\t\t\t\t_, _ = w.Write([]byte(`<?xml version=\"1.0\" encoding=\"UTF-8\"?><Response>XML response</Response>`))\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tif r.Method == MethodOptions && r.URL.Path == \"/options\" {\n\t\t\tw.Header().Set(\"Access-Control-Allow-Origin\", \"localhost\")\n\t\t\tw.Header().Set(\"Access-Control-Allow-Methods\", \"PUT, PATCH\")\n\t\t\tw.Header().Set(\"Access-Control-Expose-Headers\", \"x-go-resty-id\")\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\treturn\n\t\t}\n\n\t\tif r.Method == MethodPatch && r.URL.Path == \"/patch\" {\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\treturn\n\t\t}\n\n\t\tif r.Method == \"REPORT\" && r.URL.Path == \"/report\" {\n\t\t\tbody, _ := io.ReadAll(r.Body)\n\t\t\tif len(body) == 0 {\n\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tif r.Method == MethodTrace && r.URL.Path == \"/trace\" {\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\treturn\n\t\t}\n\n\t\tif r.Method == MethodDelete && r.URL.Path == \"/delete\" {\n\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Error: could not read get body: %s\", err.Error())\n\t\t\t}\n\t\t\t_, _ = w.Write(body)\n\t\t\treturn\n\t\t}\n\t})\n\n\treturn ts\n}\n\nfunc createRedirectServer(t *testing.T) *httptest.Server {\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tt.Logf(\"Method: %v\", r.Method)\n\t\tt.Logf(\"Path: %v\", r.URL.Path)\n\n\t\tif r.Method == MethodGet {\n\t\t\tif strings.HasPrefix(r.URL.Path, \"/redirect-host-check-\") {\n\t\t\t\tcntStr := strings.SplitAfter(r.URL.Path, \"-\")[3]\n\t\t\t\tcnt, _ := strconv.Atoi(cntStr)\n\n\t\t\t\tif cnt != 7 { // Testing hard stop via logical\n\t\t\t\t\tif cnt >= 5 {\n\t\t\t\t\t\thttp.Redirect(w, r, \"http://httpbin.org/get\", http.StatusTemporaryRedirect)\n\t\t\t\t\t} else {\n\t\t\t\t\t\thttp.Redirect(w, r, fmt.Sprintf(\"/redirect-host-check-%d\", cnt+1), http.StatusTemporaryRedirect)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if strings.HasPrefix(r.URL.Path, \"/redirect-\") {\n\t\t\t\tcntStr := strings.SplitAfter(r.URL.Path, \"-\")[1]\n\t\t\t\tcnt, _ := strconv.Atoi(cntStr)\n\n\t\t\t\thttp.Redirect(w, r, fmt.Sprintf(\"/redirect-%d\", cnt+1), http.StatusTemporaryRedirect)\n\t\t\t}\n\t\t}\n\t})\n\n\treturn ts\n}\n\nfunc createUnixSocketEchoServer(t *testing.T) string {\n\tsocketPath := filepath.Join(os.TempDir(), strconv.FormatInt(time.Now().Unix(), 10)) + \".sock\"\n\n\t// Create a Unix domain socket and listen for incoming connections.\n\tsocket, err := net.Listen(\"unix\", socketPath)\n\tif err != nil {\n\t\tt.Fatal(err)\n\t}\n\n\tm := http.NewServeMux()\n\tm.HandleFunc(\"/\", func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(\"Hi resty client from a server running on Unix domain socket!\\n\"))\n\t})\n\n\tm.HandleFunc(\"/hello\", func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(\"Hello resty client from a server running on endpoint /hello!\\n\"))\n\t})\n\n\tgo func(t *testing.T) {\n\t\tserver := http.Server{Handler: m}\n\t\tif err := server.Serve(socket); err != nil {\n\t\t\tt.Error(err)\n\t\t}\n\t}(t)\n\n\treturn socketPath\n}\n\nfunc createDigestServer(t *testing.T, conf *digestServerConfig) *httptest.Server {\n\tif conf == nil {\n\t\tconf = defaultDigestServerConf()\n\t}\n\n\tsetWWWAuthHeader := func(w http.ResponseWriter, v string) {\n\t\tw.Header().Set(\"WWW-Authenticate\", v)\n\t\tw.WriteHeader(http.StatusUnauthorized)\n\t}\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tt.Logf(\"Method: %v\", r.Method)\n\t\tt.Logf(\"Path: %v\", r.URL.Path)\n\n\t\tswitch r.URL.Path {\n\t\tcase \"/bad\":\n\t\t\tsetWWWAuthHeader(w, \"Bad Challenge\")\n\t\t\treturn\n\t\tcase \"/unknown_param\":\n\t\t\tsetWWWAuthHeader(w, \"Digest unknown_param=true\")\n\t\t\treturn\n\t\tcase \"/missing_value\":\n\t\t\tsetWWWAuthHeader(w, `Digest realm=\"hello\", domain`)\n\t\t\treturn\n\t\tcase \"/unclosed_quote\":\n\t\t\tsetWWWAuthHeader(w, `Digest realm=\"hello, qop=auth`)\n\t\t\treturn\n\t\tcase \"/no_challenge\":\n\t\t\tsetWWWAuthHeader(w, \"\")\n\t\t\treturn\n\t\tcase \"/status_500\":\n\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tw.Header().Set(hdrContentTypeKey, \"application/json; charset=utf-8\")\n\n\t\tif authorizationHeaderValid(t, r, conf) {\n\t\t\tif r.URL.Path == \"/dir/index.html\" && r.Method == MethodPost {\n\t\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\t\tassertNil(t, err)\n\t\t\t\tassertEqual(t, `{\"city\":\"Los Angeles\",\"zip_code\":\"00000\"}`, strings.TrimSpace(string(body)))\n\t\t\t}\n\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t_, _ = w.Write([]byte(`{ \"id\": \"success\", \"message\": \"login successful\" }`))\n\t\t} else {\n\t\t\tsetWWWAuthHeader(w,\n\t\t\t\tfmt.Sprintf(`Digest realm=\"%s\", domain=\"%s\", qop=\"%s\", algorithm=%s, nonce=\"%s\", opaque=\"%s\", userhash=true, charset=%s, stale=FALSE, nc=%s`,\n\t\t\t\t\tconf.realm, conf.uri, conf.qop, conf.algo, conf.nonce, conf.opaque, conf.charset, conf.nc))\n\t\t\t_, _ = w.Write([]byte(`{ \"id\": \"unauthorized\", \"message\": \"Invalid credentials\" }`))\n\t\t}\n\t})\n\n\treturn ts\n}\n\nfunc authorizationHeaderValid(t *testing.T, r *http.Request, conf *digestServerConfig) bool {\n\tinput := r.Header.Get(hdrAuthorizationKey)\n\tif input == \"\" {\n\t\treturn false\n\t}\n\n\tconst ws = \" \\n\\r\\t\"\n\tconst qs = `\"`\n\ts := strings.Trim(input, ws)\n\tassertTrue(t, strings.HasPrefix(s, \"Digest \"), \"Digest auth header prefix expected\")\n\ts = strings.Trim(s[7:], ws)\n\tsl := strings.Split(s, \", \")\n\n\tpairs := make(map[string]string, len(sl))\n\tfor i := range sl {\n\t\tpair := strings.SplitN(sl[i], \"=\", 2)\n\t\tpairs[pair[0]] = strings.Trim(pair[1], qs)\n\t}\n\n\tassertEqual(t, conf.algo, pairs[\"algorithm\"])\n\th := func(data string) string {\n\t\th := newHashFunc(pairs[\"algorithm\"])\n\t\t_, _ = h.Write([]byte(data))\n\t\treturn hex.EncodeToString(h.Sum(nil))\n\t}\n\n\tassertEqual(t, conf.opaque, pairs[\"opaque\"])\n\tassertEqual(t, \"true\", pairs[\"userhash\"])\n\n\tuserHash := h(fmt.Sprintf(\"%s:%s\", conf.username, conf.realm))\n\tassertEqual(t, userHash, pairs[\"username\"])\n\n\tha1 := h(fmt.Sprintf(\"%s:%s:%s\", conf.username, conf.realm, conf.password))\n\tif strings.HasSuffix(conf.algo, \"-sess\") {\n\t\tha1 = h(fmt.Sprintf(\"%s:%s:%s\", ha1, pairs[\"nonce\"], pairs[\"cnonce\"]))\n\t}\n\tha2 := h(fmt.Sprintf(\"%s:%s\", r.Method, conf.uri))\n\n\tqop := pairs[\"qop\"]\n\tif qop == \"\" {\n\t\tkd := h(fmt.Sprintf(\"%s:%s:%s\", ha1, pairs[\"nonce\"], ha2))\n\t\treturn kd == pairs[\"response\"]\n\t}\n\n\tnonceCount, err := strconv.Atoi(pairs[\"nc\"])\n\tassertError(t, err)\n\n\t// auth scenario\n\tif qop == qopAuth {\n\t\tkd := h(fmt.Sprintf(\"%s:%s\", ha1, fmt.Sprintf(\"%s:%08x:%s:%s:%s\",\n\t\t\tpairs[\"nonce\"], nonceCount, pairs[\"cnonce\"], pairs[\"qop\"], ha2)))\n\t\treturn kd == pairs[\"response\"]\n\t}\n\n\t// auth-int scenario\n\tbody, err := io.ReadAll(r.Body)\n\tr.Body.Close()\n\tr.Body = io.NopCloser(bytes.NewReader(body))\n\tassertError(t, err)\n\tbodyHash := \"\"\n\tif len(body) > 0 {\n\t\tbodyHash = h(string(body))\n\t}\n\n\tha2 = h(fmt.Sprintf(\"%s:%s:%s\", r.Method, conf.uri, bodyHash))\n\tkd := h(fmt.Sprintf(\"%s:%s\", ha1, fmt.Sprintf(\"%s:%08x:%s:%s:%s\",\n\t\tpairs[\"nonce\"], nonceCount, pairs[\"cnonce\"], pairs[\"qop\"], ha2)))\n\treturn kd == pairs[\"response\"]\n}\n\nfunc createTestServer(fn func(w http.ResponseWriter, r *http.Request)) *httptest.Server {\n\treturn httptest.NewServer(http.HandlerFunc(fn))\n}\n\nfunc createTestTLSServer(fn func(w http.ResponseWriter, r *http.Request), certPath, certKeyPath string) *httptest.Server {\n\tts := httptest.NewUnstartedServer(http.HandlerFunc(fn))\n\tts.TLS = &tls.Config{\n\t\tGetCertificate: func(*tls.ClientHelloInfo) (*tls.Certificate, error) {\n\t\t\tcert, err := tls.LoadX509KeyPair(certPath, certKeyPath)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn &cert, nil\n\t\t},\n\t}\n\tts.StartTLS()\n\treturn ts\n}\n\nfunc dcnl() *Client {\n\tc := New().\n\t\toutputLogTo(io.Discard)\n\treturn c\n}\n\nfunc dcnld() *Client {\n\treturn dcnl().SetDebug(true)\n}\n\nfunc dcldb() (*Client, *bytes.Buffer) {\n\tlogBuf := acquireBuffer()\n\tc := New().\n\t\tSetDebug(true).\n\t\toutputLogTo(logBuf)\n\treturn c, logBuf\n}\n\nfunc dcnlr() *Request {\n\treturn dcnl().R()\n}\n\nfunc dcnldr() *Request {\n\tc := dcnl().\n\t\tSetDebug(true)\n\treturn c.R()\n}\n\nfunc assertNil(t *testing.T, v any, failureMsgs ...string) {\n\tt.Helper()\n\tif !isNil(v) {\n\t\tt.Errorf(\"[%v] was expected to be nil. Message: %v\", v, strings.Join(failureMsgs, \" \"))\n\t}\n}\n\nfunc assertNotNil(t *testing.T, v any, failureMsgs ...string) {\n\tt.Helper()\n\tif isNil(v) {\n\t\tt.Errorf(\"[%v] was expected to be non-nil. Message: %v\", v, strings.Join(failureMsgs, \" \"))\n\t}\n}\n\nfunc assertType(t *testing.T, typ, v any, failureMsgs ...string) {\n\tt.Helper()\n\tif reflect.DeepEqual(reflect.TypeOf(typ), reflect.TypeOf(v)) {\n\t\tt.Errorf(\"Expected type %t, got %t. Message: %v\", typ, v, strings.Join(failureMsgs, \" \"))\n\t}\n}\n\nfunc assertError(t *testing.T, err error, failureMsgs ...string) {\n\tt.Helper()\n\tif err != nil {\n\t\tt.Errorf(\"Error occurred [%v]. Message: %v\", err, strings.Join(failureMsgs, \" \"))\n\t}\n}\n\nfunc assertErrorIs(t *testing.T, e, g error, failureMsgs ...string) (r bool) {\n\tt.Helper()\n\tif !errors.Is(g, e) {\n\t\tt.Errorf(\"Expected [%v], got [%v]. Message: %v\", e, g, strings.Join(failureMsgs, \" \"))\n\t}\n\n\treturn true\n}\n\nfunc assertTrue(t *testing.T, g any, failureMsgs ...string) (r bool) {\n\tt.Helper()\n\tif !equal(true, g) {\n\t\tt.Errorf(\"Expected `true`, got [%v]. Message: %v\", g, strings.Join(failureMsgs, \" \"))\n\t}\n\n\treturn\n}\n\nfunc assertFalse(t *testing.T, g any, failureMsgs ...string) (r bool) {\n\tt.Helper()\n\tif !equal(false, g) {\n\t\tt.Errorf(\"Expected `false`, got [%v]. Message: %v\", g, strings.Join(failureMsgs, \" \"))\n\t}\n\n\treturn\n}\n\nfunc assertEqual(t *testing.T, e, g any, failureMsgs ...string) (r bool) {\n\tt.Helper()\n\tif !equal(e, g) {\n\t\tt.Errorf(\"Expected [%v], got [%v]. Message: %v\", e, g, strings.Join(failureMsgs, \" \"))\n\t}\n\n\treturn\n}\n\nfunc assertNotEqual(t *testing.T, e, g any, failureMsgs ...string) (r bool) {\n\tt.Helper()\n\tif equal(e, g) {\n\t\tt.Errorf(\"Expected [%v], got [%v]. Message: %v\", e, g, strings.Join(failureMsgs, \" \"))\n\t} else {\n\t\tr = true\n\t}\n\n\treturn\n}\n\nfunc equal(expected, got any) bool {\n\treturn reflect.DeepEqual(expected, got)\n}\n\nfunc isNil(v any) bool {\n\tif v == nil {\n\t\treturn true\n\t}\n\n\trv := reflect.ValueOf(v)\n\tkind := rv.Kind()\n\tif kind >= reflect.Chan && kind <= reflect.Slice && rv.IsNil() {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc logResponse(t *testing.T, resp *Response) {\n\tt.Helper()\n\tt.Logf(\"Response Status: %v\", resp.Status())\n\tt.Logf(\"Response Duration: %v\", resp.Duration())\n\tt.Logf(\"Response Headers: %v\", resp.Header())\n\tt.Logf(\"Response Cookies: %v\", resp.Cookies())\n\tt.Logf(\"Response Body: %v\", resp)\n}\n\nfunc cleanupFiles(files ...string) {\n\tpwd, _ := os.Getwd()\n\n\tfor _, f := range files {\n\t\tif filepath.IsAbs(f) {\n\t\t\t_ = os.RemoveAll(f)\n\t\t} else {\n\t\t\t_ = os.RemoveAll(filepath.Join(pwd, f))\n\t\t}\n\t}\n}\n\nfunc createBinFile(fileName string, size int64) string {\n\tfp := filepath.Join(getTestDataPath(), fileName)\n\tf, _ := os.Create(fp)\n\t_ = f.Truncate(size)\n\t_ = f.Close()\n\treturn fp\n}\n"
  },
  {
    "path": "retry.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"crypto/tls\"\n\t\"math\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n)\n\nconst (\n\tdefaultWaitTime    = time.Duration(100) * time.Millisecond\n\tdefaultMaxWaitTime = time.Duration(2000) * time.Millisecond\n)\n\ntype (\n\t// RetryConditionFunc type is for the retry condition function\n\t// input: non-nil Response OR request execution error\n\tRetryConditionFunc func(*Response, error) bool\n\n\t// RetryHookFunc is for side-effecting functions triggered on retry\n\tRetryHookFunc func(*Response, error)\n\n\t// RetryDelayStrategyFunc is a type for implementing custom retry delay strategies.\n\t// By default, Resty employs the capped exponential backoff with a jitter delay strategy.\n\tRetryDelayStrategyFunc func(*Response, error) (time.Duration, error)\n)\n\n// RetryConstantDelayStrategy returns a RetryDelayStrategyFunc that always returns the specified delay duration.\nfunc RetryConstantDelayStrategy(delay time.Duration) RetryDelayStrategyFunc {\n\treturn func(*Response, error) (time.Duration, error) {\n\t\treturn delay, nil\n\t}\n}\n\nvar (\n\tregexErrTooManyRedirects = regexp.MustCompile(`stopped after \\d+ redirects\\z`)\n\tregexErrScheme           = regexp.MustCompile(\"unsupported protocol scheme\")\n\tregexErrInvalidHeader    = regexp.MustCompile(\"invalid header\")\n)\n\nfunc applyRetryDefaultConditions(res *Response, err error) bool {\n\t// no retry on TLS error\n\tif _, ok := err.(*tls.CertificateVerificationError); ok {\n\t\treturn false\n\t}\n\n\t// validate url error, so we can decide to retry or not\n\tif u, ok := err.(*url.Error); ok {\n\t\tif regexErrTooManyRedirects.MatchString(u.Error()) {\n\t\t\treturn false\n\t\t}\n\t\tif regexErrScheme.MatchString(u.Error()) {\n\t\t\treturn false\n\t\t}\n\t\tif regexErrInvalidHeader.MatchString(u.Error()) {\n\t\t\treturn false\n\t\t}\n\t\treturn u.Temporary() // possible retry if it's true\n\t}\n\n\tif res == nil {\n\t\treturn false\n\t}\n\n\t// certain HTTP status codes are temporary so that we can retry\n\t//\t- 429 Too Many Requests\n\t//\t- 500 or above (it's better to ignore 501 Not Implemented)\n\t//\t- 0 No status code received\n\tif res.StatusCode() == http.StatusTooManyRequests ||\n\t\t(res.StatusCode() >= 500 && res.StatusCode() != http.StatusNotImplemented) ||\n\t\tres.StatusCode() == 0 {\n\t\treturn true\n\t}\n\n\treturn false\n}\n\nfunc newBackoffWithJitter(min, max time.Duration) *backoffWithJitter {\n\tif min <= 0 {\n\t\tmin = defaultWaitTime\n\t}\n\tif max == 0 {\n\t\tmax = defaultMaxWaitTime\n\t}\n\n\treturn &backoffWithJitter{\n\t\tlock: new(sync.Mutex),\n\t\trnd:  rand.New(rand.NewSource(time.Now().UnixNano())),\n\t\tmin:  min,\n\t\tmax:  max,\n\t}\n}\n\ntype backoffWithJitter struct {\n\tlock *sync.Mutex\n\trnd  *rand.Rand\n\tmin  time.Duration\n\tmax  time.Duration\n}\n\nfunc (b *backoffWithJitter) NextWaitDuration(c *Client, res *Response, err error, attempt int) (time.Duration, error) {\n\tif res != nil {\n\t\tif res.StatusCode() == http.StatusTooManyRequests || res.StatusCode() == http.StatusServiceUnavailable {\n\t\t\tif delay, ok := parseRetryAfterHeader(res.Header().Get(hdrRetryAfterKey)); ok {\n\t\t\t\treturn delay, nil\n\t\t\t}\n\t\t}\n\t}\n\n\tconst maxInt = 1<<31 - 1 // max int for arch 386\n\tif b.max < 0 {\n\t\tb.max = maxInt\n\t}\n\n\tif res == nil || res.Request.RetryDelayStrategy == nil {\n\t\treturn b.balanceMinMax(b.defaultDelayStrategy(attempt)), nil\n\t}\n\n\t// invoke custom retry delay strategy\n\treturn res.Request.RetryDelayStrategy(res, err)\n}\n\n// Return capped exponential backoff with jitter\n// https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/\nfunc (b *backoffWithJitter) defaultDelayStrategy(attempt int) time.Duration {\n\ttemp := math.Min(float64(b.max), float64(b.min)*math.Exp2(float64(attempt)))\n\tri := time.Duration(temp / 2)\n\tif ri <= 0 {\n\t\tri = time.Nanosecond\n\t}\n\treturn b.randDuration(ri)\n}\n\nfunc (b *backoffWithJitter) randDuration(center time.Duration) time.Duration {\n\tb.lock.Lock()\n\tdefer b.lock.Unlock()\n\n\tvar ri = int64(center)\n\tvar jitter = b.rnd.Int63n(ri)\n\treturn time.Duration(math.Abs(float64(ri + jitter)))\n}\n\nfunc (b *backoffWithJitter) balanceMinMax(delay time.Duration) time.Duration {\n\tif delay <= 0 || b.max < delay {\n\t\treturn b.max\n\t}\n\tif delay < b.min {\n\t\treturn b.min\n\t}\n\treturn delay\n}\n\nvar timeNow = time.Now\n\n// parseRetryAfterHeader parses the Retry-After header and returns the\n// delay duration according to the spec: https://httpwg.org/specs/rfc7231.html#header.retry-after\n// The bool returned will be true if the header was successfully parsed.\n// Otherwise, the header was either not present, or was not parseable according to the spec.\n//\n// Retry-After headers come in two flavors: Seconds or HTTP-Date\n//\n// Examples:\n//   - Retry-After: Fri, 31 Dec 1999 23:59:59 GMT\n//   - Retry-After: 120\nfunc parseRetryAfterHeader(v string) (time.Duration, bool) {\n\tif isStringEmpty(v) {\n\t\treturn 0, false\n\t}\n\n\t// Retry-After: 120\n\tif delay, err := strconv.ParseInt(v, 10, 64); err == nil {\n\t\tif delay < 0 { // a negative delay doesn't make sense\n\t\t\treturn 0, false\n\t\t}\n\t\treturn time.Second * time.Duration(delay), true\n\t}\n\n\t// Retry-After: Fri, 31 Dec 1999 23:59:59 GMT\n\tretryTime, err := time.Parse(time.RFC1123, v)\n\tif err != nil {\n\t\treturn 0, false\n\t}\n\tif until := retryTime.Sub(timeNow()); until > 0 {\n\t\treturn until, true\n\t}\n\n\t// date is in the past\n\treturn 0, true\n}\n"
  },
  {
    "path": "retry_test.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\n// Check to make sure the functions added to add conditionals work\nfunc TestRetryConditionalGet(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\tattemptCount := 1\n\texternalCounter := 0\n\n\t// This check should pass on first run, and let the response through\n\tcheck := RetryConditionFunc(func(*Response, error) bool {\n\t\texternalCounter++\n\t\treturn attemptCount != externalCounter\n\t})\n\n\tclient := dcnl()\n\tresp, err := client.R().\n\t\tAddRetryConditions(check).\n\t\tSetRetryCount(2).\n\t\tSetQueryParam(\"request_no\", strconv.FormatInt(time.Now().Unix(), 10)).\n\t\tGet(ts.URL + \"/\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"200 OK\", resp.Status())\n\tassertEqual(t, \"TestGet: text response\", resp.String())\n\tassertEqual(t, externalCounter, attemptCount)\n\n\tlogResponse(t, resp)\n}\n\nfunc TestRequestConditionalGet(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\texternalCounter := 0\n\t// This check should pass on first run, and let the response through\n\tcheck := RetryConditionFunc(func(r *Response, _ error) bool {\n\t\texternalCounter++\n\t\treturn false\n\t})\n\n\t// Clear the default client.\n\tc, lb := dcldb()\n\n\tresp, err := c.R().\n\t\tSetDebug(true).\n\t\tAddRetryConditions(check).\n\t\tSetRetryCount(1).\n\t\tSetRetryWaitTime(50*time.Millisecond).\n\t\tSetRetryMaxWaitTime(1*time.Second).\n\t\tSetQueryParam(\"request_no\", strconv.FormatInt(time.Now().Unix(), 10)).\n\t\tGet(ts.URL + \"/\")\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"200 OK\", resp.Status())\n\tassertEqual(t, \"TestGet: text response\", resp.String())\n\tassertEqual(t, 1, resp.Request.Attempt)\n\tassertEqual(t, 1, externalCounter)\n\tassertTrue(t, strings.Contains(lb.String(), \"CORRELATION ID:\"), \"expected debug log with correlation ID\")\n\n\tlogResponse(t, resp)\n}\n\nfunc TestClientRetryGetWithTimeout(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl().\n\t\tSetTimeout(50 * time.Millisecond).\n\t\tSetRetryCount(3)\n\n\tresp, err := c.R().Get(ts.URL + \"/set-retrycount-test\")\n\tassertEqual(t, \"\", resp.Status())\n\tassertEqual(t, \"\", resp.Proto())\n\tassertEqual(t, 0, resp.StatusCode())\n\tassertEqual(t, 0, len(resp.Cookies()))\n\tassertEqual(t, 0, len(resp.Header()))\n\tassertErrorIs(t, context.DeadlineExceeded, err, \"expected context deadline exceeded error\")\n}\n\nfunc TestClientRetryWithMinAndMaxWaitTime(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tretryCount := 5\n\tretryIntervals := make([]uint64, retryCount+1)\n\n\t// Set retry wait times that do not intersect with default ones\n\tretryWaitTime := 10 * time.Millisecond\n\tretryMaxWaitTime := 100 * time.Millisecond\n\n\tc, lb := dcldb()\n\n\tc.SetRetryCount(retryCount).\n\t\tSetRetryWaitTime(retryWaitTime).\n\t\tSetRetryMaxWaitTime(retryMaxWaitTime).\n\t\tAddRetryConditions(\n\t\t\tfunc(r *Response, _ error) bool {\n\t\t\t\tretryIntervals[r.Request.Attempt-1] = parseTimeSleptFromResponse(r.String())\n\t\t\t\treturn true\n\t\t\t},\n\t\t)\n\tres, _ := c.R().SetDebug(true).Get(ts.URL + \"/set-retrywaittime-test\")\n\n\tretryIntervals[res.Request.Attempt-1] = parseTimeSleptFromResponse(res.String())\n\n\t// retryCount+1 == attempts were made\n\tassertEqual(t, retryCount+1, res.Request.Attempt)\n\n\tassertTrue(t, strings.Contains(lb.String(), \"CORRELATION ID:\"), \"expected debug log with correlation ID\")\n\n\t// Initial attempt has 0 time slept since last request\n\tassertEqual(t, retryIntervals[0], uint64(0))\n\n\tfor i := 1; i < len(retryIntervals); i++ {\n\t\tslept := time.Duration(retryIntervals[i])\n\t\t// Ensure that client has slept some duration between\n\t\t// waitTime and maxWaitTime for consequent requests\n\t\tif slept < retryWaitTime-5*time.Millisecond {\n\t\t\tt.Logf(\"Client has slept %f seconds which is s < min (%f) before retry %d\", slept.Seconds(), retryWaitTime.Seconds(), i)\n\t\t}\n\t\tif slept > retryMaxWaitTime+5*time.Millisecond {\n\t\t\tt.Logf(\"Client has slept %f seconds which is s > max (%f) before retry %d\", slept.Seconds(), retryMaxWaitTime.Seconds(), i)\n\t\t}\n\t}\n}\n\nfunc TestClientRetryWaitMaxInfinite(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tretryCount := 5\n\tretryIntervals := make([]uint64, retryCount+1)\n\n\t// Set retry wait times that do not intersect with default ones\n\tretryWaitTime := time.Duration(10) * time.Millisecond\n\tretryMaxWaitTime := time.Duration(-1.0) // negative value\n\n\tc := dcnl().\n\t\tSetRetryCount(retryCount).\n\t\tSetRetryWaitTime(retryWaitTime).\n\t\tSetRetryMaxWaitTime(retryMaxWaitTime).\n\t\tAddRetryConditions(\n\t\t\tfunc(r *Response, _ error) bool {\n\t\t\t\tretryIntervals[r.Request.Attempt-1] = parseTimeSleptFromResponse(r.String())\n\t\t\t\treturn true\n\t\t\t},\n\t\t)\n\tres, _ := c.R().Get(ts.URL + \"/set-retrywaittime-test\")\n\n\tretryIntervals[res.Request.Attempt-1] = parseTimeSleptFromResponse(res.String())\n\n\t// retryCount+1 == attempts were made\n\tassertEqual(t, retryCount+1, res.Request.Attempt)\n\n\t// Initial attempt has 0 time slept since last request\n\tassertEqual(t, retryIntervals[0], uint64(0))\n\n\tfor i := 1; i < len(retryIntervals); i++ {\n\t\tslept := time.Duration(retryIntervals[i])\n\t\t// Ensure that client has slept some duration between\n\t\t// waitTime and maxWaitTime for consequent requests\n\t\tif slept < retryWaitTime-5*time.Millisecond {\n\t\t\tt.Logf(\"Client has slept %f seconds which is s < min (%f) before retry %d\", slept.Seconds(), retryWaitTime.Seconds(), i)\n\t\t}\n\t}\n}\n\nfunc TestClientRetryWaitMaxMinimum(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tconst retryMaxWaitTime = time.Nanosecond // minimal duration value\n\n\tc := dcnl().\n\t\tSetRetryCount(1).\n\t\tSetRetryMaxWaitTime(retryMaxWaitTime).\n\t\tAddRetryConditions(func(*Response, error) bool { return true })\n\t_, err := c.R().Get(ts.URL + \"/set-retrywaittime-test\")\n\tassertError(t, err)\n}\n\nfunc TestClientRetryDelayStrategyFuncError(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tattempt := 0\n\tretryCount := 5\n\tretryIntervals := make([]uint64, retryCount+1)\n\n\t// Set retry wait times that do not intersect with default ones\n\tretryWaitTime := 50 * time.Millisecond\n\tretryMaxWaitTime := 150 * time.Millisecond\n\n\tretryDelayStrategyFunc := func(res *Response, err error) (time.Duration, error) {\n\t\treturn 0, errors.New(\"quota exceeded\")\n\t}\n\n\tc := dcnl().\n\t\tSetRetryCount(retryCount).\n\t\tSetRetryWaitTime(retryWaitTime).\n\t\tSetRetryMaxWaitTime(retryMaxWaitTime).\n\t\tSetRetryDelayStrategy(retryDelayStrategyFunc).\n\t\tAddRetryConditions(\n\t\t\tfunc(r *Response, _ error) bool {\n\t\t\t\tretryIntervals[attempt] = parseTimeSleptFromResponse(r.String())\n\t\t\t\tattempt++\n\t\t\t\treturn true\n\t\t\t},\n\t\t)\n\n\t_, err := c.R().Get(ts.URL + \"/set-retrywaittime-test\")\n\n\t// 1 attempts were made\n\tassertEqual(t, 1, attempt)\n\n\t// non-nil error was returned\n\tassertNotNil(t, err)\n}\n\nfunc TestClientRetryDelayStrategyFunc(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tretryCount := 10\n\tretryIntervals := make([]uint64, retryCount+1)\n\n\t// Set retry wait times to constant delay\n\tretryWaitTime := 50 * time.Millisecond\n\tretryMaxWaitTime := 50 * time.Millisecond\n\n\tc := dcnl().\n\t\tSetRetryCount(retryCount).\n\t\tSetRetryWaitTime(retryWaitTime).\n\t\tSetRetryMaxWaitTime(retryMaxWaitTime).\n\t\tSetRetryDelayStrategy(RetryConstantDelayStrategy(50 * time.Microsecond)).\n\t\tAddRetryConditions(\n\t\t\tfunc(r *Response, _ error) bool {\n\t\t\t\tretryIntervals[r.Request.Attempt-1] = parseTimeSleptFromResponse(r.String())\n\t\t\t\treturn true\n\t\t\t},\n\t\t)\n\tres, _ := c.R().Get(ts.URL + \"/set-retrywaittime-test\")\n\n\tretryIntervals[res.Request.Attempt-1] = parseTimeSleptFromResponse(res.String())\n\n\t// retryCount+1 == attempts were made\n\tassertEqual(t, retryCount+1, res.Request.Attempt)\n\n\t// Initial attempt has 0 time slept since last request\n\tassertEqual(t, retryIntervals[0], uint64(0))\n\n\tfor i := 1; i < len(retryIntervals); i++ {\n\t\tslept := time.Duration(retryIntervals[i])\n\t\t// Ensure that client has slept some duration between\n\t\t// waitTime and maxWaitTime for consequent requests\n\t\tif slept < retryWaitTime-5*time.Millisecond {\n\t\t\tt.Logf(\"Client has slept %f seconds which is s < min (%f) before retry %d\", slept.Seconds(), retryWaitTime.Seconds(), i)\n\t\t}\n\t\tif retryMaxWaitTime+5*time.Millisecond < slept {\n\t\t\tt.Logf(\"Client has slept %f seconds which is max < s (%f) before retry %d\", slept.Seconds(), retryMaxWaitTime.Seconds(), i)\n\t\t}\n\t}\n}\n\nfunc TestRequestRetryDelayStrategyFunc(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tretryCount := 10\n\tretryIntervals := make([]uint64, retryCount+1)\n\n\t// Set retry wait times to constant delay\n\tretryWaitTime := 50 * time.Millisecond\n\tretryMaxWaitTime := 50 * time.Millisecond\n\n\tc := dcnl()\n\n\tres, _ := c.R().\n\t\tSetRetryCount(retryCount).\n\t\tSetRetryWaitTime(retryWaitTime).\n\t\tSetRetryMaxWaitTime(retryMaxWaitTime).\n\t\tSetRetryDelayStrategy(RetryConstantDelayStrategy(50 * time.Microsecond)).\n\t\tAddRetryConditions(\n\t\t\tfunc(r *Response, _ error) bool {\n\t\t\t\tretryIntervals[r.Request.Attempt-1] = parseTimeSleptFromResponse(r.String())\n\t\t\t\treturn true\n\t\t\t},\n\t\t).\n\t\tGet(ts.URL + \"/set-retrywaittime-test\")\n\n\tretryIntervals[res.Request.Attempt-1] = parseTimeSleptFromResponse(res.String())\n\n\t// retryCount+1 == attempts were made\n\tassertEqual(t, retryCount+1, res.Request.Attempt)\n\n\t// Initial attempt has 0 time slept since last request\n\tassertEqual(t, retryIntervals[0], uint64(0))\n\n\tfor i := 1; i < len(retryIntervals); i++ {\n\t\tslept := time.Duration(retryIntervals[i])\n\t\t// Ensure that client has slept some duration between\n\t\t// waitTime and maxWaitTime for consequent requests\n\t\tif slept < retryWaitTime-5*time.Millisecond {\n\t\t\tt.Logf(\"Client has slept %f seconds which is s < min (%f) before retry %d\", slept.Seconds(), retryWaitTime.Seconds(), i)\n\t\t}\n\t\tif retryMaxWaitTime+5*time.Millisecond < slept {\n\t\t\tt.Logf(\"Client has slept %f seconds which is max < s (%f) before retry %d\", slept.Seconds(), retryMaxWaitTime.Seconds(), i)\n\t\t}\n\t}\n}\n\nfunc TestClientRetryDelayStrategyWaitTooShort(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tretryCount := 5\n\tretryIntervals := make([]uint64, retryCount+1)\n\n\t// Set retry wait times that do not intersect with default ones\n\tretryWaitTime := 50 * time.Millisecond\n\tretryMaxWaitTime := 150 * time.Millisecond\n\n\tc := dcnl().\n\t\tSetRetryCount(retryCount).\n\t\tSetRetryWaitTime(retryWaitTime).\n\t\tSetRetryMaxWaitTime(retryMaxWaitTime).\n\t\tSetRetryDelayStrategy(RetryConstantDelayStrategy(10 * time.Microsecond)).\n\t\tAddRetryConditions(\n\t\t\tfunc(r *Response, _ error) bool {\n\t\t\t\tretryIntervals[r.Request.Attempt-1] = parseTimeSleptFromResponse(r.String())\n\t\t\t\treturn true\n\t\t\t},\n\t\t)\n\tres, _ := c.R().Get(ts.URL + \"/set-retrywaittime-test\")\n\n\tretryIntervals[res.Request.Attempt-1] = parseTimeSleptFromResponse(res.String())\n\n\t// retryCount+1 == attempts were made\n\tassertEqual(t, retryCount+1, res.Request.Attempt)\n\n\t// Initial attempt has 0 time slept since last request\n\tassertEqual(t, retryIntervals[0], uint64(0))\n\n\tfor i := 1; i < len(retryIntervals); i++ {\n\t\tslept := time.Duration(retryIntervals[i])\n\t\t// Ensure that client has slept some duration between\n\t\t// waitTime and maxWaitTime for consequent requests\n\t\tif slept < retryWaitTime-5*time.Millisecond {\n\t\t\tt.Logf(\"Client has slept %f seconds which is s < min (%f) before retry %d\", slept.Seconds(), retryWaitTime.Seconds(), i)\n\t\t}\n\t\tif retryWaitTime+5*time.Millisecond < slept {\n\t\t\tt.Logf(\"Client has slept %f seconds which is min < s (%f) before retry %d\", slept.Seconds(), retryWaitTime.Seconds(), i)\n\t\t}\n\t}\n}\n\nfunc TestClientRetryDelayStrategyWaitTooLong(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tretryCount := 5\n\tretryIntervals := make([]uint64, retryCount+1)\n\n\t// Set retry wait times that do not intersect with default ones\n\tretryWaitTime := 10 * time.Millisecond\n\tretryMaxWaitTime := 50 * time.Millisecond\n\n\tc := dcnl().\n\t\tSetRetryCount(retryCount).\n\t\tSetRetryWaitTime(retryWaitTime).\n\t\tSetRetryMaxWaitTime(retryMaxWaitTime).\n\t\tSetRetryDelayStrategy(RetryConstantDelayStrategy(1 * time.Second)).\n\t\tAddRetryConditions(\n\t\t\tfunc(r *Response, _ error) bool {\n\t\t\t\tretryIntervals[r.Request.Attempt-1] = parseTimeSleptFromResponse(r.String())\n\t\t\t\treturn true\n\t\t\t},\n\t\t)\n\tres, _ := c.R().Get(ts.URL + \"/set-retrywaittime-test\")\n\n\tretryIntervals[res.Request.Attempt-1] = parseTimeSleptFromResponse(res.String())\n\n\t// retryCount+1 == attempt attempts were made\n\tassertEqual(t, retryCount+1, res.Request.Attempt)\n\n\t// Initial attempt has 0 time slept since last request\n\tassertEqual(t, retryIntervals[0], uint64(0))\n\n\tfor i := 1; i < len(retryIntervals); i++ {\n\t\tslept := time.Duration(retryIntervals[i])\n\t\t// Ensure that client has slept some duration between\n\t\t// waitTime and maxWaitTime for consequent requests\n\t\tif slept < retryMaxWaitTime-5*time.Millisecond {\n\t\t\tt.Logf(\"Client has slept %f seconds which is s < max (%f) before retry %d\", slept.Seconds(), retryMaxWaitTime.Seconds(), i)\n\t\t}\n\t\tif retryMaxWaitTime+5*time.Millisecond < slept {\n\t\t\tt.Logf(\"Client has slept %f seconds which is max < s (%f) before retry %d\", slept.Seconds(), retryMaxWaitTime.Seconds(), i)\n\t\t}\n\t}\n}\n\nfunc TestClientRetryCancel(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tretryCount := 5\n\tretryIntervals := make([]uint64, retryCount+1)\n\n\t// Set retry wait times that do not intersect with default ones\n\tretryWaitTime := 100 * time.Millisecond\n\tretryMaxWaitTime := 200 * time.Millisecond\n\n\tc := dcnl().\n\t\tSetRetryCount(retryCount).\n\t\tSetRetryWaitTime(retryWaitTime).\n\t\tSetRetryMaxWaitTime(retryMaxWaitTime).\n\t\tAddRetryConditions(\n\t\t\tfunc(r *Response, _ error) bool {\n\t\t\t\tretryIntervals[r.Request.Attempt-1] = parseTimeSleptFromResponse(r.String())\n\t\t\t\treturn true\n\t\t\t},\n\t\t)\n\n\ttimeout := 100 * time.Millisecond\n\n\tctx, cancelFunc := context.WithTimeout(context.Background(), timeout)\n\treq := c.R().SetContext(ctx)\n\t_, _ = req.Get(ts.URL + \"/set-retrywaittime-test\")\n\n\t// 1 attempts were made\n\tassertEqual(t, 1, req.Attempt)\n\n\t// Initial attempt has 0 time slept since last request\n\tassertEqual(t, retryIntervals[0], uint64(0))\n\n\t// Second attempt should be interrupted on context timeout\n\tif time.Duration(retryIntervals[1]) > timeout {\n\t\tt.Errorf(\"Client didn't awake on context cancel\")\n\t}\n\tcancelFunc()\n}\n\nfunc TestClientRetryPost(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tusersmap := map[string]any{\n\t\t\"user1\": map[string]any{\"FirstName\": \"firstname1\", \"LastName\": \"lastname1\", \"ZipCode\": \"10001\"},\n\t}\n\n\tvar users []map[string]any\n\tusers = append(users, usersmap)\n\n\tc := dcnl()\n\tc.SetRetryCount(3)\n\tc.AddRetryConditions(RetryConditionFunc(func(r *Response, _ error) bool {\n\t\treturn r.StatusCode() >= http.StatusInternalServerError\n\t}))\n\n\tresp, _ := c.R().\n\t\tSetBody(&users).\n\t\tPost(ts.URL + \"/usersmap?status=500\")\n\n\tif resp != nil {\n\t\tif resp.StatusCode() == http.StatusInternalServerError {\n\t\t\tt.Logf(\"Got response body: %s\", resp.String())\n\t\t\tvar usersResponse []map[string]any\n\t\t\terr := json.Unmarshal(resp.Bytes(), &usersResponse)\n\t\t\tassertError(t, err)\n\n\t\t\tif !reflect.DeepEqual(users, usersResponse) {\n\t\t\t\tt.Errorf(\"Expected request body to be echoed back as response body. Instead got: %s\", resp.String())\n\t\t\t}\n\n\t\t\treturn\n\t\t}\n\t\tt.Errorf(\"Got unexpected response code: %d with body: %s\", resp.StatusCode(), resp.String())\n\t}\n}\n\nfunc TestClientRetryErrorRecover(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl().\n\t\tSetRetryCount(2).\n\t\tSetResultError(AuthError{}).\n\t\tAddRetryConditions(\n\t\t\tfunc(r *Response, _ error) bool {\n\t\t\t\terr, ok := r.ResultError().(*AuthError)\n\t\t\t\tretry := ok && r.StatusCode() == 429 && err.Message == \"too many\"\n\t\t\t\treturn retry\n\t\t\t},\n\t\t)\n\n\tresp, err := c.R().\n\t\tSetHeader(hdrContentTypeKey, \"application/json; charset=utf-8\").\n\t\tSetJSONEscapeHTML(false).\n\t\tSetResult(AuthSuccess{}).\n\t\tGet(ts.URL + \"/set-retry-error-recover\")\n\n\tassertError(t, err)\n\n\tauthSuccess := resp.Result().(*AuthSuccess)\n\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"hello\", authSuccess.Message)\n\n\tassertNil(t, resp.ResultError())\n}\n\nfunc TestClientRetryCountWithTimeout(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tattempt := 0\n\n\tc := dcnl().\n\t\tSetTimeout(50 * time.Millisecond).\n\t\tSetRetryCount(1).\n\t\tAddRetryConditions(\n\t\t\tfunc(r *Response, _ error) bool {\n\t\t\t\tattempt++\n\t\t\t\treturn true\n\t\t\t},\n\t\t)\n\n\tresp, err := c.R().Get(ts.URL + \"/set-retrycount-test\")\n\tassertEqual(t, \"\", resp.Status())\n\tassertEqual(t, \"\", resp.Proto())\n\tassertEqual(t, 0, resp.StatusCode())\n\tassertEqual(t, 0, len(resp.Cookies()))\n\tassertEqual(t, 0, len(resp.Header()))\n\tassertEqual(t, 2, resp.Request.Attempt)\n\tassertErrorIs(t, context.DeadlineExceeded, err, \"expected context deadline exceeded error\")\n}\n\nfunc TestClientRetryTooManyRequestsAndRecover(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl().\n\t\tSetTimeout(time.Second * 1).\n\t\tSetRetryCount(2)\n\n\tresp, err := c.R().\n\t\tSetHeader(hdrContentTypeKey, \"application/json; charset=utf-8\").\n\t\tSetJSONEscapeHTML(false).\n\t\tSetResult(AuthSuccess{}).\n\t\tSetTimeout(10 * time.Millisecond).\n\t\tGet(ts.URL + \"/set-retry-error-recover\")\n\n\tassertError(t, err)\n\n\tauthSuccess := resp.Result().(*AuthSuccess)\n\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"hello\", authSuccess.Message)\n\n\tassertNil(t, resp.ResultError())\n}\n\nfunc TestClientRetryHookWithTimeout(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\thookCalledCount := 0\n\n\tretryHook := func(r *Response, _ error) {\n\t\thookCalledCount++\n\t}\n\n\tretryCount := 3\n\n\tc := dcnl().\n\t\tSetRetryCount(retryCount).\n\t\tSetTimeout(50 * time.Millisecond).\n\t\tAddRetryHooks(retryHook)\n\n\t// Since reflect.DeepEqual can not compare two functions\n\t// just compare pointers of the two hooks\n\toriginHookPointer := reflect.ValueOf(retryHook).Pointer()\n\tgetterHookPointer := reflect.ValueOf(c.RetryHooks()[0]).Pointer()\n\n\tassertEqual(t, originHookPointer, getterHookPointer)\n\n\tresp, err := c.R().Get(ts.URL + \"/set-retrycount-test\")\n\tassertEqual(t, \"\", resp.Status())\n\tassertEqual(t, \"\", resp.Proto())\n\tassertEqual(t, 0, resp.StatusCode())\n\tassertEqual(t, 0, len(resp.Cookies()))\n\tassertEqual(t, 0, len(resp.Header()))\n\n\tassertEqual(t, retryCount+1, resp.Request.Attempt)\n\tassertEqual(t, 3, hookCalledCount)\n\tassertErrorIs(t, context.DeadlineExceeded, err, \"expected context deadline exceeded error\")\n}\n\nvar errSeekFailure = fmt.Errorf(\"failing seek test\")\n\ntype failingSeeker struct {\n\treader *bytes.Reader\n}\n\nfunc (f failingSeeker) Read(b []byte) (n int, err error) {\n\treturn f.reader.Read(b)\n}\n\nfunc (f failingSeeker) Seek(offset int64, whence int) (int64, error) {\n\tif offset == 0 && whence == io.SeekStart {\n\t\treturn 0, errSeekFailure\n\t}\n\n\treturn f.reader.Seek(offset, whence)\n}\n\nfunc TestResetMultipartReaderSeekStartError(t *testing.T) {\n\tts := createFileUploadServer(t)\n\tdefer ts.Close()\n\n\ttestSeeker := &failingSeeker{\n\t\tbytes.NewReader([]byte(\"test\")),\n\t}\n\n\tc := dcnl().\n\t\tSetRetryCount(2).\n\t\tSetTimeout(200 * time.Millisecond)\n\n\tresp, err := c.R().\n\t\tSetFileReader(\"name\", \"filename\", testSeeker).\n\t\tPut(ts.URL + \"/set-reset-multipart-readers-test\")\n\n\tassertEqual(t, 500, resp.StatusCode())\n\tassertEqual(t, err.Error(), errSeekFailure.Error())\n}\n\nfunc TestClientResetMultipartReaders(t *testing.T) {\n\tts := createFileUploadServer(t)\n\tdefer ts.Close()\n\n\tstr := \"test\"\n\tbuf := []byte(str)\n\n\tbufReader := bytes.NewReader(buf)\n\tbufCpy := make([]byte, len(buf))\n\n\tc := dcnl().\n\t\tSetRetryCount(2).\n\t\tSetTimeout(time.Second * 3).\n\t\tAddRetryHooks(\n\t\t\tfunc(response *Response, _ error) {\n\t\t\t\tread, err := bufReader.Read(bufCpy)\n\n\t\t\t\tassertNil(t, err)\n\t\t\t\tassertEqual(t, len(buf), read)\n\t\t\t\tassertEqual(t, str, string(bufCpy))\n\t\t\t},\n\t\t)\n\n\tresp, err := c.R().\n\t\tSetFileReader(\"name\", \"filename\", bufReader).\n\t\tPut(ts.URL + \"/set-reset-multipart-readers-test\")\n\n\tassertEqual(t, 500, resp.StatusCode())\n\tassertNil(t, err)\n}\n\nfunc TestRequestResetMultipartReaders(t *testing.T) {\n\tts := createFileUploadServer(t)\n\tdefer ts.Close()\n\n\tstr := \"test\"\n\tbuf := []byte(str)\n\n\tbufReader := bytes.NewReader(buf)\n\tbufCpy := make([]byte, len(buf))\n\n\tc := dcnl().\n\t\tSetTimeout(time.Second * 3).\n\t\tAddRetryHooks(\n\t\t\tfunc(response *Response, _ error) {\n\t\t\t\tread, err := bufReader.Read(bufCpy)\n\n\t\t\t\tassertNil(t, err)\n\t\t\t\tassertEqual(t, len(buf), read)\n\t\t\t\tassertEqual(t, str, string(bufCpy))\n\t\t\t},\n\t\t)\n\n\treq := c.R().\n\t\tSetRetryCount(2).\n\t\tSetFileReader(\"name\", \"filename\", bufReader)\n\tresp, err := req.Put(ts.URL + \"/set-reset-multipart-readers-test\")\n\n\tassertEqual(t, 500, resp.StatusCode())\n\tassertNil(t, err)\n}\n\nfunc TestParseRetryAfterHeader(t *testing.T) {\n\ttestStaticTime(t)\n\n\ttests := []struct {\n\t\tname   string\n\t\theader string\n\t\tsleep  time.Duration\n\t\tok     bool\n\t}{\n\t\t{\"seconds\", \"2\", time.Second * 2, true},\n\t\t{\"date\", \"Fri, 31 Dec 1999 23:59:59 GMT\", time.Second * 2, true},\n\t\t{\"past-date\", \"Fri, 31 Dec 1999 23:59:00 GMT\", 0, true},\n\t\t{\"two-headers\", \"3\", time.Second * 3, true},\n\t\t{\"empty\", \"\", 0, false},\n\t\t{\"negative\", \"-2\", 0, false},\n\t\t{\"bad-date\", \"Fri, 32 Dec 1999 23:59:59 GMT\", 0, false},\n\t\t{\"bad-date-format\", \"badbadbad\", 0, false},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tsleep, ok := parseRetryAfterHeader(test.header)\n\t\t\tif ok != test.ok {\n\t\t\t\tt.Errorf(\"expected ok=%t, got ok=%t\", test.ok, ok)\n\t\t\t}\n\t\t\tif sleep != test.sleep {\n\t\t\t\tt.Errorf(\"expected sleep=%v, got sleep=%v\", test.sleep, sleep)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRequestRetryTooManyRequestsHeaderRetryAfter(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tc := dcnl()\n\n\tresp, err := c.R().\n\t\tSetRetryCount(2).\n\t\tSetHeader(hdrContentTypeKey, \"application/json; charset=utf-8\").\n\t\tSetResult(AuthSuccess{}).\n\t\tGet(ts.URL + \"/retry-after-delay\")\n\n\tassertError(t, err)\n\n\tauthSuccess := resp.Result().(*AuthSuccess)\n\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, \"hello\", authSuccess.Message)\n\n\tassertNil(t, resp.ResultError())\n}\n\nfunc TestRetryDefaultConditions(t *testing.T) {\n\tt.Run(\"redirect error\", func(t *testing.T) {\n\t\tts := createRedirectServer(t)\n\t\tdefer ts.Close()\n\n\t\t_, err := dcnl().R().\n\t\t\tSetRetryCount(2).\n\t\t\tGet(ts.URL + \"/redirect-1\")\n\n\t\tassertNotNil(t, err)\n\t\tassertTrue(t, (err.Error() == `Get \"/redirect-11\": stopped after 10 redirects`))\n\t})\n\n\tt.Run(\"invalid scheme error\", func(t *testing.T) {\n\t\tts := createGetServer(t)\n\t\tdefer ts.Close()\n\n\t\tc := dcnl().SetBaseURL(strings.Replace(ts.URL, \"http\", \"ftp\", 1))\n\n\t\t_, err := c.R().\n\t\t\tSetRetryCount(2).\n\t\t\tGet(\"/\")\n\t\tassertNotNil(t, err)\n\t\tassertTrue(t, strings.Contains(err.Error(), `unsupported protocol scheme \"ftp\"`),\n\t\t\t\"expected unsupported protocol scheme error\")\n\t})\n\n\tt.Run(\"invalid header error\", func(t *testing.T) {\n\t\tts := createGetServer(t)\n\t\tdefer ts.Close()\n\n\t\t_, err := dcnl().R().\n\t\t\tSetRetryCount(2).\n\t\t\tSetHeader(\"Header-Name\", \"bad header value \\033\").\n\t\t\tGet(ts.URL + \"/\")\n\t\tassertNotNil(t, err)\n\t\tassertTrue(t, strings.Contains(err.Error(), \"net/http: invalid header field value\"),\n\t\t\t\"expected invalid header field value error\")\n\n\t\t_, err = dcnl().R().\n\t\t\tSetRetryCount(2).\n\t\t\tSetHeader(\"Header-Name\\033\", \"bad header value\").\n\t\t\tGet(ts.URL + \"/\")\n\t\tassertNotNil(t, err)\n\t\tassertTrue(t, strings.Contains(err.Error(), \"net/http: invalid header field name\"),\n\t\t\t\"expected invalid header field name error\")\n\t})\n\n\tt.Run(\"nil values\", func(t *testing.T) {\n\t\tresult := applyRetryDefaultConditions(nil, nil)\n\t\tassertFalse(t, result)\n\t})\n}\n\nfunc TestRequestRetryPutIoReadSeekerForBuffer(t *testing.T) {\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tb, err := io.ReadAll(r.Body)\n\t\tassertError(t, err)\n\t\tassertEqual(t, 12, len(b))\n\t\tassertEqual(t, \"body content\", string(b))\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t}))\n\n\tc := dcnl().\n\t\tAddRetryConditions(\n\t\t\tfunc(r *Response, err error) bool {\n\t\t\t\treturn err != nil || r.StatusCode() > 499\n\t\t\t},\n\t\t).\n\t\tSetRetryCount(3).\n\t\tSetRetryAllowNonIdempotent(true)\n\n\tassertTrue(t, c.IsRetryAllowNonIdempotent(), \"expected AllowNonIdempotentRetry to be true\")\n\n\tbuf := bytes.NewBuffer([]byte(\"body content\"))\n\tresp, err := c.R().\n\t\tSetBody(buf).\n\t\tSetMethodGetAllowPayload(false).\n\t\tPut(srv.URL)\n\n\tassertNil(t, err)\n\tassertEqual(t, 4, resp.Request.Attempt)\n\tassertEqual(t, http.StatusInternalServerError, resp.StatusCode())\n\tassertEqual(t, \"\", resp.String())\n}\n\nfunc TestRequestRetryPostIoReadSeeker(t *testing.T) {\n\tsrv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tb, err := io.ReadAll(r.Body)\n\t\tassertError(t, err)\n\t\tassertEqual(t, 12, len(b))\n\t\tassertEqual(t, \"body content\", string(b))\n\t\tw.WriteHeader(http.StatusInternalServerError)\n\t}))\n\n\tc := dcnl().\n\t\tAddRetryConditions(\n\t\t\tfunc(r *Response, err error) bool {\n\t\t\t\treturn err != nil || r.StatusCode() > 499\n\t\t\t},\n\t\t).\n\t\tSetRetryCount(3).\n\t\tSetRetryAllowNonIdempotent(false)\n\n\tassertFalse(t, c.IsRetryAllowNonIdempotent())\n\n\tresp, err := c.R().\n\t\tSetBody([]byte(\"body content\")).\n\t\tSetRetryAllowNonIdempotent(true).\n\t\tPost(srv.URL)\n\n\tassertNil(t, err)\n\tassertEqual(t, 4, resp.Request.Attempt)\n\tassertEqual(t, http.StatusInternalServerError, resp.StatusCode())\n\tassertEqual(t, \"\", resp.String())\n}\n\nfunc TestRequestRetryHooks(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\thookFunc := func(msg string) RetryHookFunc {\n\t\treturn func(res *Response, err error) {\n\t\t\tres.Request.log.Debugf(msg)\n\t\t}\n\t}\n\n\tc, lb := dcldb()\n\tc.AddRetryConditions(func(r *Response, err error) bool {\n\t\treturn true\n\t}).\n\t\tAddRetryHooks(\n\t\t\thookFunc(\"This is client hook1\"),\n\t\t\thookFunc(\"This is client hook2\"),\n\t\t)\n\n\t_, _ = c.R().\n\t\tSetRetryCount(1).\n\t\tAddRetryHooks(hookFunc(\"This is request hook1\")).\n\t\tSetRetryHooks(hookFunc(\"This is request overwrite hook1\")).\n\t\tGet(\"/set-retrycount-test\")\n\n\tdebugLog := lb.String()\n\tassertFalse(t, strings.Contains(debugLog, \"This is client hook1\"))\n\tassertFalse(t, strings.Contains(debugLog, \"This is client hook2\"))\n\tassertFalse(t, strings.Contains(debugLog, \"This is request hook1\"))\n\tassertTrue(t, strings.Contains(debugLog, \"This is request overwrite hook1\"),\n\t\t\"expected to find request overwrite hook log\")\n}\n\nfunc TestRequestSetRetryConditions(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tcondFunc := func(fn func() bool) RetryConditionFunc {\n\t\treturn func(r *Response, err error) bool {\n\t\t\treturn fn()\n\t\t}\n\t}\n\n\tc := dcnl().\n\t\tAddRetryConditions(\n\t\t\tcondFunc(func() bool { return true }),\n\t\t\tcondFunc(func() bool { return true }),\n\t\t)\n\n\tres, _ := c.R().\n\t\tSetRetryCount(2).\n\t\tSetRetryConditions(condFunc(func() bool { return false })). // disable retry with overwrite condition\n\t\tGet(\"/set-retrycount-test\")\n\n\tassertEqual(t, 1, res.Request.Attempt)\n}\n\nfunc TestRequestRetryQueryParamsGH938(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\texpectedQueryParams := \"foo=baz&foo=bar&foo=bar\"\n\n\tc := dcnl().\n\t\tSetBaseURL(ts.URL).\n\t\tSetRetryCount(5).\n\t\tSetRetryWaitTime(10 * time.Millisecond).\n\t\tSetRetryMaxWaitTime(20 * time.Millisecond).\n\t\tAddRetryConditions(\n\t\t\tfunc(r *Response, _ error) bool {\n\t\t\t\tassertEqual(t, expectedQueryParams, r.Request.RawRequest.URL.RawQuery)\n\t\t\t\treturn true // always retry\n\t\t\t},\n\t\t)\n\n\t_, _ = c.R().\n\t\tSetQueryParamsFromValues(map[string][]string{\n\t\t\t\"foo\": {\n\t\t\t\t\"baz\",\n\t\t\t\t\"bar\",\n\t\t\t\t\"bar\",\n\t\t\t},\n\t\t}).\n\t\tGet(\"/set-retrycount-test\")\n}\n\nfunc TestRetryConstantDelayStrategyReturnsGivenDelay(t *testing.T) {\n\td := 250 * time.Millisecond\n\tstrat := RetryConstantDelayStrategy(d)\n\n\tgot, err := strat(nil, nil)\n\tassertNil(t, err)\n\tassertEqual(t, d, got)\n}\n\nfunc TestRetryConstantDelayStrategyZeroAndNegative(t *testing.T) {\n\t// zero duration\n\tstrategyZero := RetryConstantDelayStrategy(0)\n\td, err := strategyZero(nil, nil)\n\tassertNil(t, err)\n\tassertEqual(t, time.Duration(0), d)\n\n\t// negative duration (function should faithfully return what was provided)\n\tneg := -5 * time.Second\n\tstrategyNeg := RetryConstantDelayStrategy(neg)\n\td, err = strategyNeg(nil, nil)\n\tassertNil(t, err)\n\tassertEqual(t, neg, d)\n}\n\nfunc TestRetryConstantDelayUsingMinAndMaxWaitTime(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tretryCount := 10\n\tretryIntervals := make([]uint64, retryCount+1)\n\n\t// Set retry wait times to constant delay\n\tconstantDelay := 20 * time.Millisecond\n\n\tc := dcnl().\n\t\tSetRetryCount(retryCount).\n\t\tSetRetryWaitTime(constantDelay).\n\t\tSetRetryMaxWaitTime(constantDelay).\n\t\tAddRetryConditions(\n\t\t\tfunc(r *Response, _ error) bool {\n\t\t\t\tretryIntervals[r.Request.Attempt-1] = parseTimeSleptFromResponse(r.String())\n\t\t\t\treturn true\n\t\t\t},\n\t\t)\n\tres, _ := c.R().\n\t\tGet(ts.URL + \"/set-retrywaittime-test\")\n\n\tretryIntervals[res.Request.Attempt-1] = parseTimeSleptFromResponse(res.String())\n\n\tassertNil(t, c.RetryDelayStrategy())\n\n\t// retryCount+1 == attempts were made\n\tassertEqual(t, retryCount+1, res.Request.Attempt)\n\n\t// Initial attempt has 0 time slept since last request\n\tassertEqual(t, retryIntervals[0], uint64(0))\n\n\tfor i := 1; i < len(retryIntervals); i++ {\n\t\tslept := time.Duration(retryIntervals[i])\n\t\t// Ensure that client has slept some duration between\n\t\t// waitTime and maxWaitTime for consequent requests\n\t\tif slept < constantDelay-5*time.Millisecond {\n\t\t\tt.Logf(\"Client has slept %f seconds which is s < min (%f) before retry %d\", slept.Seconds(), constantDelay.Seconds(), i)\n\t\t}\n\t\tif constantDelay+5*time.Millisecond < slept {\n\t\t\tt.Logf(\"Client has slept %f seconds which is max < s (%f) before retry %d\", slept.Seconds(), constantDelay.Seconds(), i)\n\t\t}\n\t}\n}\n\nfunc TestRetryConstantDelayUsingStrategy(t *testing.T) {\n\tts := createGetServer(t)\n\tdefer ts.Close()\n\n\tretryCount := 10\n\tretryIntervals := make([]uint64, retryCount+1)\n\n\t// Set retry wait times to constant delay\n\tconstantDelay := 20 * time.Millisecond\n\n\tc := dcnl().\n\t\tSetRetryCount(retryCount).\n\t\tAddRetryConditions(\n\t\t\tfunc(r *Response, _ error) bool {\n\t\t\t\tretryIntervals[r.Request.Attempt-1] = parseTimeSleptFromResponse(r.String())\n\t\t\t\treturn true\n\t\t\t},\n\t\t)\n\tres, _ := c.R().\n\t\tSetRetryDelayStrategy(RetryConstantDelayStrategy(constantDelay)).\n\t\tGet(ts.URL + \"/set-retrywaittime-test\")\n\n\tretryIntervals[res.Request.Attempt-1] = parseTimeSleptFromResponse(res.String())\n\n\tassertNil(t, c.RetryDelayStrategy())\n\n\t// retryCount+1 == attempts were made\n\tassertEqual(t, retryCount+1, res.Request.Attempt)\n\n\t// Initial attempt has 0 time slept since last request\n\tassertEqual(t, retryIntervals[0], uint64(0))\n\n\tfor i := 1; i < len(retryIntervals); i++ {\n\t\tslept := time.Duration(retryIntervals[i])\n\t\t// Ensure that client has slept some duration between\n\t\t// waitTime and maxWaitTime for consequent requests\n\t\tif slept < constantDelay-5*time.Millisecond {\n\t\t\tt.Logf(\"Client has slept %f seconds which is s < min (%f) before retry %d\", slept.Seconds(), constantDelay.Seconds(), i)\n\t\t}\n\t\tif constantDelay+5*time.Millisecond < slept {\n\t\t\tt.Logf(\"Client has slept %f seconds which is max < s (%f) before retry %d\", slept.Seconds(), constantDelay.Seconds(), i)\n\t\t}\n\t}\n}\n\nfunc TestRetryCoverage(t *testing.T) {\n\tt.Run(\"apply retry default min and max value\", func(t *testing.T) {\n\t\tbackoff := newBackoffWithJitter(0, 0)\n\t\tassertEqual(t, defaultWaitTime, backoff.min)\n\t\tassertEqual(t, defaultMaxWaitTime, backoff.max)\n\n\t\tdur1 := backoff.balanceMinMax(0)\n\t\tassertEqual(t, 2*time.Second, dur1)\n\n\t\tdur2 := backoff.balanceMinMax(4 * time.Second)\n\t\tassertEqual(t, 2*time.Second, dur2)\n\t})\n\n\tt.Run(\"mock tls cert error\", func(t *testing.T) {\n\t\tcertError := tls.CertificateVerificationError{}\n\t\tresult1 := applyRetryDefaultConditions(nil, &certError)\n\t\tassertFalse(t, result1, \"expected no retry for tls.CertificateVerificationError\")\n\t})\n}\n\nfunc parseTimeSleptFromResponse(v string) uint64 {\n\ttimeSlept, _ := strconv.ParseUint(v, 10, 64)\n\treturn timeSlept\n}\n\nfunc testStaticTime(t *testing.T) {\n\ttimeNow = func() time.Time {\n\t\tnow, err := time.Parse(time.RFC1123, \"Fri, 31 Dec 1999 23:59:57 GMT\")\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\treturn now\n\t}\n\tt.Cleanup(func() {\n\t\ttimeNow = time.Now\n\t})\n}\n"
  },
  {
    "path": "sse.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\n// Spec: https://html.spec.whatwg.org/multipage/server-sent-events.html\n\nvar (\n\tdefaultSseMaxBufSize = 1 << 15 // 32kb\n\tdefaultEventName     = \"message\"\n\tdefaultHTTPMethod    = MethodGet\n\n\theaderID    = []byte(\"id:\")\n\theaderData  = []byte(\"data:\")\n\theaderEvent = []byte(\"event:\")\n\theaderRetry = []byte(\"retry:\")\n\n\thdrCacheControlKey = http.CanonicalHeaderKey(\"Cache-Control\")\n\thdrConnectionKey   = http.CanonicalHeaderKey(\"Connection\")\n\thdrLastEvevntID    = http.CanonicalHeaderKey(\"Last-Event-ID\")\n)\n\ntype (\n\t// SSEOpenFunc is a callback function type used to receive notification\n\t// when Resty establishes a connection with the server for the\n\t// Server-Sent Events(SSE)\n\tSSEOpenFunc func(url string, respHdr http.Header)\n\n\t// SSEMessageFunc is a callback function type used to receive event details\n\t// from the Server-Sent Events(SSE) stream\n\tSSEMessageFunc func(any)\n\n\t// SSEErrorFunc is a callback function type used to receive notification\n\t// when an error occurs with [SSESource] processing\n\tSSEErrorFunc func(error)\n\n\t// SSERequestFailureFunc is a callback function type used to receive event\n\t// details from the Server-Sent Events(SSE) request failure\n\tSSERequestFailureFunc func(err error, res *http.Response)\n\n\t// SSE struct represents the event details from the Server-Sent Events(SSE) stream\n\tSSE struct {\n\t\tID   string\n\t\tName string\n\t\tData string\n\t}\n\n\t// SSESource struct implements the Server-Sent Events(SSE) [specification] to receive\n\t// stream from the server\n\t//\n\t// [specification]: https://html.spec.whatwg.org/multipage/server-sent-events.html\n\tSSESource struct {\n\t\tlock             *sync.RWMutex\n\t\turl              string\n\t\tmethod           string\n\t\theader           http.Header\n\t\tbodyBytes        []byte\n\t\tlastEventID      string\n\t\tretryCount       int\n\t\tretryWaitTime    time.Duration\n\t\tretryMaxWaitTime time.Duration\n\t\tserverSentRetry  time.Duration\n\t\tmaxBufSize       int\n\t\tonOpen           SSEOpenFunc\n\t\tonError          SSEErrorFunc\n\t\tonRequestFailure SSERequestFailureFunc\n\t\tonEvent          map[string]*callback\n\t\tlog              Logger\n\t\tclosed           bool\n\t\thttpClient       *http.Client\n\t}\n\n\tcallback struct {\n\t\tFunc   SSEMessageFunc\n\t\tResult any\n\t}\n)\n\n// NewSSESource method creates a new instance of [SSESource]\n// with default values for Server-Sent Events(SSE)\n//\n//\tsse := NewSSESource().\n//\t\tSetURL(\"https://sse.dev/test\").\n//\t\tOnMessage(\n//\t\t\tfunc(e any) {\n//\t\t\t\tevent := e.(*resty.SSE)\n//\t\t\t\tfmt.Println(event)\n//\t\t\t},\n//\t\t\tnil, // see method godoc\n//\t\t)\n//\n//\terr := sse.Connect()\n//\tfmt.Println(err)\n//\n// See [SSESource.OnMessage], [SSESource.AddEventListener]\nfunc NewSSESource() *SSESource {\n\tsse := &SSESource{\n\t\tlock:             new(sync.RWMutex),\n\t\theader:           make(http.Header),\n\t\tretryCount:       3,\n\t\tretryWaitTime:    defaultWaitTime,\n\t\tretryMaxWaitTime: defaultMaxWaitTime,\n\t\tmaxBufSize:       defaultSseMaxBufSize,\n\t\tonEvent:          make(map[string]*callback),\n\t\thttpClient: &http.Client{\n\t\t\tJar:       createCookieJar(),\n\t\t\tTransport: createTransport(nil, nil),\n\t\t},\n\t}\n\treturn sse\n}\n\n// SetURL method sets a [SSESource] connection URL in the instance\n//\n//\tsse.SetURL(\"https://sse.dev/test\")\nfunc (sse *SSESource) SetURL(url string) *SSESource {\n\tsse.url = url\n\treturn sse\n}\n\n// SetMethod method sets a [SSESource] connection HTTP method in the instance\n//\n//\tsse.SetMethod(\"POST\"), or sse.SetMethod(resty.MethodPost)\nfunc (sse *SSESource) SetMethod(method string) *SSESource {\n\tsse.method = method\n\treturn sse\n}\n\n// SetHeader method sets a header and its value to the [SSESource] instance.\n// It overwrites the header value if the key already exists. These headers will be sent in\n// the request while establishing a connection to the event source\n//\n//\tsse.SetHeader(\"Authorization\", \"token here\").\n//\t\tSetHeader(\"X-Header\", \"value\")\nfunc (sse *SSESource) SetHeader(header, value string) *SSESource {\n\tsse.lock.Lock()\n\tdefer sse.lock.Unlock()\n\tsse.header.Set(header, value)\n\treturn sse\n}\n\n// SetBody method sets body value to the [SSESource] instance\n//\n// Example:\n// sse.SetBody(bytes.NewReader([]byte(`{\"test\":\"put_data\"}`)))\nfunc (sse *SSESource) SetBody(body io.Reader) *SSESource {\n\tsse.lock.Lock()\n\tdefer sse.lock.Unlock()\n\tif body == nil {\n\t\tsse.bodyBytes = nil\n\t\treturn sse\n\t}\n\n\tsse.bodyBytes = nil\n\tbodyBytes, err := ioReadAll(body)\n\tif err != nil {\n\t\tsse.log.Errorf(\"resty:sse: unable to read body, error: %v\", err)\n\t\treturn sse\n\t}\n\n\tsse.bodyBytes = bodyBytes\n\treturn sse\n}\n\n// TLSClientConfig method returns the [tls.Config] from underlying client transport\n// otherwise returns nil\nfunc (sse *SSESource) TLSClientConfig() *tls.Config {\n\tcfg, err := sse.tlsConfig()\n\tif err != nil {\n\t\tsse.Logger().Errorf(\"%v\", err)\n\t}\n\treturn cfg\n}\n\n// SetTLSClientConfig method sets TLSClientConfig for underlying client Transport.\n//\n// Values supported by https://pkg.go.dev/crypto/tls#Config can be configured.\n//\n//\t// Disable SSL cert verification for local development\n//\tsse.SetTLSClientConfig(&tls.Config{\n//\t\tInsecureSkipVerify: true\n//\t})\n//\n// NOTE: This method overwrites existing [http.Transport.TLSClientConfig]\nfunc (sse *SSESource) SetTLSClientConfig(tlsConfig *tls.Config) *SSESource {\n\tsse.lock.Lock()\n\tdefer sse.lock.Unlock()\n\n\t// TLSClientConfiger interface handling\n\tif tc, ok := sse.httpClient.Transport.(TLSClientConfiger); ok {\n\t\tif err := tc.SetTLSClientConfig(tlsConfig); err != nil {\n\t\t\tsse.log.Errorf(\"%v\", err)\n\t\t}\n\t\treturn sse\n\t}\n\n\t// default standard transport handling\n\tif transport, ok := sse.httpClient.Transport.(*http.Transport); ok {\n\t\ttransport.TLSClientConfig = tlsConfig\n\t}\n\n\treturn sse\n}\n\n// getting TLS client config if not exists then create one\nfunc (sse *SSESource) tlsConfig() (*tls.Config, error) {\n\tsse.lock.Lock()\n\tdefer sse.lock.Unlock()\n\n\tif tc, ok := sse.httpClient.Transport.(TLSClientConfiger); ok {\n\t\treturn tc.TLSClientConfig(), nil\n\t}\n\n\ttransport, ok := sse.httpClient.Transport.(*http.Transport)\n\tif !ok {\n\t\treturn nil, ErrNotHttpTransportType\n\t}\n\n\tif transport.TLSClientConfig == nil {\n\t\ttransport.TLSClientConfig = &tls.Config{}\n\t}\n\treturn transport.TLSClientConfig, nil\n}\n\n// AddHeader method adds a header and its value to the [SSESource] instance.\n// If the header key already exists, it appends. These headers will be sent in\n// the request while establishing a connection to the event source\n//\n//\tsse.AddHeader(\"Authorization\", \"token here\").\n//\t\tAddHeader(\"X-Header\", \"value\")\nfunc (sse *SSESource) AddHeader(header, value string) *SSESource {\n\tsse.lock.Lock()\n\tdefer sse.lock.Unlock()\n\tsse.header.Add(header, value)\n\treturn sse\n}\n\n// SetRetryCount method enables retry attempts on the SSE client while establishing\n// connection with the server\n//\n//\tfirst attempt + retry count = total attempts\n//\n// Default is 3\n//\n//\tsse.SetRetryCount(10)\nfunc (sse *SSESource) SetRetryCount(count int) *SSESource {\n\tsse.lock.Lock()\n\tdefer sse.lock.Unlock()\n\tsse.retryCount = count\n\treturn sse\n}\n\n// SetRetryWaitTime method sets the default wait time for sleep before retrying\n// the request\n//\n// Default is 100 milliseconds.\n//\n// NOTE: The server-sent retry value takes precedence if present.\n//\n//\tsse.SetRetryWaitTime(1 * time.Second)\nfunc (sse *SSESource) SetRetryWaitTime(waitTime time.Duration) *SSESource {\n\tsse.lock.Lock()\n\tdefer sse.lock.Unlock()\n\tsse.retryWaitTime = waitTime\n\treturn sse\n}\n\n// SetRetryMaxWaitTime method sets the max wait time for sleep before retrying\n// the request\n//\n// Default is 2 seconds.\n//\n// NOTE: The server-sent retry value takes precedence if present.\n//\n//\tsse.SetRetryMaxWaitTime(3 * time.Second)\nfunc (sse *SSESource) SetRetryMaxWaitTime(maxWaitTime time.Duration) *SSESource {\n\tsse.lock.Lock()\n\tdefer sse.lock.Unlock()\n\tsse.retryMaxWaitTime = maxWaitTime\n\treturn sse\n}\n\n// SetSizeMaxBuffer method sets the given buffer size into the SSE client\n//\n// Default is 32kb\n//\n//\tsse.SetSizeMaxBuffer(64 * 1024) // 64kb\nfunc (sse *SSESource) SetSizeMaxBuffer(bufSize int) *SSESource {\n\tsse.lock.Lock()\n\tdefer sse.lock.Unlock()\n\tsse.maxBufSize = bufSize\n\treturn sse\n}\n\n// Logger method returns the logger instance used by the event source instance.\nfunc (sse *SSESource) Logger() Logger {\n\tsse.lock.RLock()\n\tdefer sse.lock.RUnlock()\n\treturn sse.log\n}\n\n// SetLogger method sets given writer for logging\n//\n// Compliant to interface [resty.Logger]\nfunc (sse *SSESource) SetLogger(l Logger) *SSESource {\n\tsse.lock.Lock()\n\tdefer sse.lock.Unlock()\n\tsse.log = l\n\treturn sse\n}\n\n// just an internal helper method for test case\nfunc (sse *SSESource) outputLogTo(w io.Writer) *SSESource {\n\tsse.lock.Lock()\n\tdefer sse.lock.Unlock()\n\tsse.log.(*logger).l.SetOutput(w)\n\treturn sse\n}\n\n// OnOpen registered callback gets triggered when the connection is\n// established with the server\n//\n//\tsse.OnOpen(func(url string, resHdr http.Header) {\n//\t\tfmt.Println(\"I'm connected:\", url, resHdr)\n//\t})\nfunc (sse *SSESource) OnOpen(ef SSEOpenFunc) *SSESource {\n\tsse.lock.Lock()\n\tdefer sse.lock.Unlock()\n\tif sse.onOpen != nil {\n\t\tsse.log.Warnf(\"Overwriting an existing OnOpen callback from=%s to=%s\",\n\t\t\tfunctionName(sse.onOpen), functionName(ef))\n\t}\n\tsse.onOpen = ef\n\treturn sse\n}\n\n// OnError registered callback gets triggered when the error occurred\n// in the process\n//\n//\tsse.OnError(func(err error) {\n//\t\tfmt.Println(\"Error occurred:\", err)\n//\t})\nfunc (sse *SSESource) OnError(ef SSEErrorFunc) *SSESource {\n\tsse.lock.Lock()\n\tdefer sse.lock.Unlock()\n\tif sse.onError != nil {\n\t\tsse.log.Warnf(\"Overwriting an existing OnError callback from=%s to=%s\",\n\t\t\tfunctionName(sse.onError), functionName(ef))\n\t}\n\tsse.onError = ef\n\treturn sse\n}\n\n// OnRequestFailure registered callback gets triggered when the HTTP request\n// failure while establishing a SSE connection.\n//\n//\tsse.OnRequestFailure(func(err error, res *http.Response) {\n//\t\tfmt.Println(\"Error and response:\", err, res)\n//\t})\n//\n// NOTE:\n//   - Do not forget to close the HTTP response body.\n//   - HTTP response may be nil.\nfunc (sse *SSESource) OnRequestFailure(ef SSERequestFailureFunc) *SSESource {\n\tsse.lock.Lock()\n\tdefer sse.lock.Unlock()\n\tif sse.onRequestFailure != nil {\n\t\tsse.log.Warnf(\"Overwriting an existing OnRequestFailure callback from=%s to=%s\",\n\t\t\tfunctionName(sse.onRequestFailure), functionName(ef))\n\t}\n\tsse.onRequestFailure = ef\n\treturn sse\n}\n\n// OnMessage method registers a callback to emit every SSE event message\n// from the server. The second result argument is optional; it can be used\n// to register the data type for JSON data.\n//\n//\tsse.OnMessage(\n//\t\tfunc(e any) {\n//\t\t\tevent := e.(*resty.SSE)\n//\t\t\tfmt.Println(\"Event message\", event)\n//\t\t},\n//\t\tnil,\n//\t)\n//\n//\t// Receiving JSON data from the server, you can set result type\n//\t// to do auto-unmarshal\n//\tsse.OnMessage(\n//\t\tfunc(e any) {\n//\t\t\tevent := e.(*MyData)\n//\t\t\tfmt.Println(event)\n//\t\t},\n//\t\tMyData{},\n//\t)\nfunc (sse *SSESource) OnMessage(ef SSEMessageFunc, result any) *SSESource {\n\treturn sse.AddEventListener(defaultEventName, ef, result)\n}\n\n// AddEventListener method registers a callback to consume a specific event type\n// messages from the server. The second result argument is optional; it can be used\n// to register the data type for JSON data.\n//\n//\tsse.AddEventListener(\n//\t\t\"friend_logged_in\",\n//\t\tfunc(e any) {\n//\t\t\tevent := e.(*resty.SSE)\n//\t\t\tfmt.Println(event)\n//\t\t},\n//\t\tnil,\n//\t)\n//\n//\t// Receiving JSON data from the server, you can set result type\n//\t// to do auto-unmarshal\n//\tsse.AddEventListener(\n//\t\t\"friend_logged_in\",\n//\t\tfunc(e any) {\n//\t\t\tevent := e.(*UserLoggedIn)\n//\t\t\tfmt.Println(event)\n//\t\t},\n//\t\tUserLoggedIn{},\n//\t)\nfunc (sse *SSESource) AddEventListener(eventName string, ef SSEMessageFunc, result any) *SSESource {\n\tsse.lock.Lock()\n\tdefer sse.lock.Unlock()\n\tif e, found := sse.onEvent[eventName]; found {\n\t\tsse.log.Warnf(\"Overwriting an existing OnEvent callback from=%s to=%s\",\n\t\t\tfunctionName(e), functionName(ef))\n\t}\n\tcb := &callback{Func: ef, Result: nil}\n\tif result != nil {\n\t\tcb.Result = getPointer(result)\n\t}\n\tsse.onEvent[eventName] = cb\n\treturn sse\n}\n\n// Get method establishes the connection with the server.\n//\n//\tsse := NewSSE().\n//\t\tSetURL(\"https://sse.dev/test\").\n//\t\tOnMessage(\n//\t\t\tfunc(e any) {\n//\t\t\t\tevent := e.(*resty.SSE)\n//\t\t\t\tfmt.Println(event)\n//\t\t\t},\n//\t\t\tnil, // see method godoc\n//\t\t)\n//\n//\terr := sse.Get()\n//\tfmt.Println(err)\nfunc (sse *SSESource) Get() error {\n\t// Validate required values\n\tif isStringEmpty(sse.url) {\n\t\treturn fmt.Errorf(\"resty:sse: event source URL is required\")\n\t}\n\n\tif isStringEmpty(sse.method) {\n\t\t// It is up to the user to choose which http method to use, depending on the specific code implementation. No restrictions are imposed here.\n\t\t// Ensure compatibility, use GET as default http method\n\t\tsse.method = defaultHTTPMethod\n\t}\n\n\tif len(sse.onEvent) == 0 {\n\t\treturn fmt.Errorf(\"resty:sse: At least one OnMessage/AddEventListener func is required\")\n\t}\n\n\t// reset to begin\n\tsse.enableConnect()\n\n\tfor {\n\t\tif sse.isClosed() {\n\t\t\treturn nil\n\t\t}\n\t\tres, err := sse.connect()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tsse.triggerOnOpen(res.Header.Clone())\n\t\tif err := sse.listenStream(res); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n\n// Close method used to close SSE connection explicitly\nfunc (sse *SSESource) Close() {\n\tsse.lock.Lock()\n\tdefer sse.lock.Unlock()\n\tsse.closed = true\n}\n\nfunc (sse *SSESource) enableConnect() {\n\tsse.lock.Lock()\n\tdefer sse.lock.Unlock()\n\tsse.closed = false\n}\n\nfunc (sse *SSESource) isClosed() bool {\n\tsse.lock.RLock()\n\tdefer sse.lock.RUnlock()\n\treturn sse.closed\n}\n\nfunc (sse *SSESource) triggerOnOpen(hdr http.Header) {\n\tsse.lock.RLock()\n\tdefer sse.lock.RUnlock()\n\tif sse.onOpen != nil {\n\t\tsse.onOpen(strings.Clone(sse.url), hdr)\n\t}\n}\n\nfunc (sse *SSESource) triggerOnError(err error) {\n\tsse.lock.RLock()\n\tdefer sse.lock.RUnlock()\n\tif sse.onError != nil {\n\t\tsse.onError(err)\n\t}\n}\n\nfunc (sse *SSESource) triggerOnRequestFailure(err error, res *http.Response) {\n\tsse.lock.RLock()\n\tdefer sse.lock.RUnlock()\n\tif sse.onRequestFailure != nil {\n\t\tsse.onRequestFailure(err, res)\n\t}\n}\n\nfunc (sse *SSESource) createRequest() (*http.Request, error) {\n\tvar reqBody io.Reader\n\tif sse.bodyBytes != nil {\n\t\t// create reader from bytes on each request\n\t\treqBody = bytes.NewReader(sse.bodyBytes)\n\t}\n\n\treq, err := http.NewRequest(sse.method, sse.url, reqBody)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header = sse.header.Clone()\n\treq.Header.Set(hdrAcceptKey, \"text/event-stream\")\n\treq.Header.Set(hdrCacheControlKey, \"no-cache\")\n\treq.Header.Set(hdrConnectionKey, \"keep-alive\")\n\tif len(sse.lastEventID) > 0 {\n\t\treq.Header.Set(hdrLastEvevntID, sse.lastEventID)\n\t}\n\n\treturn req, nil\n}\n\nfunc (sse *SSESource) connect() (*http.Response, error) {\n\tsse.lock.RLock()\n\tdefer sse.lock.RUnlock()\n\n\tvar backoff *backoffWithJitter\n\tif sse.serverSentRetry > 0 {\n\t\tbackoff = newBackoffWithJitter(sse.serverSentRetry, sse.serverSentRetry)\n\t} else {\n\t\tbackoff = newBackoffWithJitter(sse.retryWaitTime, sse.retryMaxWaitTime)\n\t}\n\n\tvar (\n\t\terr     error\n\t\tattempt int\n\t)\n\tfor i := 0; i <= sse.retryCount; i++ {\n\t\tattempt++\n\t\treq, reqErr := sse.createRequest()\n\t\tif reqErr != nil {\n\t\t\terr = reqErr\n\t\t\tbreak\n\t\t}\n\n\t\tresp, doErr := sse.httpClient.Do(req)\n\t\tif resp != nil && resp.StatusCode == http.StatusOK {\n\t\t\t// successful connection, return response to listenStream\n\t\t\treturn resp, nil\n\t\t}\n\n\t\t// we have reached the maximum no. of requests\n\t\t// first attempt + retry count = total attempts\n\t\tif attempt-1 == sse.retryCount {\n\t\t\terr = doErr\n\t\t\tbreak\n\t\t}\n\n\t\trRes := wrapResponse(resp, req)\n\t\tneedsRetry := applyRetryDefaultConditions(rRes, doErr)\n\n\t\t// retry not required stop here\n\t\tif !needsRetry {\n\t\t\tif rRes != nil {\n\t\t\t\terr = wrapErrors(fmt.Errorf(\"resty:sse: %v\", rRes.Status()), doErr)\n\t\t\t} else {\n\t\t\t\terr = doErr\n\t\t\t}\n\t\t\tif err != nil {\n\t\t\t\tsse.triggerOnRequestFailure(err, resp)\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\n\t\t// let's drain the response body, before retry wait\n\t\tdrainBody(rRes)\n\n\t\twaitDuration, _ := backoff.NextWaitDuration(nil, rRes, doErr, attempt)\n\t\ttimer := time.NewTimer(waitDuration)\n\t\t<-timer.C\n\t\ttimer.Stop()\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn nil, fmt.Errorf(\"resty:sse: unable to connect stream\")\n}\n\nfunc (sse *SSESource) listenStream(res *http.Response) error {\n\tdefer closeq(res.Body)\n\n\tscanner := bufio.NewScanner(res.Body)\n\tscanner.Buffer(make([]byte, slices.Min([]int{4096, sse.maxBufSize})), sse.maxBufSize)\n\tscanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) {\n\t\tif atEOF && len(data) == 0 {\n\t\t\treturn 0, nil, nil\n\t\t}\n\t\tif i := bytes.Index(data, []byte{'\\n', '\\n'}); i >= 0 {\n\t\t\t// We have a full double newline-terminated line.\n\t\t\treturn i + 1, data[0:i], nil\n\t\t}\n\t\t// If we're at EOF, we have a final, non-terminated line. Return it.\n\t\tif atEOF {\n\t\t\treturn len(data), data, nil\n\t\t}\n\t\t// Request more data.\n\t\treturn 0, nil, nil\n\t})\n\n\tfor {\n\t\tif sse.isClosed() {\n\t\t\treturn nil\n\t\t}\n\n\t\tif err := sse.processEvent(scanner); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n\nfunc (sse *SSESource) processEvent(scanner *bufio.Scanner) error {\n\te, err := readEvent(scanner)\n\tif err != nil {\n\t\tif err == io.EOF {\n\t\t\treturn err\n\t\t}\n\t\tsse.triggerOnError(err)\n\t\treturn err\n\t}\n\n\ted, err := parseEvent(e)\n\tif err != nil {\n\t\tsse.triggerOnError(err)\n\t\treturn nil // parsing errors, will not return error.\n\t}\n\tdefer putRawEvent(ed)\n\n\tif len(ed.ID) > 0 {\n\t\tsse.lock.Lock()\n\t\tsse.lastEventID = string(ed.ID)\n\t\tsse.lock.Unlock()\n\t}\n\n\tif len(ed.Retry) > 0 {\n\t\tif retry, err := strconv.Atoi(string(ed.Retry)); err == nil {\n\t\t\tsse.lock.Lock()\n\t\t\tsse.serverSentRetry = time.Millisecond * time.Duration(retry)\n\t\t\tsse.lock.Unlock()\n\t\t} else {\n\t\t\tsse.triggerOnError(err)\n\t\t}\n\t}\n\n\tif len(ed.Data) > 0 {\n\t\tsse.handleCallback(&SSE{\n\t\t\tID:   string(ed.ID),\n\t\t\tName: string(ed.Event),\n\t\t\tData: string(ed.Data),\n\t\t})\n\t}\n\n\treturn nil\n}\n\nfunc (sse *SSESource) handleCallback(e *SSE) {\n\teventName := e.Name\n\tif len(eventName) == 0 {\n\t\teventName = defaultEventName\n\t}\n\n\tsse.lock.RLock()\n\tcb, found := sse.onEvent[eventName]\n\tsse.lock.RUnlock()\n\n\tif found {\n\t\tif cb.Result == nil {\n\t\t\tcb.Func(e)\n\t\t\treturn\n\t\t}\n\t\tr := newInterface(cb.Result)\n\t\tif err := decodeJSON(strings.NewReader(e.Data), r); err != nil {\n\t\t\tsse.triggerOnError(err)\n\t\t\treturn\n\t\t}\n\t\tcb.Func(r)\n\t}\n}\n\nvar readEvent = readEventFunc\n\nfunc readEventFunc(scanner *bufio.Scanner) ([]byte, error) {\n\tif scanner.Scan() {\n\t\tevent := scanner.Bytes()\n\t\treturn event, nil\n\t}\n\tif err := scanner.Err(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn nil, io.EOF\n}\n\nfunc wrapResponse(res *http.Response, req *http.Request) *Response {\n\tif res == nil {\n\t\treturn nil\n\t}\n\treturn &Response{RawResponse: res, Request: &Request{RawRequest: req}}\n}\n\ntype rawSSE struct {\n\tID    []byte\n\tData  []byte\n\tEvent []byte\n\tRetry []byte\n}\n\nvar parseEvent = parseEventFunc\n\n// event value parsing logic obtained and modified for Resty processing flow.\n// https://github.com/r3labs/sse/blob/c6d5381ee3ca63828b321c16baa008fd6c0b4564/client.go#L322\nfunc parseEventFunc(msg []byte) (*rawSSE, error) {\n\tif len(msg) < 1 {\n\t\treturn nil, errors.New(\"resty:sse: event message was empty\")\n\t}\n\n\te := newRawEvent()\n\n\t// Split the line by \"\\n\"\n\tfor _, line := range bytes.FieldsFunc(msg, func(r rune) bool { return r == '\\n' }) {\n\t\tswitch {\n\t\tcase bytes.HasPrefix(line, headerID):\n\t\t\te.ID = append([]byte(nil), trimHeader(len(headerID), line)...)\n\t\tcase bytes.HasPrefix(line, headerData):\n\t\t\t// The spec allows for multiple data fields per event, concatenated them with \"\\n\"\n\t\t\te.Data = append(e.Data[:], append(trimHeader(len(headerData), line), byte('\\n'))...)\n\t\t// The spec says that a line that simply contains the string \"data\" should be treated as a data field with an empty body.\n\t\tcase bytes.Equal(line, bytes.TrimSuffix(headerData, []byte(\":\"))):\n\t\t\te.Data = append(e.Data, byte('\\n'))\n\t\tcase bytes.HasPrefix(line, headerEvent):\n\t\t\te.Event = append([]byte(nil), trimHeader(len(headerEvent), line)...)\n\t\tcase bytes.HasPrefix(line, headerRetry):\n\t\t\te.Retry = append([]byte(nil), trimHeader(len(headerRetry), line)...)\n\t\tdefault:\n\t\t\t// Ignore anything that doesn't match the header\n\t\t}\n\t}\n\n\t// Trim the last \"\\n\" per the spec\n\te.Data = bytes.TrimSuffix(e.Data, []byte(\"\\n\"))\n\n\treturn e, nil\n}\n\nfunc trimHeader(size int, data []byte) []byte {\n\tif data == nil || len(data) < size {\n\t\treturn data\n\t}\n\tdata = data[size:]\n\tif len(data) > 0 && data[0] == ' ' {\n\t\tdata = data[1:]\n\t}\n\tif len(data) > 0 && data[len(data)-1] == '\\n' {\n\t\tdata = data[:len(data)-1]\n\t}\n\treturn data\n}\n\nvar rawEventPool = &sync.Pool{New: func() any { return new(rawSSE) }}\n\nfunc newRawEvent() *rawSSE {\n\te := rawEventPool.Get().(*rawSSE)\n\te.ID = e.ID[:0]\n\te.Data = e.Data[:0]\n\te.Event = e.Event[:0]\n\te.Retry = e.Retry[:0]\n\treturn e\n}\n\nfunc putRawEvent(e *rawSSE) {\n\trawEventPool.Put(e)\n}\n"
  },
  {
    "path": "sse_test.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestSSESourceSimpleFlow(t *testing.T) {\n\tes := createSSESource(t, \"\", nil, nil)\n\n\tmessageCounter := 0\n\tmessageFunc := func(e any) {\n\t\tevent := e.(*SSE)\n\t\tassertEqual(t, strconv.Itoa(messageCounter), event.ID)\n\t\tassertTrue(t, strings.HasPrefix(event.Data, \"The time is\"))\n\t\tmessageCounter++\n\t\tif messageCounter == 100 {\n\t\t\tes.Close()\n\t\t}\n\t}\n\tes.OnMessage(messageFunc, nil)\n\n\tcounter := 0\n\tts := createSSETestServer(\n\t\tt,\n\t\t10*time.Millisecond,\n\t\tfunc(w io.Writer) error {\n\t\t\tif counter == 100 {\n\t\t\t\treturn fmt.Errorf(\"stop sending events\")\n\t\t\t}\n\t\t\t_, err := fmt.Fprintf(w, \"id: %v\\ndata: The time is %s\\n\\n\", counter, time.Now().Format(time.UnixDate))\n\t\t\tcounter++\n\t\t\treturn err\n\t\t},\n\t)\n\tdefer ts.Close()\n\n\tes.SetURL(ts.URL)\n\tes.SetMethod(MethodPost)\n\terr := es.Get()\n\tassertNil(t, err)\n\tassertEqual(t, counter, messageCounter)\n}\n\nfunc TestSSESourceMultipleEventTypes(t *testing.T) {\n\ttype userEvent struct {\n\t\tUserName string    `json:\"username\"`\n\t\tMessage  string    `json:\"msg\"`\n\t\tTime     time.Time `json:\"time\"`\n\t}\n\n\ttm := time.Now().Add(-1 * time.Minute)\n\tuserConnectCounter := 0\n\tuserConnectFunc := func(e any) {\n\t\tdata := e.(*userEvent)\n\t\tassertEqual(t, \"username\"+strconv.Itoa(userConnectCounter), data.UserName)\n\t\tassertTrue(t, data.Time.After(tm))\n\t\tuserConnectCounter++\n\t}\n\n\tuserMessageCounter := 0\n\tuserMessageFunc := func(e any) {\n\t\tdata := e.(*userEvent)\n\t\tassertEqual(t, \"username\"+strconv.Itoa(userConnectCounter), data.UserName)\n\t\tassertEqual(t, \"Hello, how are you?\", data.Message)\n\t\tassertTrue(t, data.Time.After(tm))\n\t\tuserMessageCounter++\n\t}\n\n\tcounter := 0\n\tes := createSSESource(t, \"\", func(any) {}, nil)\n\tts := createSSETestServer(\n\t\tt,\n\t\t10*time.Millisecond,\n\t\tfunc(w io.Writer) error {\n\t\t\tif counter == 100 {\n\t\t\t\tes.Close()\n\t\t\t\treturn fmt.Errorf(\"stop sending events\")\n\t\t\t}\n\n\t\t\tid := counter / 2\n\t\t\tif counter%2 == 0 {\n\t\t\t\tevent := fmt.Sprintf(\"id: %v\\n\"+\n\t\t\t\t\t\"event: user_message\\n\"+\n\t\t\t\t\t`data: {\"username\": \"%v\", \"time\": \"%v\", \"msg\": \"Hello, how are you?\"}`+\"\\n\\n\",\n\t\t\t\t\tid,\n\t\t\t\t\t\"username\"+strconv.Itoa(id),\n\t\t\t\t\ttime.Now().Format(time.RFC3339),\n\t\t\t\t)\n\t\t\t\tfmt.Fprint(w, event)\n\t\t\t} else {\n\t\t\t\tevent := fmt.Sprintf(\"id: %v\\n\"+\n\t\t\t\t\t\"event: user_connect\\n\"+\n\t\t\t\t\t`data: {\"username\": \"%v\", \"time\": \"%v\"}`+\"\\n\\n\",\n\t\t\t\t\tint(id),\n\t\t\t\t\t\"username\"+strconv.Itoa(int(id)),\n\t\t\t\t\ttime.Now().Format(time.RFC3339),\n\t\t\t\t)\n\t\t\t\tfmt.Fprint(w, event)\n\t\t\t}\n\n\t\t\tcounter++\n\t\t\treturn nil\n\t\t},\n\t)\n\tdefer ts.Close()\n\n\tes.SetURL(ts.URL).\n\t\tSetMethod(MethodPost).\n\t\tAddEventListener(\"user_connect\", userConnectFunc, userEvent{}).\n\t\tAddEventListener(\"user_message\", userMessageFunc, userEvent{})\n\n\terr := es.Get()\n\tassertNil(t, err)\n\tassertEqual(t, userConnectCounter, userMessageCounter)\n}\n\nfunc TestSSESourceOverwriteFuncs(t *testing.T) {\n\tmessageFunc1 := func(e any) {\n\t\tassertNotNil(t, e)\n\t}\n\tes := createSSESource(t, \"\", messageFunc1, nil)\n\n\tmessage2Counter := 0\n\tmessageFunc2 := func(e any) {\n\t\tevent := e.(*SSE)\n\t\tassertEqual(t, strconv.Itoa(message2Counter), event.ID)\n\t\tassertTrue(t, strings.HasPrefix(event.Data, \"The time is\"))\n\t\tmessage2Counter++\n\t\tif message2Counter == 50 {\n\t\t\tes.Close()\n\t\t}\n\t}\n\n\tcounter := 0\n\tts := createSSETestServer(\n\t\tt,\n\t\t10*time.Millisecond,\n\t\tfunc(w io.Writer) error {\n\t\t\tif counter == 50 {\n\t\t\t\treturn fmt.Errorf(\"stop sending events\")\n\t\t\t}\n\t\t\t_, err := fmt.Fprintf(w, \"id: %v\\ndata: The time is %s\\n\\n\", counter, time.Now().Format(time.UnixDate))\n\t\t\tcounter++\n\t\t\treturn err\n\t\t},\n\t)\n\tdefer ts.Close()\n\n\tlb := new(bytes.Buffer)\n\tes.outputLogTo(lb)\n\n\tes.SetURL(ts.URL).\n\t\tOnMessage(messageFunc2, nil).\n\t\tOnOpen(func(url string, respHdr http.Header) {\n\t\t\tt.Log(\"from overwrite func\", url, respHdr)\n\t\t}).\n\t\tOnError(func(err error) {\n\t\t\tt.Log(\"from overwrite func\", err)\n\t\t})\n\n\terr := es.Get()\n\tassertNil(t, err)\n\tassertEqual(t, counter, message2Counter)\n\n\tlogLines := lb.String()\n\tassertTrue(t, strings.Contains(logLines, \"Overwriting an existing OnEvent callback\"))\n\tassertTrue(t, strings.Contains(logLines, \"Overwriting an existing OnOpen callback\"))\n\tassertTrue(t, strings.Contains(logLines, \"Overwriting an existing OnError callback\"))\n}\n\nfunc TestSSESourceRetry(t *testing.T) {\n\tes := createSSESource(t, \"\", nil, nil)\n\n\tmessageCounter := 2 // 0 & 1 connection failure\n\tmessageFunc := func(e any) {\n\t\tevent := e.(*SSE)\n\t\tassertEqual(t, strconv.Itoa(messageCounter), event.ID)\n\t\tassertTrue(t, strings.HasPrefix(event.Data, \"The time is\"))\n\t\tmessageCounter++\n\t\tif messageCounter == 15 {\n\t\t\tes.Close()\n\t\t}\n\t}\n\tes.OnMessage(messageFunc, nil)\n\n\tcounter := 0\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tif counter == 1 && r.URL.Query().Get(\"reconnect\") == \"1\" {\n\t\t\tw.WriteHeader(http.StatusTooManyRequests)\n\t\t\tcounter++\n\t\t\treturn\n\t\t}\n\t\tif counter < 2 || counter == 7 {\n\t\t\tw.WriteHeader(http.StatusTooManyRequests)\n\t\t\tcounter++\n\t\t\treturn\n\t\t}\n\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.Header().Set(\"Cache-Control\", \"no-cache\")\n\t\tw.Header().Set(\"Connection\", \"keep-alive\")\n\n\t\t// for local testing allow it\n\t\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\n\t\t// Create a channel for client disconnection\n\t\tclientGone := r.Context().Done()\n\n\t\trc := http.NewResponseController(w)\n\t\ttick := time.NewTicker(10 * time.Millisecond)\n\t\tdefer tick.Stop()\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-clientGone:\n\t\t\t\tt.Log(\"Client disconnected\")\n\t\t\t\treturn\n\t\t\tcase <-tick.C:\n\t\t\t\tif counter == 5 {\n\t\t\t\t\tfmt.Fprintf(w, \"id: %v\\nretry: abc\\ndata: The time is %s\\n\\n\", counter, time.Now().Format(time.UnixDate))\n\t\t\t\t\tcounter++\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif counter == 15 {\n\t\t\t\t\tes.Close()\n\t\t\t\t\treturn // stop sending events\n\t\t\t\t}\n\t\t\t\tfmt.Fprintf(w, \"id: %v\\nretry: 1\\ndata: The time is %s\\ndata\\n\\n\", counter, time.Now().Format(time.UnixDate))\n\t\t\t\tcounter++\n\t\t\t\tif err := rc.Flush(); err != nil {\n\t\t\t\t\tt.Log(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t})\n\tdefer ts.Close()\n\n\t// first round\n\tes.SetURL(ts.URL)\n\terr1 := es.Get()\n\tassertNotNil(t, err1)\n\n\t// second round\n\tcounter = 0\n\tmessageCounter = 2\n\tes.SetRetryCount(1).\n\t\tSetURL(ts.URL + \"?reconnect=1\")\n\terr2 := es.Get()\n\tassertNotNil(t, err2)\n}\n\nfunc TestSSESourceRetryReusesRequestBody(t *testing.T) {\n\tconst payload = `{\"test\":\"retry-body\"}`\n\n\tattempt := 0\n\tbodies := make([]string, 0, 2)\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tbody, err := io.ReadAll(r.Body)\n\t\tassertNil(t, err)\n\t\tbodies = append(bodies, string(body))\n\n\t\tattempt++\n\t\tif attempt == 1 {\n\t\t\tw.WriteHeader(http.StatusTooManyRequests)\n\t\t\treturn\n\t\t}\n\n\t\tw.WriteHeader(http.StatusOK)\n\t})\n\tdefer ts.Close()\n\n\tes := NewSSESource().\n\t\tSetURL(ts.URL).\n\t\tSetMethod(MethodPost).\n\t\tSetRetryCount(1).\n\t\tSetRetryWaitTime(1 * time.Millisecond).\n\t\tSetRetryMaxWaitTime(1 * time.Millisecond)\n\tes.SetBody(bytes.NewBufferString(payload))\n\n\tresp, err := es.connect()\n\tassertNil(t, err)\n\tassertNotNil(t, resp)\n\tif resp != nil {\n\t\tcloseq(resp.Body)\n\t}\n\n\tassertEqual(t, 2, attempt, \"expected one retry attempt\")\n\tassertEqual(t, 2, len(bodies), \"expected request body on both attempts\")\n\tassertEqual(t, payload, bodies[0], \"expected first attempt body to match\")\n\tassertEqual(t, payload, bodies[1], \"expected retry attempt body to match\")\n}\n\nfunc TestSSESourceTLSConfigerInterface(t *testing.T) {\n\n\tt.Run(\"set and get tls config\", func(t *testing.T) {\n\t\tes := createSSESource(t, \"\", func(any) {}, nil)\n\n\t\ttc, err := es.tlsConfig()\n\t\tassertNil(t, err)\n\t\tassertNotNil(t, tc)\n\n\t\ttlsConfig := &tls.Config{InsecureSkipVerify: true}\n\t\tes.SetTLSClientConfig(tlsConfig)\n\t\tassertEqual(t, tlsConfig, es.TLSClientConfig())\n\t})\n\n\tt.Run(\"get tls config error\", func(t *testing.T) {\n\t\tes := createSSESource(t, \"\", func(any) {}, nil)\n\n\t\tct := &CustomRoundTripper1{}\n\t\tes.httpClient.Transport = ct\n\t\tassertNil(t, es.TLSClientConfig())\n\t})\n\n\tt.Run(\"set tls config\", func(t *testing.T) {\n\t\tes := createSSESource(t, \"\", func(any) {}, nil)\n\n\t\tct := &CustomRoundTripper2{}\n\t\tes.httpClient.Transport = ct\n\n\t\ttlsConfig := &tls.Config{InsecureSkipVerify: true}\n\t\tes.SetTLSClientConfig(tlsConfig)\n\t\tassertNotNil(t, es.TLSClientConfig())\n\t})\n\n\tt.Run(\"set tls config error\", func(t *testing.T) {\n\t\tes := createSSESource(t, \"\", func(any) {}, nil)\n\n\t\tct := &CustomRoundTripper2{returnErr: true}\n\t\tes.httpClient.Transport = ct\n\n\t\ttlsConfig := &tls.Config{InsecureSkipVerify: true}\n\t\tes.SetTLSClientConfig(tlsConfig)\n\t\tassertNil(t, es.TLSClientConfig())\n\t})\n}\n\nfunc TestSSESourceNoRetryRequired(t *testing.T) {\n\tes := createSSESource(t, \"\", func(any) {}, nil)\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusBadRequest)\n\t})\n\tdefer ts.Close()\n\n\tes.SetURL(ts.URL)\n\terr := es.Get()\n\tfmt.Println(err)\n\tassertTrue(t, strings.Contains(err.Error(), \"400 Bad Request\"))\n}\n\nfunc TestGH1044TrimHeader(t *testing.T) {\n\tt.Run(\"data is nil\", func(t *testing.T) {\n\t\tresult := trimHeader(0, nil)\n\t\tassertNil(t, result)\n\t})\n\n\tt.Run(\"data has double whitespace\", func(t *testing.T) {\n\t\tdata := []byte(\"data:  double whitespace message\")\n\t\tresult := trimHeader(5, data)\n\t\tassertTrue(t, result[0] == ' ')\n\t})\n\n\tt.Run(\"data has newline\", func(t *testing.T) {\n\t\tdata := []byte(\"data: newline message\\n\")\n\t\tresult := trimHeader(5, data)\n\t\tassertTrue(t, result[len(result)-1] != '\\n')\n\t})\n}\n\nfunc TestGH1041RequestFailureWithResponseBody(t *testing.T) {\n\tes := createSSESource(t, \"\", func(any) {}, nil)\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(hdrContentTypeKey, jsonContentType)\n\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t_, _ = w.Write([]byte(`{ \"id\": \"bad_request\", \"message\": \"Unable to establish connection\" }`))\n\t})\n\tdefer ts.Close()\n\n\trfFunc := func(err error, res *http.Response) {\n\t\tdefer res.Body.Close()\n\t\tresBytes, _ := io.ReadAll(res.Body)\n\n\t\tassertNotNil(t, err)\n\t\tassertEqual(t, \"resty:sse: 400 Bad Request\", err.Error())\n\t\tassertEqual(t, `{ \"id\": \"bad_request\", \"message\": \"Unable to establish connection\" }`, string(resBytes))\n\t}\n\n\tes.SetURL(ts.URL).OnRequestFailure(rfFunc)\n\tes.OnRequestFailure(rfFunc)\n\terr := es.Get()\n\tassertNotNil(t, err)\n\tassertEqual(t, \"resty:sse: 400 Bad Request\", err.Error())\n}\n\nfunc TestSSESourceHTTPError(t *testing.T) {\n\tes := createSSESource(t, \"\", func(any) {}, nil)\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\thttp.Redirect(w, r, \"http://local host\", http.StatusTemporaryRedirect)\n\t})\n\tdefer ts.Close()\n\n\tes.SetURL(ts.URL)\n\terr := es.Get()\n\tassertTrue(t, strings.Contains(err.Error(), `invalid character \" \" in host name`))\n}\n\nfunc TestSSESourceParseAndReadError(t *testing.T) {\n\ttype data struct{}\n\tcounter := 0\n\tes := createSSESource(t, \"\", func(any) {}, data{})\n\tts := createSSETestServer(\n\t\tt,\n\t\t5*time.Millisecond,\n\t\tfunc(w io.Writer) error {\n\t\t\tif counter == 5 {\n\t\t\t\tes.Close()\n\t\t\t\treturn fmt.Errorf(\"stop sending events\")\n\t\t\t}\n\t\t\t_, err := fmt.Fprintf(w, \"id: %v\\n\"+\n\t\t\t\t`data: The time is %s\\n\\n`+\"\\n\\n\", counter, time.Now().Format(time.UnixDate))\n\t\t\tcounter++\n\t\t\treturn err\n\t\t},\n\t)\n\tdefer ts.Close()\n\n\tes.SetURL(ts.URL)\n\terr := es.Get()\n\tassertNil(t, err)\n\n\t// parse error\n\tparseEvent = func(_ []byte) (*rawSSE, error) {\n\t\treturn nil, errors.New(\"test error\")\n\t}\n\tcounter = 0\n\terr = es.Get()\n\tassertNil(t, err)\n\tt.Cleanup(func() {\n\t\tparseEvent = parseEventFunc\n\t})\n}\n\nfunc TestSSESourceReadError(t *testing.T) {\n\tes := createSSESource(t, \"\", func(any) {}, nil)\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.WriteHeader(http.StatusOK)\n\t})\n\tdefer ts.Close()\n\n\t// read error\n\treadEvent = func(_ *bufio.Scanner) ([]byte, error) {\n\t\treturn nil, errors.New(\"read event test error\")\n\t}\n\tt.Cleanup(func() {\n\t\treadEvent = readEventFunc\n\t})\n\n\tes.SetURL(ts.URL)\n\terr := es.Get()\n\tassertNotNil(t, err)\n\tassertTrue(t, strings.Contains(err.Error(), \"read event test error\"))\n}\n\nfunc TestSSESourceWithDifferentMethods(t *testing.T) {\n\ttestCases := []struct {\n\t\tname   string\n\t\tmethod string\n\t\tbody   []byte\n\t}{\n\t\t{\n\t\t\tname:   \"GET Method\",\n\t\t\tmethod: MethodGet,\n\t\t\tbody:   nil,\n\t\t},\n\t\t{\n\t\t\tname:   \"POST Method\",\n\t\t\tmethod: MethodPost,\n\t\t\tbody:   []byte(`{\"test\":\"post_data\"}`),\n\t\t},\n\t\t{\n\t\t\tname:   \"PUT Method\",\n\t\t\tmethod: MethodPut,\n\t\t\tbody:   []byte(`{\"test\":\"put_data\"}`),\n\t\t},\n\t\t{\n\t\t\tname:   \"DELETE Method\",\n\t\t\tmethod: MethodDelete,\n\t\t\tbody:   nil,\n\t\t},\n\t\t{\n\t\t\tname:   \"PATCH Method\",\n\t\t\tmethod: MethodPatch,\n\t\t\tbody:   []byte(`{\"test\":\"patch_data\"}`),\n\t\t},\n\t}\n\n\tfor _, tc := range testCases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tes := createSSESource(t, \"\", nil, nil)\n\n\t\t\tmessageCounter := 0\n\t\t\tmessageFunc := func(e any) {\n\t\t\t\tevent := e.(*SSE)\n\t\t\t\tassertEqual(t, strconv.Itoa(messageCounter), event.ID)\n\t\t\t\tassertTrue(t, strings.HasPrefix(event.Data, fmt.Sprintf(\"%s method test:\", tc.method)))\n\t\t\t\tmessageCounter++\n\t\t\t\tif messageCounter == 20 {\n\t\t\t\t\tes.Close()\n\t\t\t\t}\n\t\t\t}\n\t\t\tes.OnMessage(messageFunc, nil)\n\n\t\t\tcounter := 0\n\t\t\tmethodVerified := false\n\t\t\tbodyVerified := false\n\n\t\t\tts := createMethodVerifyingSSETestServer(\n\t\t\t\tt,\n\t\t\t\t10*time.Millisecond,\n\t\t\t\ttc.method,\n\t\t\t\ttc.body,\n\t\t\t\t&methodVerified,\n\t\t\t\t&bodyVerified,\n\t\t\t\tfunc(w io.Writer) error {\n\t\t\t\t\tif counter == 20 {\n\t\t\t\t\t\treturn fmt.Errorf(\"stop sending events\")\n\t\t\t\t\t}\n\t\t\t\t\t_, err := fmt.Fprintf(w, \"id: %v\\ndata: %s method test: %s\\n\\n\", counter, tc.method, time.Now().Format(time.RFC3339))\n\t\t\t\t\tcounter++\n\t\t\t\t\treturn err\n\t\t\t\t},\n\t\t\t)\n\t\t\tdefer ts.Close()\n\n\t\t\tes.SetURL(ts.URL)\n\t\t\tes.SetMethod(tc.method)\n\n\t\t\t// set body\n\t\t\tif tc.body != nil {\n\t\t\t\tes.SetBody(bytes.NewBuffer(tc.body))\n\t\t\t}\n\n\t\t\terr := es.Get()\n\t\t\tassertNil(t, err)\n\n\t\t\t// check the message count\n\t\t\tassertEqual(t, counter, messageCounter)\n\n\t\t\t// check if server receive correct method and body\n\t\t\tassertTrue(t, methodVerified)\n\t\t\tif tc.body != nil {\n\t\t\t\tassertTrue(t, bodyVerified)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestSSESource_readEventFunc(t *testing.T) {\n\tt.Run(\"successful scan\", func(t *testing.T) {\n\t\tinput := \"event: test\\ndata: test data\\n\\n\"\n\t\tscanner := bufio.NewScanner(strings.NewReader(input))\n\n\t\tevent, err := readEventFunc(scanner)\n\n\t\tassertNil(t, err)\n\t\tassertNotNil(t, event)\n\t\tassertEqual(t, \"event: test\", string(event))\n\t})\n\n\tt.Run(\"scanner error\", func(t *testing.T) {\n\t\t// Create a custom reader that returns an error\n\t\tscanner := bufio.NewScanner(&errorReader{})\n\n\t\tevent, err := readEventFunc(scanner)\n\n\t\tassertNotNil(t, err)\n\t\tassertNil(t, event)\n\t\tassertEqual(t, \"fake\", err.Error())\n\t})\n\n\tt.Run(\"EOF error\", func(t *testing.T) {\n\t\t// Empty reader will immediately return EOF\n\t\tscanner := bufio.NewScanner(strings.NewReader(\"\"))\n\n\t\tevent, err := readEventFunc(scanner)\n\n\t\tassertEqual(t, io.EOF, err)\n\t\tassertNil(t, event)\n\t})\n\n\tt.Run(\"multiple lines\", func(t *testing.T) {\n\t\tinput := \"line1\\nline2\\nline3\\n\"\n\t\tscanner := bufio.NewScanner(strings.NewReader(input))\n\n\t\t// First call should return the first line\n\t\tevent1, err1 := readEventFunc(scanner)\n\t\tassertNil(t, err1)\n\t\tassertEqual(t, \"line1\", string(event1))\n\n\t\t// Second call should return the second line\n\t\tevent2, err2 := readEventFunc(scanner)\n\t\tassertNil(t, err2)\n\t\tassertEqual(t, \"line2\", string(event2))\n\n\t\t// Third call should return the third line\n\t\tevent3, err3 := readEventFunc(scanner)\n\t\tassertNil(t, err3)\n\t\tassertEqual(t, \"line3\", string(event3))\n\n\t\t// Fourth call should return EOF\n\t\tevent4, err4 := readEventFunc(scanner)\n\t\tassertEqual(t, io.EOF, err4)\n\t\tassertNil(t, event4)\n\t})\n}\n\nfunc TestSSESourceCoverage(t *testing.T) {\n\tes := NewSSESource()\n\terr1 := es.Get()\n\tassertEqual(t, \"resty:sse: event source URL is required\", err1.Error())\n\n\tes.SetURL(\"https://sse.dev/test\")\n\terr2 := es.Get()\n\tassertEqual(t, \"resty:sse: At least one OnMessage/AddEventListener func is required\", err2.Error())\n\n\tes.OnMessage(func(a any) {}, nil)\n\tes.SetURL(\"//res%20ty.dev\")\n\terr3 := es.Get()\n\tassertTrue(t, strings.Contains(err3.Error(), `invalid URL escape \"%20\"`))\n\n\twrapResponse(nil, nil)\n\ttrimHeader(2, nil)\n\tparseEvent([]byte{})\n}\n\nfunc TestSSESetBody(t *testing.T) {\n\tt.Run(\"nil input\", func(t *testing.T) {\n\t\tes := createSSESource(t, \"\", nil, nil)\n\n\t\tes.SetBody(nil)\n\t\tassertNil(t, es.bodyBytes)\n\t})\n\n\tt.Run(\"read error\", func(t *testing.T) {\n\t\tes := createSSESource(t, \"\", nil, nil)\n\n\t\tes.SetBody(&errorReader{})\n\t\tassertNil(t, es.bodyBytes)\n\t})\n}\n\nfunc createSSESource(t *testing.T, url string, fn SSEMessageFunc, rt any) *SSESource {\n\tes := NewSSESource().\n\t\tSetURL(url).\n\t\tSetMethod(MethodGet).\n\t\tAddHeader(\"X-Test-Header-1\", \"test header 1\").\n\t\tSetHeader(\"X-Test-Header-2\", \"test header 2\").\n\t\tSetRetryCount(2).\n\t\tSetRetryWaitTime(200 * time.Millisecond).\n\t\tSetRetryMaxWaitTime(1000 * time.Millisecond).\n\t\tSetSizeMaxBuffer(1 << 14). // 16kb\n\t\tSetLogger(createLogger()).\n\t\tOnOpen(func(url string, respHdr http.Header) {\n\t\t\tt.Log(\"I'm connected:\", url, respHdr)\n\t\t}).\n\t\tOnError(func(err error) {\n\t\t\tt.Log(\"Error occurred:\", err)\n\t\t})\n\tif fn != nil {\n\t\tes.OnMessage(fn, rt)\n\t}\n\treturn es\n}\n\nfunc createSSETestServer(t *testing.T, ticker time.Duration, fn func(io.Writer) error) *httptest.Server {\n\treturn createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.Header().Set(\"Cache-Control\", \"no-cache\")\n\t\tw.Header().Set(\"Connection\", \"keep-alive\")\n\n\t\t// for local testing allow it\n\t\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\n\t\t// Create a channel for client disconnection\n\t\tclientGone := r.Context().Done()\n\n\t\trc := http.NewResponseController(w)\n\t\ttick := time.NewTicker(ticker)\n\t\tdefer tick.Stop()\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-clientGone:\n\t\t\t\tt.Log(\"Client disconnected\")\n\t\t\t\treturn\n\t\t\tcase <-tick.C:\n\t\t\t\tif err := fn(w); err != nil {\n\t\t\t\t\tt.Log(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif err := rc.Flush(); err != nil {\n\t\t\t\t\tt.Log(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t})\n}\n\n// almost like create server before but add verifying method and body\nfunc createMethodVerifyingSSETestServer(\n\tt *testing.T,\n\tticker time.Duration,\n\texpectedMethod string,\n\texpectedBody []byte,\n\tmethodVerified *bool,\n\tbodyVerified *bool,\n\tfn func(io.Writer) error,\n) *httptest.Server {\n\treturn createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\t// validate method\n\t\tif r.Method == expectedMethod {\n\t\t\t*methodVerified = true\n\t\t} else {\n\t\t\tt.Errorf(\"Expected method %s, got %s\", expectedMethod, r.Method)\n\t\t}\n\n\t\t// validate body\n\t\tif expectedBody != nil {\n\t\t\tbody, err := io.ReadAll(r.Body)\n\t\t\tif err != nil {\n\t\t\t\tt.Errorf(\"Failed to read request body: %v\", err)\n\t\t\t} else if string(body) == string(expectedBody) {\n\t\t\t\t*bodyVerified = true\n\t\t\t} else {\n\t\t\t\tt.Errorf(\"Expected body %s, got %s\", string(expectedBody), string(body))\n\t\t\t}\n\t\t}\n\n\t\t// same as createSSETestServer\n\t\tw.Header().Set(\"Content-Type\", \"text/event-stream\")\n\t\tw.Header().Set(\"Cache-Control\", \"no-cache\")\n\t\tw.Header().Set(\"Connection\", \"keep-alive\")\n\t\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\n\t\tclientGone := r.Context().Done()\n\n\t\trc := http.NewResponseController(w)\n\t\ttick := time.NewTicker(ticker)\n\t\tdefer tick.Stop()\n\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-clientGone:\n\t\t\t\tt.Log(\"Client disconnected\")\n\t\t\t\treturn\n\t\t\tcase <-tick.C:\n\t\t\t\tif err := fn(w); err != nil {\n\t\t\t\t\tt.Log(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tif err := rc.Flush(); err != nil {\n\t\t\t\t\tt.Log(err)\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "stream.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bytes\"\n\t\"compress/flate\"\n\t\"compress/gzip\"\n\t\"context\"\n\t\"encoding/json\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"sync\"\n)\n\nvar (\n\tErrContentDecompresserNotFound = errors.New(\"resty: content decoder not found\")\n\n\t// It's good to have decode limit; let's start with object size\n\t// limit as 1M objects, which should be more than enough for most\n\t// use cases if users need more, they can always implement their\n\t// own decoder and set it in Client.SetContentDecoder\n\t//\n\t// Max 1 million objects, +1 to detect if we exceed the limit without EOF\n\tmaxDecodeObjects = 1000001\n)\n\ntype (\n\t// ContentTypeEncoder type is for encoding the request body based on header Content-Type\n\tContentTypeEncoder func(io.Writer, any) error\n\n\t// ContentTypeDecoder type is for decoding the response body based on header Content-Type\n\tContentTypeDecoder func(io.Reader, any) error\n\n\t// ContentDecompresser type is for decompressing response body based on header Content-Encoding\n\t// ([RFC 9110])\n\t//\n\t// For example, gzip, deflate, etc.\n\t//\n\t// [RFC 9110]: https://datatracker.ietf.org/doc/html/rfc9110\n\tContentDecompresser func(io.ReadCloser) (io.ReadCloser, error)\n)\n\nfunc encodeJSON(w io.Writer, v any) error {\n\treturn encodeJSONEscapeHTML(w, v, true)\n}\n\nfunc encodeJSONEscapeHTML(w io.Writer, v any, esc bool) error {\n\tenc := json.NewEncoder(w)\n\tenc.SetEscapeHTML(esc)\n\treturn enc.Encode(v)\n}\n\nfunc encodeJSONEscapeHTMLIndent(w io.Writer, v any, esc bool, indent string) error {\n\tenc := json.NewEncoder(w)\n\tenc.SetEscapeHTML(esc)\n\tenc.SetIndent(\"\", indent)\n\treturn enc.Encode(v)\n}\n\nfunc decodeJSON(r io.Reader, v any) error {\n\tdec := json.NewDecoder(r)\n\n\t// Handle nopReadCloser specially to support multiple JSON objects\n\t// while preventing infinite loops\n\tif nrc, ok := r.(*nopReadCloser); ok {\n\t\t// Temporarily disable auto-reset to prevent infinite loops\n\t\toriginalReset := nrc.resetOnEOF\n\t\tnrc.resetOnEOF = false\n\t\tdefer func() { nrc.resetOnEOF = originalReset }()\n\n\t\tif err := doDecodeJSON(dec, v); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// After decoding, reset for future reads\n\t\tnrc.Reset()\n\t\treturn nil\n\t}\n\n\t// For other readers, decode multiple JSON objects as intended\n\treturn doDecodeJSON(dec, v)\n}\n\nfunc doDecodeJSON(dec *json.Decoder, v any) error {\n\t// Decode all JSON objects in the data\n\tfor range maxDecodeObjects {\n\t\tif err := dec.Decode(v); err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\treturn fmt.Errorf(\"resty: JSON decode exceeded %d objects without EOF\", maxDecodeObjects)\n}\n\nfunc encodeXML(w io.Writer, v any) error {\n\treturn xml.NewEncoder(w).Encode(v)\n}\n\nfunc decodeXML(r io.Reader, v any) error {\n\tdec := xml.NewDecoder(r)\n\tfor range maxDecodeObjects {\n\t\tif err := dec.Decode(v); err != nil {\n\t\t\tif err == io.EOF {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\treturn fmt.Errorf(\"resty: XML decode exceeded %d objects without EOF\", maxDecodeObjects)\n}\n\n// gzipReaderPool pools actual *gzip.Reader objects for reuse via Reset().\n// This avoids the allocation cost of gzip.NewReader for each decompression.\n// Thread-safety is ensured by the gzipReaderWrapper's mutex which guards access.\nvar gzipReaderPool = sync.Pool{\n\tNew: func() any {\n\t\t// Return nil; let's create reader on first use or get them from pool\n\t\treturn nil\n\t},\n}\n\n// gzipReaderWrapper wraps a pooled gzip.Reader with a mutex for safe concurrent access.\n// The mutex ensures exclusive access to the reader during Read() and state transitions.\ntype gzipReaderWrapper struct {\n\tmu *sync.Mutex\n\tr  io.ReadCloser\n\tgr *gzip.Reader\n}\n\n// acquireGzipReader gets a gzip.Reader from the pool or creates one.\n// It resets the reader for the new stream using the provided io.ReadCloser.\nfunc acquireGzipReader(r io.ReadCloser) (*gzipReaderWrapper, error) {\n\tw := &gzipReaderWrapper{\n\t\tmu: new(sync.Mutex),\n\t\tr:  r,\n\t}\n\n\tw.mu.Lock()\n\tdefer w.mu.Unlock()\n\n\t// Try to get a cached reader from the pool\n\tif cached := gzipReaderPool.Get(); cached != nil {\n\t\tw.gr = cached.(*gzip.Reader)\n\t\t// Reset the pooled reader for the new stream\n\t\tif err := w.gr.Reset(r); err != nil {\n\t\t\tgzipReaderPool.Put(w.gr) // Return to pool on reset error\n\t\t\treturn nil, err\n\t\t}\n\t} else {\n\t\t// Pool is empty, create a new reader\n\t\tgr, err := gzip.NewReader(r)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tw.gr = gr\n\t}\n\n\treturn w, nil\n}\n\n// releaseGzipReader returns the gzip reader to the pool for reuse,\n// and closes the underlying source.\nfunc releaseGzipReader(w *gzipReaderWrapper) {\n\tw.mu.Lock()\n\tdefer w.mu.Unlock()\n\n\tif w.gr != nil {\n\t\tw.gr.Reset(nopReader{}) // clear reference to the closed source before pooling\n\t\tgzipReaderPool.Put(w.gr)\n\t\tw.gr = nil\n\t}\n\tif w.r != nil {\n\t\tcloseq(w.r)\n\t\tw.r = nil\n\t}\n}\n\nfunc decompressGzip(r io.ReadCloser) (io.ReadCloser, error) {\n\treturn acquireGzipReader(r)\n}\n\n// Implement io.ReadCloser for gzipReaderWrapper\nfunc (w *gzipReaderWrapper) Read(p []byte) (n int, err error) {\n\t// Hold the lock during Read to ensure exclusive access to the gzip reader\n\tw.mu.Lock()\n\tdefer w.mu.Unlock()\n\n\tif w.gr == nil {\n\t\treturn 0, io.EOF\n\t}\n\treturn w.gr.Read(p)\n}\n\nfunc (w *gzipReaderWrapper) Close() error {\n\treleaseGzipReader(w)\n\treturn nil\n}\n\n// flateReaderPool pools io.ReadCloser (flate.Reader) objects for reuse via Reset().\n// This avoids the allocation cost of flate.NewReader for each decompression.\n// Thread-safety is ensured by the deflateReaderWrapper's mutex which guards access.\nvar flateReaderPool = sync.Pool{\n\tNew: func() any {\n\t\t// Return nil; let's create reader on first use or get them from pool\n\t\treturn nil\n\t},\n}\n\n// deflateReaderWrapper wraps a pooled flate.Reader with a mutex for safe concurrent access.\n// The mutex ensures exclusive access to the reader during Read() and state transitions.\ntype deflateReaderWrapper struct {\n\tmu *sync.Mutex\n\tr  io.ReadCloser\n\tfr io.ReadCloser\n}\n\n// acquireDeflateReader gets a flate.Reader from the pool or creates one.\n// It resets the reader for the new stream using the provided io.ReadCloser.\nfunc acquireDeflateReader(r io.ReadCloser) (*deflateReaderWrapper, error) {\n\tw := &deflateReaderWrapper{\n\t\tmu: new(sync.Mutex),\n\t\tr:  r,\n\t}\n\n\tw.mu.Lock()\n\tdefer w.mu.Unlock()\n\n\t// Try to get a cached reader from the pool\n\tif cached := flateReaderPool.Get(); cached != nil {\n\t\tw.fr = cached.(io.ReadCloser)\n\t\t// Reset the pooled reader for the new stream; flate.Resetter.Reset never errors\n\t\tw.fr.(flate.Resetter).Reset(r, nil)\n\t} else {\n\t\t// Pool is empty, create a new reader\n\t\tw.fr = flate.NewReader(r)\n\t}\n\n\treturn w, nil\n}\n\n// releaseDeflateReader returns the flate reader to the pool for reuse,\n// and closes the underlying source.\nfunc releaseDeflateReader(w *deflateReaderWrapper) {\n\tw.mu.Lock()\n\tdefer w.mu.Unlock()\n\n\tif w.fr != nil {\n\t\tw.fr.(flate.Resetter).Reset(nopReader{}, nil)\n\t\tflateReaderPool.Put(w.fr)\n\t\tw.fr = nil\n\t}\n\tif w.r != nil {\n\t\tcloseq(w.r)\n\t\tw.r = nil\n\t}\n}\n\nfunc decompressDeflate(r io.ReadCloser) (io.ReadCloser, error) {\n\treturn acquireDeflateReader(r)\n}\n\n// Implement io.ReadCloser for deflateReaderWrapper\nfunc (w *deflateReaderWrapper) Read(p []byte) (n int, err error) {\n\t// Hold the lock during Read to ensure exclusive access to the flate reader\n\tw.mu.Lock()\n\tdefer w.mu.Unlock()\n\n\tif w.fr == nil {\n\t\treturn 0, io.EOF\n\t}\n\treturn w.fr.Read(p)\n}\n\nfunc (w *deflateReaderWrapper) Close() error {\n\treleaseDeflateReader(w)\n\treturn nil\n}\n\n// ErrReadExceedsThresholdLimit is returned when the read operation exceeds the defined threshold limit.\nvar ErrReadExceedsThresholdLimit = errors.New(\"resty: read exceeds the threshold limit\")\n\nvar _ io.ReadCloser = (*limitReadCloser)(nil)\nvar _ resetter = (*limitReadCloser)(nil)\n\n// resetter is an interface that defines a Reset method for resetting the reader state.\ntype resetter interface {\n\tReset() error\n}\n\nconst unlimitedRead = 0\n\ntype limitReadCloser struct {\n\tr io.Reader\n\tl int64 // Limit (0 or <0 - unlimited, >0 limit)\n\tt int64 // Total bytes read\n\tf func(s int64)\n}\n\nfunc (l *limitReadCloser) Read(p []byte) (n int, err error) {\n\tswitch {\n\tcase l.l <= unlimitedRead:\n\t\tn, err = l.r.Read(p)\n\t\tl.t += int64(n)\n\t\tl.f(l.t)\n\t\treturn n, err\n\tdefault:\n\t\tremaining := l.l - l.t\n\t\tif remaining <= 0 {\n\t\t\treturn 0, ErrReadExceedsThresholdLimit\n\t\t}\n\t\tif remaining < int64(len(p)) {\n\t\t\tp = p[:remaining]\n\t\t}\n\t\tn, err = l.r.Read(p)\n\t\tl.t += int64(n)\n\t\tl.f(l.t)\n\t\treturn n, err\n\t}\n}\n\nfunc (l *limitReadCloser) Close() error {\n\tif c, ok := l.r.(io.Closer); ok {\n\t\treturn c.Close()\n\t}\n\treturn nil\n}\n\nfunc (l *limitReadCloser) Reset() error {\n\tl.t = 0 // Reset total bytes read to zero\n\treturn nil\n}\n\nvar _ io.ReadCloser = (*copyReadCloser)(nil)\n\ntype copyReadCloser struct {\n\ts io.Reader\n\tt *bytes.Buffer\n\tc bool\n\tf func(*bytes.Buffer)\n}\n\nfunc (r *copyReadCloser) Read(p []byte) (int, error) {\n\tn, err := r.s.Read(p)\n\tif n > 0 {\n\t\t_, _ = r.t.Write(p[:n])\n\t}\n\tif err == io.EOF || err == ErrReadExceedsThresholdLimit {\n\t\tif !r.c {\n\t\t\tr.f(r.t)\n\t\t\tr.c = true\n\t\t}\n\t}\n\treturn n, err\n}\n\nfunc (r *copyReadCloser) Close() error {\n\tif c, ok := r.s.(io.Closer); ok {\n\t\treturn c.Close()\n\t}\n\treturn nil\n}\n\nvar _ io.ReadCloser = (*nopReadCloser)(nil)\n\ntype nopReadCloser struct {\n\tr          io.Reader\n\tresetOnEOF bool // Whether to reset on EOF\n}\n\nfunc (r *nopReadCloser) Read(p []byte) (int, error) {\n\tn, err := r.r.Read(p)\n\tif err == io.EOF && r.resetOnEOF {\n\t\tr.Reset()\n\t}\n\treturn n, err\n}\n\nfunc (r *nopReadCloser) Close() error { return nil }\n\n// Reset allows manual reset of the reader position\nfunc (r *nopReadCloser) Reset() {\n\t// If the underlying reader supports seeking, reset to the beginning\n\tif seeker, ok := r.r.(io.Seeker); ok {\n\t\tseeker.Seek(0, io.SeekStart)\n\t}\n\n\t// Also try to reset underlying layer\n\tif ur, ok := r.r.(resetter); ok {\n\t\t_ = ur.Reset()\n\t}\n}\n\nvar _ flate.Reader = (*nopReader)(nil)\n\ntype nopReader struct{}\n\nfunc (nopReader) Read([]byte) (int, error) { return 0, io.EOF }\nfunc (nopReader) ReadByte() (byte, error)  { return 0, io.EOF }\n\ntype gracefulStopReader struct {\n\tctx context.Context\n\tr   io.Reader\n}\n\nfunc (gsr *gracefulStopReader) Read(p []byte) (n int, err error) {\n\tif err := gsr.ctx.Err(); err != nil {\n\t\t// Return io.EOF to stop io.Copy gracefully without an error.\n\t\treturn 0, io.EOF\n\t}\n\treturn gsr.r.Read(p)\n}\n"
  },
  {
    "path": "stream_test.go",
    "content": "package resty\n\nimport (\n\t\"bytes\"\n\t\"compress/flate\"\n\t\"compress/gzip\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"strings\"\n\t\"sync\"\n\t\"testing\"\n)\n\nfunc TestDecodeJSONWhenResponseBodyIsNull(t *testing.T) {\n\tr := &Response{\n\t\tBody: io.NopCloser(bytes.NewReader([]byte(\"null\"))),\n\t}\n\tr.wrapCopyReadCloser()\n\terr := r.readAll()\n\tassertNil(t, err)\n\n\tvar result map[int]int\n\terr = decodeJSON(r.Body, &result)\n\tassertNil(t, err)\n\tassertNil(t, result, \"expected result to be nil map when JSON is null\")\n}\n\nfunc TestGetMethodWhenResponseIsNull(t *testing.T) {\n\tserver := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Write([]byte(\"null\"))\n\t}))\n\n\tclient := New().SetRetryCount(3).SetCurlCmdGenerate(true)\n\n\tvar x any\n\tresp, err := client.R().SetBody(\"{}\").\n\t\tSetHeader(\"Content-Type\", \"application/json; charset=utf-8\").\n\t\tSetResponseForceContentType(\"application/json\").\n\t\tSetMethodGetAllowPayload(true).\n\t\tSetResponseBodyUnlimitedReads(true).\n\t\tSetResult(&x).\n\t\tGet(server.URL + \"/test\")\n\n\tassertNil(t, err)\n\tassertEqual(t, \"null\", resp.String())\n\tassertNil(t, x, \"expected result to be nil when response body is null\")\n}\n\nfunc TestDecodeJSON(t *testing.T) {\n\tt.Run(\"single object\", func(t *testing.T) {\n\t\tjsonData := `{\"name\": \"John\", \"age\": 30}`\n\t\treader := bytes.NewReader([]byte(jsonData))\n\t\tvar result map[string]any\n\t\terr := decodeJSON(reader, &result)\n\t\tassertNil(t, err)\n\t\tassertEqual(t, \"John\", result[\"name\"])\n\t\tassertEqual(t, float64(30), result[\"age\"])\n\t})\n\n\tt.Run(\"multiple objects\", func(t *testing.T) {\n\t\tmultipleJSON := `{\"id\": 1}\n{\"id\": 2}\n{\"id\": 3}`\n\t\treader2 := bytes.NewReader([]byte(multipleJSON))\n\t\tvar result2 map[string]any\n\t\terr := decodeJSON(reader2, &result2)\n\t\tassertNil(t, err)\n\t\tassertEqual(t, float64(3), result2[\"id\"])\n\t})\n\n\tt.Run(\"list of objects\", func(t *testing.T) {\n\t\tmultipleJSON := `[{\"id\": 1},\n{\"id\": 2},\n{\"id\": 3}]`\n\t\treader2 := bytes.NewReader([]byte(multipleJSON))\n\t\tvar result2 []map[string]any\n\t\terr := decodeJSON(reader2, &result2)\n\t\tassertNil(t, err)\n\t\tassertEqual(t, float64(3), result2[2][\"id\"])\n\t})\n\n\tt.Run(\"malformed JSON\", func(t *testing.T) {\n\t\tmalformedJSON := `{\"name\": \"John\", \"age\":}`\n\t\treader3 := bytes.NewReader([]byte(malformedJSON))\n\t\tvar result3 map[string]any\n\t\terr := decodeJSON(reader3, &result3)\n\t\tassertNotNil(t, err)\n\t})\n\n\tt.Run(\"empty body\", func(t *testing.T) {\n\t\temptyJSON := ``\n\t\treader4 := bytes.NewReader([]byte(emptyJSON))\n\t\tvar result4 map[string]any\n\t\terr := decodeJSON(reader4, &result4)\n\t\tassertNil(t, err)\n\t})\n\n\tt.Run(\"exceeds maxDecodeObjects limit\", func(t *testing.T) {\n\t\tpreMaxDecodeObjects := maxDecodeObjects\n\t\tmaxDecodeObjects = 51 // Set a lower limit for testing\n\t\tt.Cleanup(func() {\n\t\t\tmaxDecodeObjects = preMaxDecodeObjects // Reset to original value after test\n\t\t})\n\n\t\t// Build a reader that returns maxDecodeObjects+1 objects without EOF\n\t\t// by using a custom reader that signals no EOF until asked enough times.\n\t\t// Simplest approach: patch the limit via the loop by creating a reader\n\t\t// backed by a sufficient number of elements. We instead test the boundary\n\t\t// by constructing exactly that many elements with a streaming reader\n\t\t// built from io.MultiReader.\n\t\telem := []byte(`{\"key\": \"value\"}`)\n\t\treaders := make([]io.Reader, maxDecodeObjects+1)\n\t\tfor i := range readers {\n\t\t\treaders[i] = bytes.NewReader(elem)\n\t\t}\n\t\tr := io.MultiReader(readers...)\n\n\t\tvar v map[string]any\n\t\terr := decodeJSON(r, &v)\n\t\tassertNotNil(t, err)\n\t\tassertEqual(t, \"resty: JSON decode exceeded 51 objects without EOF\", err.Error())\n\t})\n}\n\nfunc TestWrapCopyReadCloser(t *testing.T) {\n\ttestData := \"Hello, World!\"\n\tr := &Response{\n\t\tBody: io.NopCloser(bytes.NewReader([]byte(testData))),\n\t}\n\n\t// Before wrapping, bodyBytes should be empty\n\tassertEqual(t, 0, len(r.bodyBytes))\n\n\tr.wrapCopyReadCloser()\n\n\t// Read data - should trigger copy mechanism and transform to nopReadCloser\n\tdata, err := io.ReadAll(r.Body)\n\tassertNil(t, err)\n\tassertEqual(t, testData, string(data))\n\tassertEqual(t, testData, string(r.bodyBytes))\n\n\t// Should now be nopReadCloser for unlimited reads\n\t_, ok := r.Body.(*nopReadCloser)\n\tassertTrue(t, ok, \"expected Body to be of type *nopReadCloser\")\n\n\t// Test unlimited reads\n\tdata2, err := io.ReadAll(r.Body)\n\tassertNil(t, err)\n\tassertEqual(t, testData, string(data2))\n}\n\nfunc TestMultipleJSONObjectsSupport(t *testing.T) {\n\t// Test multiple JSON objects with wrapCopyReadCloser\n\tjsonData := `{\"first\": 1}\n{\"second\": 2}\n{\"third\": 3}`\n\n\tr := &Response{\n\t\tBody: io.NopCloser(bytes.NewReader([]byte(jsonData))),\n\t}\n\tr.wrapCopyReadCloser()\n\n\t// Should process all objects and get the last one\n\tvar result map[string]any\n\terr := decodeJSON(r.Body, &result)\n\tassertNil(t, err)\n\tassertEqual(t, float64(3), result[\"third\"])\n\n\t// Should support unlimited reads and decoding\n\tvar result2 map[string]any\n\terr = decodeJSON(r.Body, &result2)\n\tassertNil(t, err)\n\tassertEqual(t, float64(3), result2[\"third\"])\n\n\t// Test direct nopReadCloser usage\n\tnopReader := &nopReadCloser{\n\t\tr:          bytes.NewReader([]byte(jsonData)),\n\t\tresetOnEOF: true,\n\t}\n\n\tvar result3 map[string]any\n\terr = decodeJSON(nopReader, &result3)\n\tassertNil(t, err)\n\tassertEqual(t, float64(3), result3[\"third\"])\n}\n\n// Test case from GH-#1087 to ensure no panic occurs\n// with gzip.Reader on corrupted gzip data when multiple\n// concurrent requests are made.\nfunc TestGzipReaderPanicOnConcurrentCorruptedBody(t *testing.T) {\n\twriteHeaders := func(w http.ResponseWriter) {\n\t\tw.Header().Set(hdrContentEncodingKey, \"gzip\")\n\t\tw.Header().Set(hdrContentTypeKey, \"application/json\")\n\t\tw.WriteHeader(http.StatusOK)\n\t}\n\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\twriteHeaders(w)\n\n\t\t// We want the Client to think it's reading Gzip, but fail immediately\n\t\t// upon processing these bytes.\n\t\tw.Write([]byte{0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x01})\n\t})\n\tdefer ts.Close()\n\n\tclient := NewWithTransportSettings(&TransportSettings{MaxIdleConns: 1000, MaxIdleConnsPerHost: 1000}).\n\t\tSetRetryCount(2).\n\t\tAddRetryConditions(func(r *Response, err error) bool {\n\t\t\treturn err != nil\n\t\t})\n\n\ttotalRequests := 100\n\tconcurrencyLimit := 100\n\tsem := make(chan struct{}, concurrencyLimit)\n\n\tpanicChan := make(chan any, 1)\n\tdoneChan := make(chan struct{})\n\n\tgo func() {\n\t\tvar wg sync.WaitGroup\n\t\tdefer close(doneChan)\n\n\t\tfor range totalRequests {\n\t\t\tselect {\n\t\t\tcase <-panicChan:\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\twg.Add(1)\n\t\t\tsem <- struct{}{}\n\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tdefer func() { <-sem }()\n\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tselect {\n\t\t\t\t\t\tcase panicChan <- r:\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}()\n\n\t\t\t\tvar out map[string]any\n\t\t\t\tclient.R().\n\t\t\t\t\tSetRetryAllowNonIdempotent(true).\n\t\t\t\t\tSetResult(&out).\n\t\t\t\t\tPost(ts.URL)\n\t\t\t}()\n\t\t}\n\t\twg.Wait()\n\t}()\n\n\tselect {\n\tcase r := <-panicChan:\n\t\tt.Errorf(\"Test Failed Immediately: Panic detected: %v\", r)\n\tcase <-doneChan:\n\t\tselect {\n\t\tcase r := <-panicChan:\n\t\t\tt.Errorf(\"Test Failed: Panic detected at end of run: %v\", r)\n\t\tdefault:\n\t\t\t// If we get here, no panic occurred.\n\t\t}\n\t}\n\n\t// at the end the client should still be functional\n\t// and can make valid requests\n\tgoodServer := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\twriteHeaders(w)\n\n\t\tgz := gzip.NewWriter(w)\n\t\tdefer gz.Close()\n\t\tgz.Write([]byte(`{\"status\": \"ok\"}`))\n\t})\n\tdefer goodServer.Close()\n\n\tvar result map[string]string\n\tres, err := client.R().\n\t\tSetResult(&result).\n\t\tPost(goodServer.URL)\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, res.StatusCode())\n\tassertEqual(t, \"ok\", result[\"status\"], \"expected to successfully decode valid gzip response\")\n}\n\nfunc TestGzipReaderAcquireAndResetError(t *testing.T) {\n\tt.Run(\"invalid data\", func(t *testing.T) {\n\t\t// Test the scenario where gzip.NewReader fails (pool empty path)\n\t\tinvalidData := io.NopCloser(bytes.NewReader([]byte(\"not gzip data\")))\n\n\t\t// This should trigger the gzip.NewReader error path\n\t\twrapper, err := acquireGzipReader(invalidData)\n\t\tassertNotNil(t, err)\n\t\tassertNil(t, wrapper)\n\t\tassertTrue(t, strings.Contains(err.Error(), \"gzip\") ||\n\t\t\tstrings.Contains(err.Error(), \"header\") ||\n\t\t\tstrings.Contains(err.Error(), \"invalid\"),\n\t\t\t\"expected gzip-related error, got: \"+err.Error())\n\t})\n\n\tt.Run(\"reset error\", func(t *testing.T) {\n\t\t// Test the scenario where Reset fails (pool hit path)\n\t\tvalidData := io.NopCloser(bytes.NewReader(createGzipValidData()))\n\n\t\t// First acquire to populate the pool\n\t\twrapper, err := acquireGzipReader(validData)\n\t\tassertNil(t, err)\n\t\tassertNotNil(t, wrapper)\n\t\treleaseGzipReader(wrapper)\n\n\t\terrorReader := &brokenReadCloser{}\n\n\t\t// Now acquire again with a broken reader to trigger Reset error on pool-hit path\n\t\twrapper2, err := acquireGzipReader(errorReader)\n\t\tassertNotNil(t, err)\n\t\tassertNil(t, wrapper2)\n\t\tassertTrue(t, strings.Contains(err.Error(), \"read error\"))\n\t})\n}\n\nfunc TestGzipReaderPoolConcurrentAccess(t *testing.T) {\n\t// Test concurrent pool access to ensure thread safety\n\n\tconst numGoroutines = 10\n\tconst numOperations = 5\n\n\tvar wg sync.WaitGroup\n\twg.Add(numGoroutines)\n\n\tfor range numGoroutines {\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\tfor range numOperations {\n\t\t\t\t// Create fresh data for each operation\n\t\t\t\tvalidData := io.NopCloser(bytes.NewReader(createGzipValidData()))\n\t\t\t\twrapper, err := acquireGzipReader(validData)\n\t\t\t\tassertNil(t, err)\n\t\t\t\tassertNotNil(t, wrapper)\n\n\t\t\t\t// Use the reader briefly\n\t\t\t\t_, err = wrapper.gr.Read(make([]byte, 5))\n\t\t\t\tassertNil(t, err)\n\n\t\t\t\t// Release back to pool\n\t\t\t\treleaseGzipReader(wrapper)\n\t\t\t}\n\t\t}()\n\t}\n\n\twg.Wait()\n}\n\n// Helper functions for testing\n\nfunc createGzipValidData() []byte {\n\tvar buf bytes.Buffer\n\tzw := gzip.NewWriter(&buf)\n\tzw.Write([]byte(\"test data\"))\n\tzw.Close()\n\treturn buf.Bytes()\n}\n\nfunc createDeflateValidData() []byte {\n\tvar buf bytes.Buffer\n\tzw, _ := flate.NewWriter(&buf, flate.BestSpeed)\n\tzw.Write([]byte(\"test data\"))\n\tzw.Close()\n\treturn buf.Bytes()\n}\n\n// Test case to ensure no panic occurs with flate.Reader on corrupted deflate data\n// when multiple concurrent requests are made.\nfunc TestDeflateReaderPanicOnConcurrentCorruptedBody(t *testing.T) {\n\twriteHeaders := func(w http.ResponseWriter) {\n\t\tw.Header().Set(hdrContentEncodingKey, \"deflate\")\n\t\tw.Header().Set(hdrContentTypeKey, \"application/json\")\n\t\tw.WriteHeader(http.StatusOK)\n\t}\n\n\tts := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\twriteHeaders(w)\n\t\t// Send bytes that are not valid deflate data to force a read error.\n\t\tw.Write([]byte{0xde, 0xad, 0xbe, 0xef, 0x00, 0x01, 0x02, 0x03})\n\t})\n\tdefer ts.Close()\n\n\tclient := NewWithTransportSettings(&TransportSettings{MaxIdleConns: 1000, MaxIdleConnsPerHost: 1000}).\n\t\tSetRetryCount(2).\n\t\tAddRetryConditions(func(r *Response, err error) bool {\n\t\t\treturn err != nil\n\t\t})\n\n\ttotalRequests := 100\n\tconcurrencyLimit := 100\n\tsem := make(chan struct{}, concurrencyLimit)\n\n\tpanicChan := make(chan any, 1)\n\tdoneChan := make(chan struct{})\n\n\tgo func() {\n\t\tvar wg sync.WaitGroup\n\t\tdefer close(doneChan)\n\n\t\tfor range totalRequests {\n\t\t\tselect {\n\t\t\tcase <-panicChan:\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\twg.Add(1)\n\t\t\tsem <- struct{}{}\n\n\t\t\tgo func() {\n\t\t\t\tdefer wg.Done()\n\t\t\t\tdefer func() { <-sem }()\n\n\t\t\t\tdefer func() {\n\t\t\t\t\tif r := recover(); r != nil {\n\t\t\t\t\t\tselect {\n\t\t\t\t\t\tcase panicChan <- r:\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}()\n\n\t\t\t\tvar out map[string]any\n\t\t\t\tclient.R().\n\t\t\t\t\tSetRetryAllowNonIdempotent(true).\n\t\t\t\t\tSetResult(&out).\n\t\t\t\t\tPost(ts.URL)\n\t\t\t}()\n\t\t}\n\t\twg.Wait()\n\t}()\n\n\tselect {\n\tcase r := <-panicChan:\n\t\tt.Errorf(\"Test Failed Immediately: Panic detected: %v\", r)\n\tcase <-doneChan:\n\t\tselect {\n\t\tcase r := <-panicChan:\n\t\t\tt.Errorf(\"Test Failed: Panic detected at end of run: %v\", r)\n\t\tdefault:\n\t\t\t// If we get here, no panic occurred.\n\t\t}\n\t}\n\n\t// at the end the client should still be functional\n\t// and can make valid requests\n\tgoodServer := createTestServer(func(w http.ResponseWriter, r *http.Request) {\n\t\twriteHeaders(w)\n\t\tzw, _ := flate.NewWriter(w, flate.BestSpeed)\n\t\tdefer zw.Close()\n\t\tzw.Write([]byte(`{\"status\": \"ok\"}`))\n\t})\n\tdefer goodServer.Close()\n\n\tvar result map[string]string\n\tres, err := client.R().\n\t\tSetResult(&result).\n\t\tPost(goodServer.URL)\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, res.StatusCode())\n\tassertEqual(t, \"ok\", result[\"status\"], \"expected to successfully decode valid deflate response\")\n}\n\nfunc TestDeflateReaderPoolAcquireAndRead(t *testing.T) {\n\t// Test successful creation and read with valid deflate data\n\tvalidData := io.NopCloser(bytes.NewReader(createDeflateValidData()))\n\twrapper, err := acquireDeflateReader(validData)\n\tassertNil(t, err)\n\tassertNotNil(t, wrapper)\n\n\tbuf := make([]byte, 128)\n\t// flate.Reader may return (n, io.EOF) in the same call on the final read; ignore it.\n\tn, _ := wrapper.Read(buf)\n\tassertTrue(t, n > 0, \"expected to read some bytes from valid deflate data\")\n\tassertEqual(t, \"test data\", strings.TrimRight(string(buf[:n]), \"\\x00\"))\n\n\twrapper.Close()\n\n\t// Test that Read on a closed wrapper returns io.EOF\n\t_, err = wrapper.Read(buf)\n\tassertEqual(t, io.EOF, err)\n}\n\nfunc TestDeflateReaderPoolConcurrentAccess(t *testing.T) {\n\t// Test concurrent pool access to ensure thread safety\n\tconst numGoroutines = 10\n\tconst numOperations = 5\n\n\tvar wg sync.WaitGroup\n\twg.Add(numGoroutines)\n\n\tfor range numGoroutines {\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\tfor range numOperations {\n\t\t\t\t// Create fresh data for each operation\n\t\t\t\tvalidData := io.NopCloser(bytes.NewReader(createDeflateValidData()))\n\t\t\t\twrapper, err := acquireDeflateReader(validData)\n\t\t\t\tassertNil(t, err)\n\t\t\t\tassertNotNil(t, wrapper)\n\n\t\t\t\t// Use the reader briefly\n\t\t\t\t_, err = wrapper.fr.Read(make([]byte, 5))\n\t\t\t\tassertNil(t, err)\n\n\t\t\t\t// Release back to pool\n\t\t\t\treleaseDeflateReader(wrapper)\n\t\t\t}\n\t\t}()\n\t}\n\n\twg.Wait()\n}\n\nfunc TestLimitCloserResetterInterface(t *testing.T) {\n\ttestStr := \"This is limit reset test\"\n\ttestStrLen := int64(len(testStr))\n\tr := bytes.NewReader([]byte(testStr))\n\tlc := &limitReadCloser{\n\t\tr: r,\n\t\tl: testStrLen,\n\t\tf: func(total int64) {},\n\t}\n\tassertEqual(t, testStrLen, lc.l)\n\n\trc := nopReadCloser{r: lc, resetOnEOF: true}\n\trc.Read(make([]byte, 25)) // read to reach total size\n\tassertEqual(t, testStrLen, lc.l)\n\tassertEqual(t, testStrLen, lc.t)\n\n\trc.Reset() // reset should change the total to 0\n\tassertEqual(t, int64(0), lc.t)\n}\n\nfunc TestDecodeXML(t *testing.T) {\n\ttype Item struct {\n\t\tName string `xml:\"name\"`\n\t}\n\n\tt.Run(\"single object\", func(t *testing.T) {\n\t\tdata := `<Item><name>foo</name></Item>`\n\t\tvar v Item\n\t\terr := decodeXML(bytes.NewReader([]byte(data)), &v)\n\t\tassertNil(t, err)\n\t\tassertEqual(t, \"foo\", v.Name)\n\t})\n\n\tt.Run(\"multiple objects - last one wins\", func(t *testing.T) {\n\t\tdata := `<Item><name>first</name></Item><Item><name>last</name></Item>`\n\t\tvar v Item\n\t\terr := decodeXML(bytes.NewReader([]byte(data)), &v)\n\t\tassertNil(t, err)\n\t\tassertEqual(t, \"last\", v.Name)\n\t})\n\n\tt.Run(\"malformed XML returns error\", func(t *testing.T) {\n\t\tdata := `<Item><name>broken</name>`\n\t\tvar v Item\n\t\terr := decodeXML(bytes.NewReader([]byte(data)), &v)\n\t\tassertNotNil(t, err)\n\t})\n\n\tt.Run(\"exceeds maxDecodeObjects limit\", func(t *testing.T) {\n\t\tpreMaxDecodeObjects := maxDecodeObjects\n\t\tmaxDecodeObjects = 51 // Set a lower limit for testing\n\t\tt.Cleanup(func() {\n\t\t\tmaxDecodeObjects = preMaxDecodeObjects // Reset to original value after test\n\t\t})\n\n\t\t// Build a reader that returns maxDecodeObjects+1 objects without EOF\n\t\t// by using a custom reader that signals no EOF until asked enough times.\n\t\t// Simplest approach: patch the limit via the loop by creating a reader\n\t\t// backed by a sufficient number of elements. We instead test the boundary\n\t\t// by constructing exactly that many elements with a streaming reader\n\t\t// built from io.MultiReader.\n\t\telem := []byte(`<Item><name>x</name></Item>`)\n\t\treaders := make([]io.Reader, maxDecodeObjects+1)\n\t\tfor i := range readers {\n\t\t\treaders[i] = bytes.NewReader(elem)\n\t\t}\n\t\tr := io.MultiReader(readers...)\n\n\t\tvar v Item\n\t\terr := decodeXML(r, &v)\n\t\tassertNotNil(t, err)\n\t\tassertEqual(t, \"resty: XML decode exceeded 51 objects without EOF\", err.Error())\n\t})\n}\n\nfunc TestStreamMisc(t *testing.T) {\n\tt.Run(\"wrapper gzip reader is nil\", func(t *testing.T) {\n\t\t// Simulate a scenario where gzip.NewReader returns a wrapper with nil gr\n\t\t// due to an error, and ensure that Read on the wrapper does not panic\n\t\t// and returns an appropriate error instead.\n\t\tgzipReader := &gzipReaderWrapper{mu: new(sync.Mutex)}\n\t\tn, err := gzipReader.Read(make([]byte, 5))\n\t\tassertNotNil(t, err)\n\t\tassertErrorIs(t, io.EOF, err)\n\t\tassertEqual(t, 0, n)\n\n\t})\n}\n"
  },
  {
    "path": "trace.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net/http/httptrace\"\n\t\"sync\"\n\t\"time\"\n)\n\n// TraceInfo struct is used to provide request trace info such as DNS lookup\n// duration, Connection obtain duration, Server processing duration, etc.\ntype TraceInfo struct {\n\t// DNSLookup is the duration that transport took to perform\n\t// DNS lookup.\n\tDNSLookup time.Duration `json:\"dns_lookup_time\"`\n\n\t// ConnTime is the duration it took to obtain a successful connection.\n\tConnTime time.Duration `json:\"connection_time\"`\n\n\t// TCPConnTime is the duration it took to obtain the TCP connection.\n\tTCPConnTime time.Duration `json:\"tcp_connection_time\"`\n\n\t// TLSHandshake is the duration of the TLS handshake.\n\tTLSHandshake time.Duration `json:\"tls_handshake_time\"`\n\n\t// ServerTime is the server's duration for responding to the first byte.\n\tServerTime time.Duration `json:\"server_time\"`\n\n\t// ResponseTime is the duration since the first response byte from the server to\n\t// request completion.\n\tResponseTime time.Duration `json:\"response_time\"`\n\n\t// TotalTime is the duration of the total time request taken end-to-end.\n\tTotalTime time.Duration `json:\"total_time\"`\n\n\t// IsConnReused is whether this connection has been previously\n\t// used for another HTTP request.\n\tIsConnReused bool `json:\"is_connection_reused\"`\n\n\t// IsConnWasIdle is whether this connection was obtained from an\n\t// idle pool.\n\tIsConnWasIdle bool `json:\"is_connection_was_idle\"`\n\n\t// ConnIdleTime is the duration how long the connection that was previously\n\t// idle, if IsConnWasIdle is true.\n\tConnIdleTime time.Duration `json:\"connection_idle_time\"`\n\n\t// RequestAttempt is to represent the request attempt made during a Resty\n\t// request execution flow, including retry count.\n\tRequestAttempt int `json:\"request_attempt\"`\n\n\t// RemoteAddr returns the remote network address.\n\tRemoteAddr string `json:\"remote_address\"`\n}\n\n// String method returns string representation of request trace information.\nfunc (ti TraceInfo) String() string {\n\treturn fmt.Sprintf(`TRACE INFO:\n  DNSLookupTime : %v\n  ConnTime      : %v\n  TCPConnTime   : %v\n  TLSHandshake  : %v\n  ServerTime    : %v\n  ResponseTime  : %v\n  TotalTime     : %v\n  IsConnReused  : %v\n  IsConnWasIdle : %v\n  ConnIdleTime  : %v\n  RequestAttempt: %v\n  RemoteAddr    : %v`, ti.DNSLookup, ti.ConnTime, ti.TCPConnTime,\n\t\tti.TLSHandshake, ti.ServerTime, ti.ResponseTime, ti.TotalTime,\n\t\tti.IsConnReused, ti.IsConnWasIdle, ti.ConnIdleTime, ti.RequestAttempt,\n\t\tti.RemoteAddr)\n}\n\n// JSON method returns the JSON string of request trace information\nfunc (ti TraceInfo) JSON() string {\n\treturn toJSON(ti)\n}\n\n// Clone method returns the clone copy of [TraceInfo]\nfunc (ti TraceInfo) Clone() *TraceInfo {\n\tti2 := new(TraceInfo)\n\t*ti2 = ti\n\treturn ti2\n}\n\n// clientTrace struct maps the [httptrace.ClientTrace] hooks into Fields\n// with the same naming for easy understanding. Plus additional insights\n// [Request].\ntype clientTrace struct {\n\tlock                 sync.RWMutex\n\tgetConn              time.Time\n\tdnsStart             time.Time\n\tdnsDone              time.Time\n\tconnectDone          time.Time\n\ttlsHandshakeStart    time.Time\n\ttlsHandshakeDone     time.Time\n\tgotConn              time.Time\n\tgotFirstResponseByte time.Time\n\tendTime              time.Time\n\tgotConnInfo          httptrace.GotConnInfo\n}\n\nfunc (t *clientTrace) createContext(ctx context.Context) context.Context {\n\treturn httptrace.WithClientTrace(\n\t\tctx,\n\t\t&httptrace.ClientTrace{\n\t\t\tDNSStart: func(_ httptrace.DNSStartInfo) {\n\t\t\t\tt.lock.Lock()\n\t\t\t\tt.dnsStart = time.Now()\n\t\t\t\tt.lock.Unlock()\n\t\t\t},\n\t\t\tDNSDone: func(_ httptrace.DNSDoneInfo) {\n\t\t\t\tt.lock.Lock()\n\t\t\t\tt.dnsDone = time.Now()\n\t\t\t\tt.lock.Unlock()\n\t\t\t},\n\t\t\tConnectStart: func(_, _ string) {\n\t\t\t\tt.lock.Lock()\n\t\t\t\tif t.dnsDone.IsZero() {\n\t\t\t\t\tt.dnsDone = time.Now()\n\t\t\t\t}\n\t\t\t\tif t.dnsStart.IsZero() {\n\t\t\t\t\tt.dnsStart = t.dnsDone\n\t\t\t\t}\n\t\t\t\tt.lock.Unlock()\n\t\t\t},\n\t\t\tConnectDone: func(net, addr string, err error) {\n\t\t\t\tt.lock.Lock()\n\t\t\t\tt.connectDone = time.Now()\n\t\t\t\tt.lock.Unlock()\n\t\t\t},\n\t\t\tGetConn: func(_ string) {\n\t\t\t\tt.lock.Lock()\n\t\t\t\tt.getConn = time.Now()\n\t\t\t\tt.lock.Unlock()\n\t\t\t},\n\t\t\tGotConn: func(ci httptrace.GotConnInfo) {\n\t\t\t\tt.lock.Lock()\n\t\t\t\tt.gotConn = time.Now()\n\t\t\t\tt.gotConnInfo = ci\n\t\t\t\tt.lock.Unlock()\n\t\t\t},\n\t\t\tGotFirstResponseByte: func() {\n\t\t\t\tt.lock.Lock()\n\t\t\t\tt.gotFirstResponseByte = time.Now()\n\t\t\t\tt.lock.Unlock()\n\t\t\t},\n\t\t\tTLSHandshakeStart: func() {\n\t\t\t\tt.lock.Lock()\n\t\t\t\tt.tlsHandshakeStart = time.Now()\n\t\t\t\tt.lock.Unlock()\n\t\t\t},\n\t\t\tTLSHandshakeDone: func(_ tls.ConnectionState, _ error) {\n\t\t\t\tt.lock.Lock()\n\t\t\t\tt.tlsHandshakeDone = time.Now()\n\t\t\t\tt.lock.Unlock()\n\t\t\t},\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "transport_dial.go",
    "content": "// Copyright 2021 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n//go:build !(js && wasm)\n// +build !js !wasm\n\npackage resty\n\nimport (\n\t\"context\"\n\t\"net\"\n)\n\nfunc transportDialContext(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error) {\n\treturn dialer.DialContext\n}\n"
  },
  {
    "path": "transport_dial_wasm.go",
    "content": "// Copyright 2021 The Go Authors. All rights reserved.\n// Use of this source code is governed by a BSD-style\n// license that can be found in the LICENSE file.\n\n//go:build (js && wasm) || wasip1\n// +build js,wasm wasip1\n\npackage resty\n\nimport (\n\t\"context\"\n\t\"net\"\n)\n\nfunc transportDialContext(_ *net.Dialer) func(context.Context, string, string) (net.Conn, error) {\n\treturn nil\n}\n"
  },
  {
    "path": "util.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bytes\"\n\t\"crypto/rand\"\n\t\"crypto/sha256\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"encoding/xml\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\n//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\n// Logger interface\n//_______________________________________________________________________\n\n// Logger interface is to abstract the logging from Resty. Gives control to\n// the Resty users, choice of the logger.\ntype Logger interface {\n\tErrorf(format string, v ...any)\n\tWarnf(format string, v ...any)\n\tDebugf(format string, v ...any)\n}\n\nfunc createLogger() *logger {\n\tl := &logger{l: log.New(os.Stderr, \"\", log.Ldate|log.Lmicroseconds)}\n\treturn l\n}\n\nvar _ Logger = (*logger)(nil)\n\ntype logger struct {\n\tl *log.Logger\n}\n\nfunc (l *logger) Errorf(format string, v ...any) {\n\tl.output(\"ERROR RESTY \"+format, v...)\n}\n\nfunc (l *logger) Warnf(format string, v ...any) {\n\tl.output(\"WARN RESTY \"+format, v...)\n}\n\nfunc (l *logger) Debugf(format string, v ...any) {\n\tl.output(\"DEBUG RESTY \"+format, v...)\n}\n\nfunc (l *logger) output(format string, v ...any) {\n\tif len(v) == 0 {\n\t\tl.l.Print(format)\n\t\treturn\n\t}\n\tl.l.Printf(format, v...)\n}\n\n//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\n// In Memory JSON & XML Marshal and Unmarshal using Go package\n//_____________________________________________________________\n\nvar (\n\t// InMemoryJSONMarshal function performs the JSON marshalling completely in memory.\n\t//\n\t//\tc := resty.New()\n\t//\tdefer c.Close()\n\t//\n\t//\tc.AddContentTypeEncoder(\"application/json\", resty.InMemoryJSONMarshal)\n\tInMemoryJSONMarshal = func(w io.Writer, v any) error {\n\t\tjsonData, err := json.Marshal(v)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_, err = w.Write(jsonData)\n\t\treturn err\n\t}\n\n\t// InMemoryJSONUnmarshal function performs the JSON unmarshalling completely in memory.\n\t//\n\t//\tc := resty.New()\n\t//\tdefer c.Close()\n\t//\n\t//\tc.AddContentTypeDecoder(\"application/json\", resty.InMemoryJSONUnmarshal)\n\tInMemoryJSONUnmarshal = func(r io.Reader, v any) error {\n\t\tbyteData, err := io.ReadAll(r)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn json.Unmarshal(byteData, v)\n\t}\n\n\t// InMemoryXMLMarshal function performs the XML marshalling completely in memory.\n\t//\n\t//\tc := resty.New()\n\t//\tdefer c.Close()\n\t//\n\t//\tc.AddContentTypeEncoder(\"application/xml\", resty.InMemoryXMLMarshal)\n\tInMemoryXMLMarshal = func(w io.Writer, v any) error {\n\t\txmlData, err := xml.Marshal(v)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\t_, err = w.Write(xmlData)\n\t\treturn err\n\t}\n\n\t// InMemoryJSONUnmarshal function performs the XML unmarshalling completely in memory.\n\t//\n\t//\tc := resty.New()\n\t//\tdefer c.Close()\n\t//\n\t//\tc.AddContentTypeDecoder(\"application/xml\", resty.InMemoryXMLUnmarshal)\n\tInMemoryXMLUnmarshal = func(r io.Reader, v any) error {\n\t\tbyteData, err := io.ReadAll(r)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\treturn xml.Unmarshal(byteData, v)\n\t}\n)\n\n// credentials type is to hold an username and password information\ntype credentials struct {\n\tUsername string `json:\"username\"`\n\tPassword string `json:\"password\"`\n}\n\n// Clone method returns clone of c.\nfunc (c *credentials) Clone() *credentials {\n\tcc := new(credentials)\n\t*cc = *c\n\treturn cc\n}\n\n// String method returns masked value of username and password\nfunc (c credentials) String() string {\n\treturn \"Username: **********, Password: **********\"\n}\n\n//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\n// Package Helper methods\n//_______________________________________________________________________\n\n// isStringEmpty method tells whether given string is empty or not\nfunc isStringEmpty(str string) bool {\n\treturn len(strings.TrimSpace(str)) == 0\n}\n\n// detectContentType method is used to figure out `Request.Body` content type for request header\nfunc detectContentType(body any) string {\n\tcontentType := plainTextType\n\tkind := inferKind(body)\n\tswitch kind {\n\tcase reflect.Struct, reflect.Map:\n\t\tcontentType = jsonContentType\n\tcase reflect.String:\n\t\tcontentType = plainTextType\n\tdefault:\n\t\tif b, ok := body.([]byte); ok {\n\t\t\tcontentType = http.DetectContentType(b)\n\t\t} else if kind == reflect.Slice { // check slice here to differentiate between any slice vs byte slice\n\t\t\tcontentType = jsonContentType\n\t\t}\n\t}\n\n\treturn contentType\n}\n\nfunc isJSONContentType(ct string) bool {\n\treturn strings.Contains(ct, jsonKey)\n}\n\nfunc isXMLContentType(ct string) bool {\n\treturn strings.Contains(ct, xmlKey)\n}\n\nfunc inferContentTypeMapKey(v string) string {\n\tif isJSONContentType(v) {\n\t\treturn jsonKey\n\t} else if isXMLContentType(v) {\n\t\treturn xmlKey\n\t}\n\treturn \"\"\n}\n\nfunc firstNonEmpty(v ...string) string {\n\tfor _, s := range v {\n\t\tif !isStringEmpty(s) {\n\t\t\treturn s\n\t\t}\n\t}\n\treturn \"\"\n}\n\nvar (\n\tmkdirAll   = os.MkdirAll\n\tcreateFile = os.Create\n\tioCopy     = io.Copy\n)\n\nfunc createDirectory(dir string) (err error) {\n\tif _, err = os.Stat(dir); err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\tif err = mkdirAll(dir, 0755); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\treturn\n}\n\nfunc getPointer(v any) any {\n\tif v == nil {\n\t\treturn nil\n\t}\n\tvv := reflect.ValueOf(v)\n\tif vv.Kind() == reflect.Ptr {\n\t\treturn v\n\t}\n\treturn reflect.New(vv.Type()).Interface()\n}\n\nfunc inferType(v any) reflect.Type {\n\treturn reflect.Indirect(reflect.ValueOf(v)).Type()\n}\n\nfunc inferKind(v any) reflect.Kind {\n\treturn inferType(v).Kind()\n}\n\nfunc newInterface(v any) any {\n\tif v == nil {\n\t\treturn nil\n\t}\n\treturn reflect.New(inferType(v)).Interface()\n}\n\nfunc functionName(i any) string {\n\treturn runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()\n}\n\nfunc acquireBuffer() *bytes.Buffer {\n\tbuf := bufPool.Get().(*bytes.Buffer)\n\tif buf.Len() == 0 {\n\t\tbuf.Reset()\n\t\treturn buf\n\t}\n\tbufPool.Put(buf)\n\treturn new(bytes.Buffer)\n}\n\nfunc releaseBuffer(buf *bytes.Buffer) {\n\tif buf != nil {\n\t\tbuf.Reset()\n\t\tbufPool.Put(buf)\n\t}\n}\n\nfunc backToBufPool(buf *bytes.Buffer) {\n\tif buf != nil {\n\t\tbufPool.Put(buf)\n\t}\n}\n\nfunc closeq(v any) {\n\tif c, ok := v.(io.Closer); ok {\n\t\tsilently(c.Close())\n\t}\n}\n\nfunc silently(_ ...any) {}\n\nvar sanitizeHeaderToken = []string{\n\t\"authorization\",\n\t\"auth\",\n\t\"token\",\n}\n\nfunc isSanitizeHeader(k string) bool {\n\tkk := strings.ToLower(k)\n\tfor _, v := range sanitizeHeaderToken {\n\t\tif strings.Contains(kk, v) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc sanitizeHeaders(hdr http.Header) http.Header {\n\tfor k := range hdr {\n\t\tif isSanitizeHeader(k) {\n\t\t\thdr[k] = []string{\"********************\"}\n\t\t}\n\t}\n\treturn hdr\n}\n\nfunc composeHeaders(hdr http.Header) string {\n\tstr := make([]string, 0, len(hdr))\n\tfor _, k := range sortHeaderKeys(hdr) {\n\t\tstr = append(str, \"\\t\"+strings.TrimSpace(fmt.Sprintf(\"%25s: %s\", k, strings.Join(hdr[k], \", \"))))\n\t}\n\treturn strings.Join(str, \"\\n\")\n}\n\nfunc sortHeaderKeys(hdr http.Header) []string {\n\tkeys := make([]string, 0, len(hdr))\n\tfor key := range hdr {\n\t\tkeys = append(keys, key)\n\t}\n\tsort.Strings(keys)\n\treturn keys\n}\n\nfunc wrapErrors(n error, inner error) error {\n\tif n == nil && inner == nil {\n\t\treturn nil\n\t}\n\tif inner == nil {\n\t\treturn n\n\t}\n\tif n == nil {\n\t\treturn inner\n\t}\n\treturn &restyError{\n\t\terr:   n,\n\t\tinner: inner,\n\t}\n}\n\ntype restyError struct {\n\terr   error\n\tinner error\n}\n\nfunc (e *restyError) Error() string {\n\treturn e.err.Error()\n}\n\nfunc (e *restyError) Unwrap() error {\n\treturn e.inner\n}\n\n// cloneURLValues is a helper function to deep copy url.Values.\nfunc cloneURLValues(v url.Values) url.Values {\n\tif v == nil {\n\t\treturn nil\n\t}\n\treturn url.Values(http.Header(v).Clone())\n}\n\nfunc cloneCookie(c *http.Cookie) *http.Cookie {\n\treturn &http.Cookie{\n\t\tName:       c.Name,\n\t\tValue:      c.Value,\n\t\tPath:       c.Path,\n\t\tDomain:     c.Domain,\n\t\tExpires:    c.Expires,\n\t\tRawExpires: c.RawExpires,\n\t\tMaxAge:     c.MaxAge,\n\t\tSecure:     c.Secure,\n\t\tHttpOnly:   c.HttpOnly,\n\t\tSameSite:   c.SameSite,\n\t\tRaw:        c.Raw,\n\t\tUnparsed:   c.Unparsed,\n\t}\n}\n\ntype invalidRequestError struct {\n\tErr error\n}\n\nfunc (ire *invalidRequestError) Error() string {\n\treturn ire.Err.Error()\n}\n\nfunc drainBody(res *Response) {\n\tif res != nil && res.Body != nil {\n\t\tdrainReadCloser(res.Body)\n\t}\n}\n\nfunc drainReadCloser(body io.ReadCloser) {\n\tif body != nil {\n\t\tdefer closeq(body)\n\t\t_, _ = io.Copy(io.Discard, body)\n\t}\n}\n\nfunc toJSON(v any) string {\n\tbuf := acquireBuffer()\n\tdefer releaseBuffer(buf)\n\t_ = encodeJSON(buf, v)\n\treturn buf.String()\n}\n\n// formatAnyToString converts various types of values to their string representation\n// based on predefined formatting rules.\nfunc formatAnyToString(value any) string {\n\tswitch v := value.(type) {\n\n\t// Tier 1: most common URL types\n\tcase string:\n\t\treturn v\n\tcase int:\n\t\treturn strconv.Itoa(v)\n\tcase bool:\n\t\treturn strconv.FormatBool(v)\n\tcase int64:\n\t\treturn strconv.FormatInt(v, 10)\n\tcase []string:\n\t\treturn strings.Join(v, \",\")\n\n\t// Tier 2: common stdlib types\n\tcase time.Time:\n\t\treturn v.Format(time.RFC3339)\n\tcase []byte:\n\t\treturn string(v)\n\tcase float64:\n\t\treturn strconv.FormatFloat(v, 'f', -1, 64)\n\n\t// Tier 3: less common integers (signed)\n\tcase int32:\n\t\treturn strconv.FormatInt(int64(v), 10)\n\tcase int16:\n\t\treturn strconv.FormatInt(int64(v), 10)\n\tcase int8:\n\t\treturn strconv.FormatInt(int64(v), 10)\n\n\t// Tier 4: less common integers (unsigned)\n\tcase uint64:\n\t\treturn strconv.FormatUint(v, 10)\n\tcase uint32:\n\t\treturn strconv.FormatUint(uint64(v), 10)\n\tcase uint16:\n\t\treturn strconv.FormatUint(uint64(v), 10)\n\tcase uint8:\n\t\treturn strconv.FormatUint(uint64(v), 10)\n\tcase uint:\n\t\treturn strconv.FormatUint(uint64(v), 10)\n\n\t// Tier 5: rare types and fallbacks\n\tcase float32:\n\t\treturn strconv.FormatFloat(float64(v), 'f', -1, 32)\n\tcase fmt.Stringer:\n\t\treturn v.String()\n\tdefault:\n\t\treturn fmt.Sprint(v)\n\t}\n}\n\n//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾\n// GUID generation\n// Code inspired from mgo/bson ObjectId\n// Code obtained from https://github.com/go-aah/aah/blob/edge/essentials/guid.go\n//___________________________________\n\nvar (\n\t// guidCounter is atomically incremented when generating a new GUID\n\t// using UniqueID() function. It's used as a counter part of an id.\n\tguidCounter = readRandomUint32()\n\n\t// machineID stores machine id generated once and used in subsequent calls\n\t// to UniqueId function.\n\tmachineID = readMachineID()\n\n\t// processID is current Process Id\n\tprocessID = os.Getpid()\n)\n\n// newGUID method returns a new Globally Unique Identifier (GUID).\n//\n// The 12-byte `UniqueId` consists of-\n//   - 4-byte value representing the seconds since the Unix epoch,\n//   - 3-byte machine identifier,\n//   - 2-byte process id, and\n//   - 3-byte counter, starting with a random value.\n//\n// Uses Mongo Object ID algorithm to generate globally unique ids -\n// https://docs.mongodb.com/manual/reference/method/ObjectId/\nfunc newGUID() string {\n\tvar b [12]byte\n\t// Timestamp, 4 bytes, big endian\n\tbinary.BigEndian.PutUint32(b[:], uint32(time.Now().Unix()))\n\n\t// Machine, first 3 bytes of sha256.Sum256([]byte(hostname))\n\tb[4], b[5], b[6] = machineID[0], machineID[1], machineID[2]\n\n\t// Pid, 2 bytes, specs don't specify endianness, but we use big endian.\n\tb[7], b[8] = byte(processID>>8), byte(processID)\n\n\t// Increment, 3 bytes, big endian\n\ti := atomic.AddUint32(&guidCounter, 1)\n\tb[9], b[10], b[11] = byte(i>>16), byte(i>>8), byte(i)\n\n\treturn hex.EncodeToString(b[:])\n}\n\nvar ioReadFull = io.ReadFull\n\n// readRandomUint32 returns a random guidCounter.\nfunc readRandomUint32() uint32 {\n\tvar b [4]byte\n\tif _, err := ioReadFull(rand.Reader, b[:]); err == nil {\n\t\treturn (uint32(b[0]) << 0) | (uint32(b[1]) << 8) | (uint32(b[2]) << 16) | (uint32(b[3]) << 24)\n\t}\n\n\t// To initialize package unexported variable 'guidCounter'.\n\t// This panic would happen at program startup, so no worries at runtime panic.\n\tpanic(errors.New(\"resty - guid: unable to generate random object id\"))\n}\n\nvar osHostname = os.Hostname\n\n// readMachineID generates and returns a machine id.\n// If this function fails to get the hostname it will cause a runtime error.\nfunc readMachineID() []byte {\n\tconst idSize = 3\n\tid := make([]byte, idSize)\n\n\tif hostname, err := osHostname(); err == nil {\n\t\thash := sha256.Sum256([]byte(hostname))\n\t\tcopy(id, hash[:idSize])\n\t\treturn id\n\t}\n\n\tif _, err := ioReadFull(rand.Reader, id); err == nil {\n\t\treturn id\n\t}\n\n\t// To initialize package unexported variable 'machineID'.\n\t// This panic would happen at program startup, so no worries at runtime panic.\n\tpanic(errors.New(\"resty - guid: unable to get hostname and random bytes\"))\n}\n"
  },
  {
    "path": "util_test.go",
    "content": "// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com), All rights reserved.\n// resty source code and usage is governed by a MIT style\n// license that can be found in the LICENSE file.\n// SPDX-License-Identifier: MIT\n\npackage resty\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n)\n\nfunc TestIsJSONContentType(t *testing.T) {\n\tfor _, test := range []struct {\n\t\tinput  string\n\t\texpect bool\n\t}{\n\t\t{\"application/json\", true},\n\t\t{\"application/xml+json\", true},\n\t\t{\"application/vnd.foo+json\", true},\n\n\t\t{\"application/json; charset=utf-8\", true},\n\t\t{\"application/vnd.foo+json; charset=utf-8\", true},\n\n\t\t{\"text/json\", true},\n\t\t{\"text/vnd.foo+json\", true},\n\n\t\t{\"application/foo-json\", true},\n\t\t{\"application/foo.json\", true},\n\t\t{\"application/vnd.foo-json\", true},\n\t\t{\"application/vnd.foo.json\", true},\n\t\t{\"application/x-amz-json-1.1\", true},\n\n\t\t{\"text/foo-json\", true},\n\t\t{\"text/foo.json\", true},\n\t\t{\"text/vnd.foo-json\", true},\n\t\t{\"text/vnd.foo.json\", true},\n\t} {\n\t\tresult := isJSONContentType(test.input)\n\n\t\tif result != test.expect {\n\t\t\tt.Errorf(\"failed on %q: want %v, got %v\", test.input, test.expect, result)\n\t\t}\n\t}\n}\n\nfunc TestIsXMLContentType(t *testing.T) {\n\tfor _, test := range []struct {\n\t\tinput  string\n\t\texpect bool\n\t}{\n\t\t{\"application/xml\", true},\n\t\t{\"application/vnd.foo+xml\", true},\n\n\t\t{\"application/xml; charset=utf-8\", true},\n\t\t{\"application/vnd.foo+xml; charset=utf-8\", true},\n\n\t\t{\"text/xml\", true},\n\t\t{\"text/vnd.foo+xml\", true},\n\n\t\t{\"application/foo-xml\", true},\n\t\t{\"application/foo.xml\", true},\n\t\t{\"application/vnd.foo-xml\", true},\n\t\t{\"application/vnd.foo.xml\", true},\n\n\t\t{\"text/foo-xml\", true},\n\t\t{\"text/foo.xml\", true},\n\t\t{\"text/vnd.foo-xml\", true},\n\t\t{\"text/vnd.foo.xml\", true},\n\t} {\n\t\tresult := isXMLContentType(test.input)\n\n\t\tif result != test.expect {\n\t\t\tt.Errorf(\"failed on %q: want %v, got %v\", test.input, test.expect, result)\n\t\t}\n\t}\n}\n\nfunc TestCloneURLValues(t *testing.T) {\n\tv := url.Values{}\n\tv.Add(\"foo\", \"bar\")\n\tv.Add(\"foo\", \"baz\")\n\tv.Add(\"qux\", \"quux\")\n\n\tc := cloneURLValues(v)\n\tnilUrl := cloneURLValues(nil)\n\tassertEqual(t, v, c)\n\tassertNil(t, nilUrl)\n}\n\nfunc TestRestyErrorFuncs(t *testing.T) {\n\tne1 := errors.New(\"new error 1\")\n\tnie1 := errors.New(\"inner error 1\")\n\n\tassertNil(t, wrapErrors(nil, nil))\n\n\te := wrapErrors(ne1, nie1)\n\tassertEqual(t, \"new error 1\", e.Error())\n\tassertEqual(t, \"inner error 1\", errors.Unwrap(e).Error())\n\n\te = wrapErrors(ne1, nil)\n\tassertEqual(t, \"new error 1\", e.Error())\n\n\te = wrapErrors(nil, nie1)\n\tassertEqual(t, \"inner error 1\", e.Error())\n}\n\nfunc Test_createDirectory(t *testing.T) {\n\terrMsg := \"test dir error\"\n\tmkdirAll = func(path string, perm os.FileMode) error {\n\t\treturn errors.New(errMsg)\n\t}\n\tt.Cleanup(func() {\n\t\tmkdirAll = os.MkdirAll\n\t})\n\n\ttempDir := filepath.Join(t.TempDir(), \"test-dir\")\n\terr := createDirectory(tempDir)\n\tassertEqual(t, errMsg, err.Error())\n}\n\nfunc TestUtil_readRandomUint32(t *testing.T) {\n\tdefer func() {\n\t\tif r := recover(); r == nil {\n\t\t\t// panic: resty - guid: unable to generate random object id\n\t\t\tt.Errorf(\"The code did not panic\")\n\t\t}\n\t}()\n\terrMsg := \"read full error\"\n\tioReadFull = func(_ io.Reader, _ []byte) (int, error) {\n\t\treturn 0, errors.New(errMsg)\n\t}\n\tt.Cleanup(func() {\n\t\tioReadFull = io.ReadFull\n\t})\n\n\treadRandomUint32()\n}\n\nfunc TestUtil_readMachineID(t *testing.T) {\n\tt.Run(\"hostname error\", func(t *testing.T) {\n\t\terrHostMsg := \"hostname error\"\n\t\tosHostname = func() (string, error) {\n\t\t\treturn \"\", errors.New(errHostMsg)\n\t\t}\n\t\tt.Cleanup(func() {\n\t\t\tosHostname = os.Hostname\n\t\t})\n\n\t\treadMachineID()\n\t})\n\n\tt.Run(\"hostname and read full error\", func(t *testing.T) {\n\t\tdefer func() {\n\t\t\tif r := recover(); r == nil {\n\t\t\t\t// panic: resty - guid: unable to get hostname and random bytes\n\t\t\t\tt.Errorf(\"The code did not panic\")\n\t\t\t}\n\t\t}()\n\t\terrHostMsg := \"hostname error\"\n\t\tosHostname = func() (string, error) {\n\t\t\treturn \"\", errors.New(errHostMsg)\n\t\t}\n\t\terrReadMsg := \"read full error\"\n\t\tioReadFull = func(_ io.Reader, _ []byte) (int, error) {\n\t\t\treturn 0, errors.New(errReadMsg)\n\t\t}\n\t\tt.Cleanup(func() {\n\t\t\tosHostname = os.Hostname\n\t\t\tioReadFull = io.ReadFull\n\t\t})\n\n\t\treadMachineID()\n\t})\n}\n\nfunc TestInMemoryJSONMarshalUnmarshal(t *testing.T) {\n\tt.Run(\"json encoder\", func(t *testing.T) {\n\t\tuser := &credentials{Username: \"testuser\", Password: \"testpass\"}\n\t\tbuf := acquireBuffer()\n\t\tdefer releaseBuffer(buf)\n\t\terr := InMemoryJSONMarshal(buf, user)\n\t\tassertNil(t, err)\n\t\tassertEqual(t, `{\"username\":\"testuser\",\"password\":\"testpass\"}`, buf.String())\n\t})\n\n\tt.Run(\"json encoder error\", func(t *testing.T) {\n\t\tobj := &brokenMarshalJSON{}\n\t\tbuf := acquireBuffer()\n\t\tdefer releaseBuffer(buf)\n\t\terr := InMemoryJSONMarshal(buf, obj)\n\t\tassertNotNil(t, err)\n\t\tassertTrue(t, strings.Contains(err.Error(), \"b0rk3d\"), \"broken marshal json error\")\n\t})\n\n\tt.Run(\"json decoder\", func(t *testing.T) {\n\t\tbyteData := []byte(`{\"username\":\"testuser\",\"password\":\"testpass\"}`)\n\t\tcred := &credentials{}\n\t\terr := InMemoryJSONUnmarshal(bytes.NewReader(byteData), cred)\n\t\tassertNil(t, err)\n\t\tassertEqual(t, \"testuser\", cred.Username)\n\t\tassertEqual(t, \"testpass\", cred.Password)\n\t})\n\n\tt.Run(\"json decoder read error\", func(t *testing.T) {\n\t\tcred := &credentials{}\n\t\terr := InMemoryJSONUnmarshal(&brokenReadCloser{}, cred)\n\t\tassertNotNil(t, err)\n\t\tassertEqual(t, err.Error(), \"read error\")\n\t})\n\n\tt.Run(\"json decoder error\", func(t *testing.T) {\n\t\tbyteData := []byte(`\"username\":\"testuser\",\"password\":\"testpass\"}`)\n\t\tcred := &credentials{}\n\t\terr := InMemoryJSONUnmarshal(bytes.NewReader(byteData), cred)\n\t\tassertNotNil(t, err)\n\t\tassertTrue(t, strings.Contains(err.Error(), \"invalid character ':' after top-level value\"),\n\t\t\t\"invalid json unmarshal error\")\n\t})\n}\n\nfunc TestInMemoryXMLMarshalUnmarshal(t *testing.T) {\n\tt.Run(\"xml encoder\", func(t *testing.T) {\n\t\tuser := &credentials{Username: \"testuser\", Password: \"testpass\"}\n\t\tbuf := acquireBuffer()\n\t\tdefer releaseBuffer(buf)\n\t\terr := InMemoryXMLMarshal(buf, user)\n\t\tassertNil(t, err)\n\t\tassertEqual(t, `<credentials><Username>testuser</Username><Password>testpass</Password></credentials>`, buf.String())\n\t})\n\n\tt.Run(\"xml encoder error\", func(t *testing.T) {\n\t\tobj := &brokenMarshalXML{}\n\t\tbuf := acquireBuffer()\n\t\tdefer releaseBuffer(buf)\n\t\terr := InMemoryXMLMarshal(buf, obj)\n\t\tassertNotNil(t, err)\n\t\tassertEqual(t, err.Error(), \"b0rk3d\")\n\t})\n\n\tt.Run(\"xml decoder\", func(t *testing.T) {\n\t\tbyteData := []byte(`<?xml version=\"1.0\" encoding=\"UTF-8\"?><credentials><Username>testuser</Username><Password>testpass</Password></credentials>`)\n\t\tcred := &credentials{}\n\t\terr := InMemoryXMLUnmarshal(bytes.NewReader(byteData), cred)\n\t\tassertNil(t, err)\n\t\tassertEqual(t, \"testuser\", cred.Username)\n\t\tassertEqual(t, \"testpass\", cred.Password)\n\t})\n\n\tt.Run(\"xml decoder read error\", func(t *testing.T) {\n\t\tcred := &credentials{}\n\t\terr := InMemoryXMLUnmarshal(&brokenReadCloser{}, cred)\n\t\tassertNotNil(t, err)\n\t\tassertEqual(t, err.Error(), \"read error\")\n\t})\n\n\tt.Run(\"xml decoder error\", func(t *testing.T) {\n\t\tbyteData := []byte(`<?xml version=\"1.0\" encoding=\"UTF-8\"?><Username>testuser</Username><Password>testpass</Password></credentials>`)\n\t\tcred := &credentials{}\n\t\terr := InMemoryJSONUnmarshal(bytes.NewReader(byteData), cred)\n\t\tfmt.Println(err)\n\t\tassertNotNil(t, err)\n\t\tassertEqual(t, err.Error(), \"invalid character '<' looking for beginning of value\")\n\t})\n}\n\nfunc TestInMemoryJSONPost(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\tuser := &credentials{Username: \"testuser\", Password: \"testpass\"}\n\tassertEqual(t, \"Username: **********, Password: **********\", user.String())\n\n\tc := dcnl().\n\t\tAddContentTypeEncoder(jsonContentType, InMemoryJSONMarshal).\n\t\tAddContentTypeDecoder(\"appLiCaTion/JSon\", InMemoryJSONUnmarshal)\n\n\tr := c.R().\n\t\tSetHeader(hdrContentTypeKey, jsonContentType).\n\t\tSetBody(user).\n\t\tSetResult(&AuthSuccess{})\n\n\tresp, err := r.Post(ts.URL + \"/login\")\n\tauthResp := resp.Result().(*AuthSuccess)\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, int64(50), resp.Size())\n\tassertEqual(t, authResp.ID, \"success\")\n\tassertEqual(t, authResp.Message, \"login successful\")\n}\n\nfunc TestInMemoryXMLPost(t *testing.T) {\n\tts := createPostServer(t)\n\tdefer ts.Close()\n\n\txmlContentType := \"application/xml\"\n\tc := dcnl().\n\t\tAddContentTypeEncoder(xmlContentType, InMemoryXMLMarshal).\n\t\tAddContentTypeDecoder(xmlContentType, InMemoryXMLUnmarshal)\n\n\tresp, err := c.R().\n\t\tSetHeader(hdrContentTypeKey, xmlContentType).\n\t\tSetBody(credentials{Username: \"testuser\", Password: \"testpass\"}).\n\t\tSetResult(&AuthSuccess{}).\n\t\tPost(ts.URL + \"/login\")\n\n\tauthResp := resp.Result().(*AuthSuccess)\n\n\tassertError(t, err)\n\tassertEqual(t, http.StatusOK, resp.StatusCode())\n\tassertEqual(t, int64(116), resp.Size())\n\tassertEqual(t, authResp.ID, \"success\")\n\tassertEqual(t, authResp.Message, \"login successful\")\n}\n\n// This test methods exist for test coverage purpose\n// to validate the getter and setter\nfunc TestUtilMiscTestCoverage(t *testing.T) {\n\tl := &limitReadCloser{r: strings.NewReader(\"hello test close for no io.Closer\")}\n\tassertNil(t, l.Close())\n\n\tr := &copyReadCloser{s: strings.NewReader(\"hello test close for no io.Closer\")}\n\tassertNil(t, r.Close())\n\n\tv := struct {\n\t\tID      string `json:\"id\"`\n\t\tMessage string `json:\"message\"`\n\t}{}\n\terr := decodeJSON(bytes.NewReader([]byte(`{\\\"  \\\": \\\"some value\\\"}`)), &v)\n\tassertEqual(t, \"invalid character '\\\\\\\\' looking for beginning of object key string\", err.Error())\n\n\tireErr := &invalidRequestError{Err: errors.New(\"test coverage\")}\n\tassertEqual(t, \"test coverage\", ireErr.Error())\n}\n\n// customStringer implements fmt.Stringer for testing\ntype customStringer struct {\n\tvalue string\n}\n\nfunc (c customStringer) String() string {\n\treturn c.value\n}\n\nfunc TestFormatAnyToString(t *testing.T) {\n\tfixedTime := time.Date(2024, 6, 15, 10, 30, 0, 0, time.UTC)\n\n\tfor _, test := range []struct {\n\t\tname   string\n\t\tinput  any\n\t\texpect string\n\t}{\n\t\t// Tier 1: most common URL types\n\t\t{\"string\", \"hello\", \"hello\"},\n\t\t{\"empty string\", \"\", \"\"},\n\t\t{\"int\", 42, \"42\"},\n\t\t{\"int negative\", -123, \"-123\"},\n\t\t{\"bool true\", true, \"true\"},\n\t\t{\"bool false\", false, \"false\"},\n\t\t{\"int64\", int64(9223372036854775807), \"9223372036854775807\"},\n\t\t{\"int64 negative\", int64(-9223372036854775808), \"-9223372036854775808\"},\n\t\t{\"[]string\", []string{\"a\", \"b\", \"c\"}, \"a,b,c\"},\n\t\t{\"[]string single\", []string{\"only\"}, \"only\"},\n\t\t{\"[]string empty\", []string{}, \"\"},\n\n\t\t// Tier 2: common stdlib types\n\t\t{\"time.Time\", fixedTime, \"2024-06-15T10:30:00Z\"},\n\t\t{\"[]byte\", []byte(\"binary data\"), \"binary data\"},\n\t\t{\"float64\", 3.14159, \"3.14159\"},\n\t\t{\"float64 whole\", float64(42), \"42\"},\n\t\t{\"float64 negative\", -2.5, \"-2.5\"},\n\n\t\t// Tier 3: less common integers (signed)\n\t\t{\"int32\", int32(2147483647), \"2147483647\"},\n\t\t{\"int16\", int16(32767), \"32767\"},\n\t\t{\"int8\", int8(127), \"127\"},\n\n\t\t// Tier 4: less common integers (unsigned)\n\t\t{\"uint64\", uint64(18446744073709551615), \"18446744073709551615\"},\n\t\t{\"uint32\", uint32(4294967295), \"4294967295\"},\n\t\t{\"uint16\", uint16(65535), \"65535\"},\n\t\t{\"uint8\", uint8(255), \"255\"},\n\t\t{\"uint\", uint(12345), \"12345\"},\n\n\t\t// Tier 5: rare types and fallbacks\n\t\t{\"float32\", float32(3.14), \"3.14\"},\n\t\t{\"fmt.Stringer\", customStringer{value: \"custom value\"}, \"custom value\"},\n\t\t{\"default struct\", struct{ Name string }{Name: \"test\"}, \"{test}\"},\n\t\t{\"nil\", nil, \"<nil>\"},\n\t} {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tresult := formatAnyToString(test.input)\n\t\t\tassertEqual(t, test.expect, result)\n\t\t})\n\t}\n}\n"
  }
]