Initial QSfera import
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
coverage.out
|
||||
coverage.txt
|
||||
|
||||
# Exclude IDE folders
|
||||
.idea/*
|
||||
.vscode/*
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
load("@bazel_gazelle//:def.bzl", "gazelle")
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
# gazelle:prefix github.com/go-resty/resty/v2
|
||||
# gazelle:go_naming_convention import_alias
|
||||
gazelle(name = "gazelle")
|
||||
|
||||
go_library(
|
||||
name = "resty",
|
||||
srcs = [
|
||||
"client.go",
|
||||
"digest.go",
|
||||
"middleware.go",
|
||||
"redirect.go",
|
||||
"request.go",
|
||||
"response.go",
|
||||
"resty.go",
|
||||
"retry.go",
|
||||
"trace.go",
|
||||
"transport.go",
|
||||
"transport112.go",
|
||||
"transport_js.go",
|
||||
"transport_other.go",
|
||||
"util.go",
|
||||
"util_curl.go",
|
||||
],
|
||||
importpath = "github.com/go-resty/resty/v2",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//shellescape",
|
||||
"@org_golang_x_net//publicsuffix:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "resty_test",
|
||||
srcs = [
|
||||
"client_test.go",
|
||||
"context_test.go",
|
||||
"example_test.go",
|
||||
"middleware_test.go",
|
||||
"request_test.go",
|
||||
"resty_test.go",
|
||||
"retry_test.go",
|
||||
"util_test.go",
|
||||
],
|
||||
data = glob([".testdata/*"]),
|
||||
embed = [":resty"],
|
||||
deps = [
|
||||
"@org_golang_x_net//proxy:go_default_library",
|
||||
"@org_golang_x_time//rate:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":resty",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2024 Jeevanandam M., https://myjeeva.com <jeeva@myjeeva.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+944
@@ -0,0 +1,944 @@
|
||||
<p align="center">
|
||||
<h1 align="center">Resty</h1>
|
||||
<p align="center">Simple HTTP and REST client library for Go (inspired by Ruby rest-client)</p>
|
||||
<p align="center"><a href="#features">Features</a> section describes in detail about Resty capabilities</p>
|
||||
</p>
|
||||
<p align="center">
|
||||
<p align="center"><a href="https://github.com/go-resty/resty/actions/workflows/ci.yml?query=branch%3Av2"><img src="https://github.com/go-resty/resty/actions/workflows/ci.yml/badge.svg?branch=v2" alt="Build Status"></a> <a href="https://app.codecov.io/gh/go-resty/resty/tree/v2"><img src="https://codecov.io/gh/go-resty/resty/branch/v2/graph/badge.svg" alt="Code Coverage"></a> <a href="https://goreportcard.com/report/go-resty/resty"><img src="https://goreportcard.com/badge/go-resty/resty" alt="Go Report Card"></a> <a href="https://github.com/go-resty/resty/releases/latest"><img src="https://img.shields.io/badge/version-2.17.2-blue.svg" alt="Release Version"></a> <a href="https://pkg.go.dev/github.com/go-resty/resty/v2"><img src="https://pkg.go.dev/badge/github.com/go-resty/resty" alt="GoDoc"></a> <a href="LICENSE"><img src="https://img.shields.io/github/license/go-resty/resty.svg" alt="License"></a> <a href="https://github.com/avelino/awesome-go"><img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome Go"></a></p>
|
||||
</p>
|
||||
|
||||
## News
|
||||
|
||||
* v2.17.2 [released](https://github.com/go-resty/resty/releases/tag/v2.17.2) and tagged on Feb 14, 2026.
|
||||
* v2.0.0 [released](https://github.com/go-resty/resty/releases/tag/v2.0.0) and tagged on Jul 16, 2019.
|
||||
* v1.12.0 [released](https://github.com/go-resty/resty/releases/tag/v1.12.0) and tagged on Feb 27, 2019.
|
||||
* v1.0 released and tagged on Sep 25, 2017. - Resty's first version was released on Sep 15, 2015 then it grew gradually as a very handy and helpful library. Its been a two years since first release. I'm very thankful to Resty users and its [contributors](https://github.com/go-resty/resty/graphs/contributors).
|
||||
|
||||
## Features
|
||||
|
||||
* GET, POST, PUT, DELETE, HEAD, PATCH, OPTIONS, etc.
|
||||
* Simple and chainable methods for settings and request
|
||||
* [Request](https://pkg.go.dev/github.com/go-resty/resty/v2#Request) Body can be `string`, `[]byte`, `struct`, `map`, `slice` and `io.Reader` too
|
||||
* Auto detects `Content-Type`
|
||||
* Buffer less processing for `io.Reader`
|
||||
* Native `*http.Request` instance may be accessed during middleware and request execution via `Request.RawRequest`
|
||||
* Request Body can be read multiple times via `Request.RawRequest.GetBody()`
|
||||
* [Response](https://pkg.go.dev/github.com/go-resty/resty/v2#Response) object gives you more possibility
|
||||
* Access as `[]byte` array - `response.Body()` OR Access as `string` - `response.String()`
|
||||
* Know your `response.Time()` and when we `response.ReceivedAt()`
|
||||
* Automatic marshal and unmarshal for `JSON` and `XML` content type
|
||||
* Default is `JSON`, if you supply `struct/map` without header `Content-Type`
|
||||
* For auto-unmarshal, refer to -
|
||||
- Success scenario [Request.SetResult()](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.SetResult) and [Response.Result()](https://pkg.go.dev/github.com/go-resty/resty/v2#Response.Result).
|
||||
- Error scenario [Request.SetError()](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.SetError) and [Response.Error()](https://pkg.go.dev/github.com/go-resty/resty/v2#Response.Error).
|
||||
- Supports [RFC7807](https://tools.ietf.org/html/rfc7807) - `application/problem+json` & `application/problem+xml`
|
||||
* Resty provides an option to override [JSON Marshal/Unmarshal and XML Marshal/Unmarshal](#override-json--xml-marshalunmarshal)
|
||||
* Easy to upload one or more file(s) via `multipart/form-data`
|
||||
* Auto detects file content type
|
||||
* Request URL [Path Params (aka URI Params)](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.SetPathParams)
|
||||
* Backoff Retry Mechanism with retry condition function [reference](retry_test.go)
|
||||
* Resty client HTTP & REST [Request](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.OnBeforeRequest) and [Response](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.OnAfterResponse) middlewares
|
||||
* `Request.SetContext` supported
|
||||
* Authorization option of `BasicAuth` and `Bearer` token
|
||||
* Set request `ContentLength` value for all request or particular request
|
||||
* Custom [Root Certificates](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.SetRootCertificate) and Client [Certificates](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.SetCertificates)
|
||||
* Download/Save HTTP response directly into File, like `curl -o` flag. See [SetOutputDirectory](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.SetOutputDirectory) & [SetOutput](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.SetOutput).
|
||||
* Cookies for your request and CookieJar support
|
||||
* SRV Record based request instead of Host URL
|
||||
* Client settings like `Timeout`, `RedirectPolicy`, `Proxy`, `TLSClientConfig`, `Transport`, etc.
|
||||
* Optionally allows GET request with payload, see [SetAllowGetMethodPayload](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.SetAllowGetMethodPayload)
|
||||
* Supports registering external JSON library into resty, see [how to use](https://github.com/go-resty/resty/issues/76#issuecomment-314015250)
|
||||
* Exposes Response reader without reading response (no auto-unmarshaling) if need be, see [how to use](https://github.com/go-resty/resty/issues/87#issuecomment-322100604)
|
||||
* Option to specify expected `Content-Type` when response `Content-Type` header missing. Refer to [#92](https://github.com/go-resty/resty/issues/92)
|
||||
* Resty design
|
||||
* Have client level settings & options and also override at Request level if you want to
|
||||
* Request and Response middleware
|
||||
* Create Multiple clients if you want to `resty.New()`
|
||||
* Supports `http.RoundTripper` implementation, see [SetTransport](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.SetTransport)
|
||||
* goroutine concurrent safe
|
||||
* Resty Client trace, see [Client.EnableTrace](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.EnableTrace) and [Request.EnableTrace](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.EnableTrace)
|
||||
* Since v2.4.0, trace info contains a `RequestAttempt` value, and the `Request` object contains an `Attempt` attribute
|
||||
* Supports on-demand CURL command generation, see [Client.EnableGenerateCurlOnDebug](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.EnableGenerateCurlOnDebug), [Request.EnableGenerateCurlOnDebug](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.EnableGenerateCurlOnDebug). It requires debug mode to be enabled.
|
||||
* Debug mode - clean and informative logging presentation
|
||||
* Gzip - Go does it automatically also resty has fallback handling too
|
||||
* Works fine with `HTTP/2` and `HTTP/1.1`, also `HTTP/3` can be used with Resty, see this [comment](https://github.com/go-resty/resty/issues/846#issuecomment-2329696110)
|
||||
* [Bazel support](#bazel-support)
|
||||
* Easily mock Resty for testing, [for e.g.](#mocking-http-requests-using-httpmock-library)
|
||||
* Well tested client library
|
||||
|
||||
### Included Batteries
|
||||
|
||||
* Redirect Policies - see [how to use](#redirect-policy)
|
||||
* NoRedirectPolicy
|
||||
* FlexibleRedirectPolicy
|
||||
* DomainCheckRedirectPolicy
|
||||
* etc. [more info](redirect.go)
|
||||
* Retry Mechanism [how to use](#retries)
|
||||
* Backoff Retry
|
||||
* Conditional Retry
|
||||
* Since v2.6.0, Retry Hooks - [Client](https://pkg.go.dev/github.com/go-resty/resty/v2#Client.AddRetryHook), [Request](https://pkg.go.dev/github.com/go-resty/resty/v2#Request.AddRetryHook)
|
||||
* SRV Record based request instead of Host URL [how to use](resty_test.go#L1412)
|
||||
* etc (upcoming - throw your idea's [here](https://github.com/go-resty/resty/issues)).
|
||||
|
||||
|
||||
#### Supported Go Versions
|
||||
|
||||
Recommended to use `go1.23` and above.
|
||||
|
||||
Initially Resty started supporting `go modules` since `v1.10.0` release.
|
||||
|
||||
Starting Resty v2 and higher versions, it fully embraces [go modules](https://github.com/golang/go/wiki/Modules) package release. It requires a Go version capable of understanding `/vN` suffixed imports:
|
||||
|
||||
- 1.9.7+
|
||||
- 1.10.3+
|
||||
- 1.11+
|
||||
|
||||
|
||||
## It might be beneficial for your project :smile:
|
||||
|
||||
Resty author also published following projects for Go Community.
|
||||
|
||||
* [go-model](https://github.com/jeevatkm/go-model) - Robust & Easy to use model mapper and utility methods for Go `struct`.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
# Go Modules
|
||||
require github.com/go-resty/resty/v2 v2.16.5
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
The following samples will assist you to become as comfortable as possible with resty library.
|
||||
|
||||
```go
|
||||
// Import resty into your code and refer it as `resty`.
|
||||
import "github.com/go-resty/resty/v2"
|
||||
```
|
||||
|
||||
#### Simple GET
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
resp, err := client.R().
|
||||
EnableTrace().
|
||||
Get("https://httpbin.org/get")
|
||||
|
||||
// Explore response object
|
||||
fmt.Println("Response Info:")
|
||||
fmt.Println(" Error :", err)
|
||||
fmt.Println(" Status Code:", resp.StatusCode())
|
||||
fmt.Println(" Status :", resp.Status())
|
||||
fmt.Println(" Proto :", resp.Proto())
|
||||
fmt.Println(" Time :", resp.Time())
|
||||
fmt.Println(" Received At:", resp.ReceivedAt())
|
||||
fmt.Println(" Body :\n", resp)
|
||||
fmt.Println()
|
||||
|
||||
// Explore trace info
|
||||
fmt.Println("Request Trace Info:")
|
||||
ti := resp.Request.TraceInfo()
|
||||
fmt.Println(" DNSLookup :", ti.DNSLookup)
|
||||
fmt.Println(" ConnTime :", ti.ConnTime)
|
||||
fmt.Println(" TCPConnTime :", ti.TCPConnTime)
|
||||
fmt.Println(" TLSHandshake :", ti.TLSHandshake)
|
||||
fmt.Println(" ServerTime :", ti.ServerTime)
|
||||
fmt.Println(" ResponseTime :", ti.ResponseTime)
|
||||
fmt.Println(" TotalTime :", ti.TotalTime)
|
||||
fmt.Println(" IsConnReused :", ti.IsConnReused)
|
||||
fmt.Println(" IsConnWasIdle :", ti.IsConnWasIdle)
|
||||
fmt.Println(" ConnIdleTime :", ti.ConnIdleTime)
|
||||
fmt.Println(" RequestAttempt:", ti.RequestAttempt)
|
||||
fmt.Println(" RemoteAddr :", ti.RemoteAddr.String())
|
||||
|
||||
/* Output
|
||||
Response Info:
|
||||
Error : <nil>
|
||||
Status Code: 200
|
||||
Status : 200 OK
|
||||
Proto : HTTP/2.0
|
||||
Time : 457.034718ms
|
||||
Received At: 2020-09-14 15:35:29.784681 -0700 PDT m=+0.458137045
|
||||
Body :
|
||||
{
|
||||
"args": {},
|
||||
"headers": {
|
||||
"Accept-Encoding": "gzip",
|
||||
"Host": "httpbin.org",
|
||||
"User-Agent": "go-resty/2.4.0 (https://github.com/go-resty/resty)",
|
||||
"X-Amzn-Trace-Id": "Root=1-5f5ff031-000ff6292204aa6898e4de49"
|
||||
},
|
||||
"origin": "0.0.0.0",
|
||||
"url": "https://httpbin.org/get"
|
||||
}
|
||||
|
||||
Request Trace Info:
|
||||
DNSLookup : 4.074657ms
|
||||
ConnTime : 381.709936ms
|
||||
TCPConnTime : 77.428048ms
|
||||
TLSHandshake : 299.623597ms
|
||||
ServerTime : 75.414703ms
|
||||
ResponseTime : 79.337µs
|
||||
TotalTime : 457.034718ms
|
||||
IsConnReused : false
|
||||
IsConnWasIdle : false
|
||||
ConnIdleTime : 0s
|
||||
RequestAttempt: 1
|
||||
RemoteAddr : 3.221.81.55:443
|
||||
*/
|
||||
```
|
||||
|
||||
#### Enhanced GET
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
resp, err := client.R().
|
||||
SetQueryParams(map[string]string{
|
||||
"page_no": "1",
|
||||
"limit": "20",
|
||||
"sort":"name",
|
||||
"order": "asc",
|
||||
"random":strconv.FormatInt(time.Now().Unix(), 10),
|
||||
}).
|
||||
SetHeader("Accept", "application/json").
|
||||
SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F").
|
||||
Get("/search_result")
|
||||
|
||||
|
||||
// Sample of using Request.SetQueryString method
|
||||
resp, err := client.R().
|
||||
SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more").
|
||||
SetHeader("Accept", "application/json").
|
||||
SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F").
|
||||
Get("/show_product")
|
||||
|
||||
|
||||
// If necessary, you can force response content type to tell Resty to parse a JSON response into your struct
|
||||
resp, err := client.R().
|
||||
SetResult(result).
|
||||
ForceContentType("application/json").
|
||||
Get("v2/alpine/manifests/latest")
|
||||
```
|
||||
|
||||
#### Various POST method combinations
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
// POST JSON string
|
||||
// No need to set content type, if you have client level setting
|
||||
resp, err := client.R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(`{"username":"testuser", "password":"testpass"}`).
|
||||
SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}).
|
||||
Post("https://myapp.com/login")
|
||||
|
||||
// POST []byte array
|
||||
// No need to set content type, if you have client level setting
|
||||
resp, err := client.R().
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody([]byte(`{"username":"testuser", "password":"testpass"}`)).
|
||||
SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}).
|
||||
Post("https://myapp.com/login")
|
||||
|
||||
// POST Struct, default is JSON content type. No need to set one
|
||||
resp, err := client.R().
|
||||
SetBody(User{Username: "testuser", Password: "testpass"}).
|
||||
SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}).
|
||||
SetError(&AuthError{}). // or SetError(AuthError{}).
|
||||
Post("https://myapp.com/login")
|
||||
|
||||
// POST Map, default is JSON content type. No need to set one
|
||||
resp, err := client.R().
|
||||
SetBody(map[string]interface{}{"username": "testuser", "password": "testpass"}).
|
||||
SetResult(&AuthSuccess{}). // or SetResult(AuthSuccess{}).
|
||||
SetError(&AuthError{}). // or SetError(AuthError{}).
|
||||
Post("https://myapp.com/login")
|
||||
|
||||
// POST of raw bytes for file upload. For example: upload file to Dropbox
|
||||
fileBytes, _ := os.ReadFile("/Users/jeeva/mydocument.pdf")
|
||||
|
||||
// See we are not setting content-type header, since go-resty automatically detects Content-Type for you
|
||||
resp, err := client.R().
|
||||
SetBody(fileBytes).
|
||||
SetContentLength(true). // Dropbox expects this value
|
||||
SetAuthToken("<your-auth-token>").
|
||||
SetError(&DropboxError{}). // or SetError(DropboxError{}).
|
||||
Post("https://content.dropboxapi.com/1/files_put/auto/resty/mydocument.pdf") // for upload Dropbox supports PUT too
|
||||
|
||||
// Note: resty detects Content-Type for request body/payload if content type header is not set.
|
||||
// * For struct and map data type defaults to 'application/json'
|
||||
// * Fallback is plain text content type
|
||||
```
|
||||
|
||||
#### Sample PUT
|
||||
|
||||
You can use various combinations of `PUT` method call like demonstrated for `POST`.
|
||||
|
||||
```go
|
||||
// Note: This is one sample of PUT method usage, refer POST for more combination
|
||||
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
// Request goes as JSON content type
|
||||
// No need to set auth token, error, if you have client level settings
|
||||
resp, err := client.R().
|
||||
SetBody(Article{
|
||||
Title: "go-resty",
|
||||
Content: "This is my article content, oh ya!",
|
||||
Author: "Jeevanandam M",
|
||||
Tags: []string{"article", "sample", "resty"},
|
||||
}).
|
||||
SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
|
||||
SetError(&Error{}). // or SetError(Error{}).
|
||||
Put("https://myapp.com/article/1234")
|
||||
```
|
||||
|
||||
#### Sample PATCH
|
||||
|
||||
You can use various combinations of `PATCH` method call like demonstrated for `POST`.
|
||||
|
||||
```go
|
||||
// Note: This is one sample of PUT method usage, refer POST for more combination
|
||||
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
// Request goes as JSON content type
|
||||
// No need to set auth token, error, if you have client level settings
|
||||
resp, err := client.R().
|
||||
SetBody(Article{
|
||||
Tags: []string{"new tag1", "new tag2"},
|
||||
}).
|
||||
SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
|
||||
SetError(&Error{}). // or SetError(Error{}).
|
||||
Patch("https://myapp.com/articles/1234")
|
||||
```
|
||||
|
||||
#### Sample DELETE, HEAD, OPTIONS
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
// DELETE a article
|
||||
// No need to set auth token, error, if you have client level settings
|
||||
resp, err := client.R().
|
||||
SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
|
||||
SetError(&Error{}). // or SetError(Error{}).
|
||||
Delete("https://myapp.com/articles/1234")
|
||||
|
||||
// DELETE a articles with payload/body as a JSON string
|
||||
// No need to set auth token, error, if you have client level settings
|
||||
resp, err := client.R().
|
||||
SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
|
||||
SetError(&Error{}). // or SetError(Error{}).
|
||||
SetHeader("Content-Type", "application/json").
|
||||
SetBody(`{article_ids: [1002, 1006, 1007, 87683, 45432] }`).
|
||||
Delete("https://myapp.com/articles")
|
||||
|
||||
// HEAD of resource
|
||||
// No need to set auth token, if you have client level settings
|
||||
resp, err := client.R().
|
||||
SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
|
||||
Head("https://myapp.com/videos/hi-res-video")
|
||||
|
||||
// OPTIONS of resource
|
||||
// No need to set auth token, if you have client level settings
|
||||
resp, err := client.R().
|
||||
SetAuthToken("C6A79608-782F-4ED0-A11D-BD82FAD829CD").
|
||||
Options("https://myapp.com/servers/nyc-dc-01")
|
||||
```
|
||||
|
||||
#### Override JSON & XML Marshal/Unmarshal
|
||||
|
||||
User could register choice of JSON/XML library into resty or write your own. By default resty registers standard `encoding/json` and `encoding/xml` respectively.
|
||||
```go
|
||||
// Example of registering json-iterator
|
||||
import jsoniter "github.com/json-iterator/go"
|
||||
|
||||
json := jsoniter.ConfigCompatibleWithStandardLibrary
|
||||
|
||||
client := resty.New().
|
||||
SetJSONMarshaler(json.Marshal).
|
||||
SetJSONUnmarshaler(json.Unmarshal)
|
||||
|
||||
// similarly user could do for XML too with -
|
||||
client.SetXMLMarshaler(xml.Marshal).
|
||||
SetXMLUnmarshaler(xml.Unmarshal)
|
||||
```
|
||||
|
||||
### Multipart File(s) upload
|
||||
|
||||
#### Using io.Reader
|
||||
|
||||
```go
|
||||
profileImgBytes, _ := os.ReadFile("/Users/jeeva/test-img.png")
|
||||
notesBytes, _ := os.ReadFile("/Users/jeeva/text-file.txt")
|
||||
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
resp, err := client.R().
|
||||
SetFileReader("profile_img", "test-img.png", bytes.NewReader(profileImgBytes)).
|
||||
SetFileReader("notes", "text-file.txt", bytes.NewReader(notesBytes)).
|
||||
SetFormData(map[string]string{
|
||||
"first_name": "Jeevanandam",
|
||||
"last_name": "M",
|
||||
}).
|
||||
Post("http://myapp.com/upload")
|
||||
```
|
||||
|
||||
#### Using File directly from Path
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
// Single file scenario
|
||||
resp, err := client.R().
|
||||
SetFile("profile_img", "/Users/jeeva/test-img.png").
|
||||
Post("http://myapp.com/upload")
|
||||
|
||||
// Multiple files scenario
|
||||
resp, err := client.R().
|
||||
SetFiles(map[string]string{
|
||||
"profile_img": "/Users/jeeva/test-img.png",
|
||||
"notes": "/Users/jeeva/text-file.txt",
|
||||
}).
|
||||
Post("http://myapp.com/upload")
|
||||
|
||||
// Multipart of form fields and files
|
||||
resp, err := client.R().
|
||||
SetFiles(map[string]string{
|
||||
"profile_img": "/Users/jeeva/test-img.png",
|
||||
"notes": "/Users/jeeva/text-file.txt",
|
||||
}).
|
||||
SetFormData(map[string]string{
|
||||
"first_name": "Jeevanandam",
|
||||
"last_name": "M",
|
||||
"zip_code": "00001",
|
||||
"city": "my city",
|
||||
"access_token": "C6A79608-782F-4ED0-A11D-BD82FAD829CD",
|
||||
}).
|
||||
Post("http://myapp.com/profile")
|
||||
```
|
||||
|
||||
#### Sample Form submission
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
// just mentioning about POST as an example with simple flow
|
||||
// User Login
|
||||
resp, err := client.R().
|
||||
SetFormData(map[string]string{
|
||||
"username": "jeeva",
|
||||
"password": "mypass",
|
||||
}).
|
||||
Post("http://myapp.com/login")
|
||||
|
||||
// Followed by profile update
|
||||
resp, err := client.R().
|
||||
SetFormData(map[string]string{
|
||||
"first_name": "Jeevanandam",
|
||||
"last_name": "M",
|
||||
"zip_code": "00001",
|
||||
"city": "new city update",
|
||||
}).
|
||||
Post("http://myapp.com/profile")
|
||||
|
||||
// Multi value form data
|
||||
criteria := url.Values{
|
||||
"search_criteria": []string{"book", "glass", "pencil"},
|
||||
}
|
||||
resp, err := client.R().
|
||||
SetFormDataFromValues(criteria).
|
||||
Post("http://myapp.com/search")
|
||||
```
|
||||
|
||||
#### Save HTTP Response into File
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
// Setting output directory path, If directory not exists then resty creates one!
|
||||
// This is optional one, if you're planning using absolute path in
|
||||
// `Request.SetOutput` and can used together.
|
||||
client.SetOutputDirectory("/Users/jeeva/Downloads")
|
||||
|
||||
// HTTP response gets saved into file, similar to curl -o flag
|
||||
_, err := client.R().
|
||||
SetOutput("plugin/ReplyWithHeader-v5.1-beta.zip").
|
||||
Get("http://bit.ly/1LouEKr")
|
||||
|
||||
// OR using absolute path
|
||||
// Note: output directory path is not used for absolute path
|
||||
_, err := client.R().
|
||||
SetOutput("/MyDownloads/plugin/ReplyWithHeader-v5.1-beta.zip").
|
||||
Get("http://bit.ly/1LouEKr")
|
||||
```
|
||||
|
||||
#### Request URL Path Params
|
||||
|
||||
Resty provides easy to use dynamic request URL path params. Params can be set at client and request level. Client level params value can be overridden at request level.
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
client.R().SetPathParams(map[string]string{
|
||||
"userId": "sample@sample.com",
|
||||
"subAccountId": "100002",
|
||||
}).
|
||||
Get("/v1/users/{userId}/{subAccountId}/details")
|
||||
|
||||
// Result:
|
||||
// Composed URL - /v1/users/sample@sample.com/100002/details
|
||||
```
|
||||
|
||||
#### Request and Response Middleware
|
||||
|
||||
Resty provides middleware ability to manipulate for Request and Response. It is more flexible than callback approach.
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
// Registering Request Middleware
|
||||
client.OnBeforeRequest(func(c *resty.Client, req *resty.Request) error {
|
||||
// Now you have access to Client and current Request object
|
||||
// manipulate it as per your need
|
||||
|
||||
return nil // if its success otherwise return error
|
||||
})
|
||||
|
||||
// Registering Response Middleware
|
||||
client.OnAfterResponse(func(c *resty.Client, resp *resty.Response) error {
|
||||
// Now you have access to Client and current Response object
|
||||
// manipulate it as per your need
|
||||
|
||||
return nil // if its success otherwise return error
|
||||
})
|
||||
```
|
||||
|
||||
#### OnError Hooks
|
||||
|
||||
Resty provides OnError hooks that may be called because:
|
||||
|
||||
- The client failed to send the request due to connection timeout, TLS handshake failure, etc...
|
||||
- The request was retried the maximum amount of times, and still failed.
|
||||
|
||||
If there was a response from the server, the original error will be wrapped in `*resty.ResponseError` which contains the last response received.
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
client.OnError(func(req *resty.Request, err error) {
|
||||
if v, ok := err.(*resty.ResponseError); ok {
|
||||
// v.Response contains the last response from the server
|
||||
// v.Err contains the original error
|
||||
}
|
||||
// Log the error, increment a metric, etc...
|
||||
})
|
||||
```
|
||||
|
||||
#### Generate CURL Command
|
||||
>Refer: [curl_cmd_test.go](https://github.com/go-resty/resty/blob/v2/curl_cmd_test.go)
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
resp, err := client.R().
|
||||
SetDebug(true).
|
||||
EnableGenerateCurlOnDebug(). // CURL command generated when debug mode enabled with this option
|
||||
SetBody(map[string]string{"name": "Alex"}).
|
||||
Post("https://httpbin.org/post")
|
||||
|
||||
curlCmdExecuted := resp.Request.GenerateCurlCommand()
|
||||
|
||||
// Explore curl command
|
||||
fmt.Println("Curl Command:\n ", curlCmdExecuted+"\n")
|
||||
|
||||
/* Output
|
||||
Curl Command:
|
||||
curl -X POST -H 'Content-Type: application/json' -H 'User-Agent: go-resty/2.14.0 (https://github.com/go-resty/resty)' -d '{"name":"Alex"}' https://httpbin.org/post
|
||||
*/
|
||||
```
|
||||
|
||||
#### Redirect Policy
|
||||
|
||||
Resty provides few ready to use redirect policy(s) also it supports multiple policies together.
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
// Assign Client Redirect Policy. Create one as per you need
|
||||
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(15))
|
||||
|
||||
// Wanna multiple policies such as redirect count, domain name check, etc
|
||||
client.SetRedirectPolicy(resty.FlexibleRedirectPolicy(20),
|
||||
resty.DomainCheckRedirectPolicy("host1.com", "host2.org", "host3.net"))
|
||||
```
|
||||
|
||||
##### Custom Redirect Policy
|
||||
|
||||
Implement [RedirectPolicy](redirect.go#L20) interface and register it with resty client. Have a look [redirect.go](redirect.go) for more information.
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
// Using raw func into resty.SetRedirectPolicy
|
||||
client.SetRedirectPolicy(resty.RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
|
||||
// Implement your logic here
|
||||
|
||||
// return nil for continue redirect otherwise return error to stop/prevent redirect
|
||||
return nil
|
||||
}))
|
||||
|
||||
//---------------------------------------------------
|
||||
|
||||
// Using struct create more flexible redirect policy
|
||||
type CustomRedirectPolicy struct {
|
||||
// variables goes here
|
||||
}
|
||||
|
||||
func (c *CustomRedirectPolicy) Apply(req *http.Request, via []*http.Request) error {
|
||||
// Implement your logic here
|
||||
|
||||
// return nil for continue redirect otherwise return error to stop/prevent redirect
|
||||
return nil
|
||||
}
|
||||
|
||||
// Registering in resty
|
||||
client.SetRedirectPolicy(CustomRedirectPolicy{/* initialize variables */})
|
||||
```
|
||||
|
||||
#### Custom Root Certificates and Client Certificates
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
// Custom Root certificates, just supply .pem file.
|
||||
// you can add one or more root certificates, its get appended
|
||||
client.SetRootCertificate("/path/to/root/pemFile1.pem")
|
||||
client.SetRootCertificate("/path/to/root/pemFile2.pem")
|
||||
// ... and so on!
|
||||
|
||||
// Adding Client Certificates, you add one or more certificates
|
||||
// Sample for creating certificate object
|
||||
// Parsing public/private key pair from a pair of files. The files must contain PEM encoded data.
|
||||
cert1, err := tls.LoadX509KeyPair("certs/client.pem", "certs/client.key")
|
||||
if err != nil {
|
||||
log.Fatalf("ERROR client certificate: %s", err)
|
||||
}
|
||||
// ...
|
||||
|
||||
// You add one or more certificates
|
||||
client.SetCertificates(cert1, cert2, cert3)
|
||||
```
|
||||
|
||||
#### Custom Root Certificates and Client Certificates from string
|
||||
|
||||
```go
|
||||
// Custom Root certificates from string
|
||||
// You can pass you certificates through env variables as strings
|
||||
// you can add one or more root certificates, its get appended
|
||||
client.SetRootCertificateFromString("-----BEGIN CERTIFICATE-----content-----END CERTIFICATE-----")
|
||||
client.SetRootCertificateFromString("-----BEGIN CERTIFICATE-----content-----END CERTIFICATE-----")
|
||||
// ... and so on!
|
||||
|
||||
// Adding Client Certificates, you add one or more certificates
|
||||
// Sample for creating certificate object
|
||||
// Parsing public/private key pair from a pair of files. The files must contain PEM encoded data.
|
||||
cert1, err := tls.X509KeyPair([]byte("-----BEGIN CERTIFICATE-----content-----END CERTIFICATE-----"), []byte("-----BEGIN CERTIFICATE-----content-----END CERTIFICATE-----"))
|
||||
if err != nil {
|
||||
log.Fatalf("ERROR client certificate: %s", err)
|
||||
}
|
||||
// ...
|
||||
|
||||
// You add one or more certificates
|
||||
client.SetCertificates(cert1, cert2, cert3)
|
||||
```
|
||||
|
||||
#### Proxy Settings
|
||||
|
||||
Default `Go` supports Proxy via environment variable `HTTP_PROXY`. Resty provides support via `SetProxy` & `RemoveProxy`.
|
||||
Choose as per your need.
|
||||
|
||||
**Client Level Proxy** settings applied to all the request
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
// Setting a Proxy URL and Port
|
||||
client.SetProxy("http://proxyserver:8888")
|
||||
|
||||
// Want to remove proxy setting
|
||||
client.RemoveProxy()
|
||||
```
|
||||
|
||||
#### Retries
|
||||
|
||||
Resty uses [backoff](http://www.awsarchitectureblog.com/2015/03/backoff.html)
|
||||
to increase retry intervals after each attempt.
|
||||
|
||||
Usage example:
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
// Retries are configured per client
|
||||
client.
|
||||
// Set retry count to non zero to enable retries
|
||||
SetRetryCount(3).
|
||||
// You can override initial retry wait time.
|
||||
// Default is 100 milliseconds.
|
||||
SetRetryWaitTime(5 * time.Second).
|
||||
// MaxWaitTime can be overridden as well.
|
||||
// Default is 2 seconds.
|
||||
SetRetryMaxWaitTime(20 * time.Second).
|
||||
// SetRetryAfter sets callback to calculate wait time between retries.
|
||||
// Default (nil) implies exponential backoff with jitter
|
||||
SetRetryAfter(func(client *resty.Client, resp *resty.Response) (time.Duration, error) {
|
||||
return 0, errors.New("quota exceeded")
|
||||
})
|
||||
```
|
||||
|
||||
By default, resty will retry requests that return a non-nil error during execution.
|
||||
Therefore, the above setup will result in resty retrying requests with non-nil errors up to 3 times,
|
||||
with the delay increasing after each attempt.
|
||||
|
||||
You can optionally provide client with [custom retry conditions](https://pkg.go.dev/github.com/go-resty/resty/v2#RetryConditionFunc):
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
client.AddRetryCondition(
|
||||
// RetryConditionFunc type is for retry condition function
|
||||
// input: non-nil Response OR request execution error
|
||||
func(r *resty.Response, err error) bool {
|
||||
return r.StatusCode() == http.StatusTooManyRequests
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
The above example will make resty retry requests that end with a `429 Too Many Requests` status code.
|
||||
It's important to note that when you specify conditions using `AddRetryCondition`,
|
||||
it will override the default retry behavior, which retries on errors encountered during the request.
|
||||
If you want to retry on errors encountered during the request, similar to the default behavior,
|
||||
you'll need to configure it as follows:
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
client.AddRetryCondition(
|
||||
func(r *resty.Response, err error) bool {
|
||||
// Including "err != nil" emulates the default retry behavior for errors encountered during the request.
|
||||
return err != nil || r.StatusCode() == http.StatusTooManyRequests
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Multiple retry conditions can be added.
|
||||
Note that if multiple conditions are specified, a retry will occur if any of the conditions are met.
|
||||
|
||||
It is also possible to use `resty.Backoff(...)` to get arbitrary retry scenarios
|
||||
implemented. [Reference](retry_test.go).
|
||||
|
||||
#### Allow GET request with Payload
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
// Allow GET request with Payload. This is disabled by default.
|
||||
client.SetAllowGetMethodPayload(true)
|
||||
```
|
||||
|
||||
#### Wanna Multiple Clients
|
||||
|
||||
```go
|
||||
// Here you go!
|
||||
// Client 1
|
||||
client1 := resty.New()
|
||||
client1.R().Get("http://httpbin.org")
|
||||
// ...
|
||||
|
||||
// Client 2
|
||||
client2 := resty.New()
|
||||
client2.R().Head("http://httpbin.org")
|
||||
// ...
|
||||
|
||||
// Bend it as per your need!!!
|
||||
```
|
||||
|
||||
#### Remaining Client Settings & its Options
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
// Unique settings at Client level
|
||||
//--------------------------------
|
||||
// Enable debug mode
|
||||
client.SetDebug(true)
|
||||
|
||||
// Assign Client TLSClientConfig
|
||||
// One can set custom root-certificate. Refer: http://golang.org/pkg/crypto/tls/#example_Dial
|
||||
client.SetTLSClientConfig(&tls.Config{ RootCAs: roots })
|
||||
|
||||
// or One can disable security check (https)
|
||||
client.SetTLSClientConfig(&tls.Config{ InsecureSkipVerify: true })
|
||||
|
||||
// Set client timeout as per your need
|
||||
client.SetTimeout(1 * time.Minute)
|
||||
|
||||
|
||||
// You can override all below settings and options at request level if you want to
|
||||
//--------------------------------------------------------------------------------
|
||||
// Host URL for all request. So you can use relative URL in the request
|
||||
client.SetBaseURL("http://httpbin.org")
|
||||
|
||||
// Headers for all request
|
||||
client.SetHeader("Accept", "application/json")
|
||||
client.SetHeaders(map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "My custom User Agent String",
|
||||
})
|
||||
|
||||
// Cookies for all request
|
||||
client.SetCookie(&http.Cookie{
|
||||
Name:"go-resty",
|
||||
Value:"This is cookie value",
|
||||
Path: "/",
|
||||
Domain: "sample.com",
|
||||
MaxAge: 36000,
|
||||
HttpOnly: true,
|
||||
Secure: false,
|
||||
})
|
||||
client.SetCookies(cookies)
|
||||
|
||||
// URL query parameters for all request
|
||||
client.SetQueryParam("user_id", "00001")
|
||||
client.SetQueryParams(map[string]string{ // sample of those who use this manner
|
||||
"api_key": "api-key-here",
|
||||
"api_secret": "api-secret",
|
||||
})
|
||||
client.R().SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more")
|
||||
|
||||
// Form data for all request. Typically used with POST and PUT
|
||||
client.SetFormData(map[string]string{
|
||||
"access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F",
|
||||
})
|
||||
|
||||
// Basic Auth for all request
|
||||
client.SetBasicAuth("myuser", "mypass")
|
||||
|
||||
// Bearer Auth Token for all request
|
||||
client.SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F")
|
||||
|
||||
// Enabling Content length value for all request
|
||||
client.SetContentLength(true)
|
||||
|
||||
// Registering global Error object structure for JSON/XML request
|
||||
client.SetError(&Error{}) // or resty.SetError(Error{})
|
||||
```
|
||||
|
||||
#### Unix Socket
|
||||
|
||||
```go
|
||||
unixSocket := "/var/run/my_socket.sock"
|
||||
|
||||
// Create a Go's http.Transport so we can set it in resty.
|
||||
transport := http.Transport{
|
||||
Dial: func(_, _ string) (net.Conn, error) {
|
||||
return net.Dial("unix", unixSocket)
|
||||
},
|
||||
}
|
||||
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
// Set the previous transport that we created, set the scheme of the communication to the
|
||||
// socket and set the unixSocket as the HostURL.
|
||||
client.SetTransport(&transport).SetScheme("http").SetBaseURL(unixSocket)
|
||||
|
||||
// No need to write the host's URL on the request, just the path.
|
||||
client.R().Get("http://localhost/index.html")
|
||||
```
|
||||
|
||||
#### Bazel Support
|
||||
|
||||
Resty can be built, tested and depended upon via [Bazel](https://bazel.build).
|
||||
For example, to run all tests:
|
||||
|
||||
```shell
|
||||
bazel test :resty_test
|
||||
```
|
||||
|
||||
#### Mocking http requests using [httpmock](https://github.com/jarcoal/httpmock) library
|
||||
|
||||
In order to mock the http requests when testing your application you
|
||||
could use the `httpmock` library.
|
||||
|
||||
When using the default resty client, you should pass the client to the library as follow:
|
||||
|
||||
```go
|
||||
// Create a Resty Client
|
||||
client := resty.New()
|
||||
|
||||
// Get the underlying HTTP Client and set it to Mock
|
||||
httpmock.ActivateNonDefault(client.GetClient())
|
||||
```
|
||||
|
||||
More detailed example of mocking resty http requests using ginko could be found [here](https://github.com/jarcoal/httpmock#ginkgo--resty-example).
|
||||
|
||||
## Versioning
|
||||
|
||||
Resty releases versions according to [Semantic Versioning](http://semver.org)
|
||||
|
||||
* Resty v2 does not use `gopkg.in` service for library versioning.
|
||||
* Resty fully adapted to `go mod` capabilities since `v1.10.0` release.
|
||||
* 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`.
|
||||
* Development takes place at the master branch. Although the code in master should always compile and test successfully, it might break API's. I aim to maintain backwards compatibility, but sometimes API's and behavior might be changed to fix a bug.
|
||||
|
||||
## Contribution
|
||||
|
||||
I would welcome your contribution! If you find any improvement or issue you want to fix, feel free to send a pull request, I like pull requests that include test cases for fix/enhancement. I have done my best to bring pretty good code coverage. Feel free to write tests.
|
||||
|
||||
BTW, I'd like to know what you think about `Resty`. Kindly open an issue or send me an email; it'd mean a lot to me.
|
||||
|
||||
## Creator
|
||||
|
||||
[Jeevanandam M.](https://github.com/jeevatkm) (jeeva@myjeeva.com)
|
||||
|
||||
## Core Team
|
||||
|
||||
Have a look on [Members](https://github.com/orgs/go-resty/people) page.
|
||||
|
||||
## Contributors
|
||||
|
||||
Have a look on [Contributors](https://github.com/go-resty/resty/graphs/contributors) page.
|
||||
|
||||
## License
|
||||
|
||||
Resty released under MIT license, refer [LICENSE](LICENSE) file.
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
workspace(name = "resty")
|
||||
|
||||
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
|
||||
|
||||
http_archive(
|
||||
name = "io_bazel_rules_go",
|
||||
sha256 = "80a98277ad1311dacd837f9b16db62887702e9f1d1c4c9f796d0121a46c8e184",
|
||||
urls = [
|
||||
"https://mirror.bazel.build/github.com/bazelbuild/rules_go/releases/download/v0.46.0/rules_go-v0.46.0.zip",
|
||||
"https://github.com/bazelbuild/rules_go/releases/download/v0.46.0/rules_go-v0.46.0.zip",
|
||||
],
|
||||
)
|
||||
|
||||
http_archive(
|
||||
name = "bazel_gazelle",
|
||||
sha256 = "62ca106be173579c0a167deb23358fdfe71ffa1e4cfdddf5582af26520f1c66f",
|
||||
urls = [
|
||||
"https://mirror.bazel.build/github.com/bazelbuild/bazel-gazelle/releases/download/v0.23.0/bazel-gazelle-v0.23.0.tar.gz",
|
||||
"https://github.com/bazelbuild/bazel-gazelle/releases/download/v0.23.0/bazel-gazelle-v0.23.0.tar.gz",
|
||||
],
|
||||
)
|
||||
|
||||
load("@io_bazel_rules_go//go:deps.bzl", "go_register_toolchains", "go_rules_dependencies")
|
||||
|
||||
go_rules_dependencies()
|
||||
|
||||
go_register_toolchains(version = "1.19")
|
||||
|
||||
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies")
|
||||
|
||||
gazelle_dependencies()
|
||||
+1500
File diff suppressed because it is too large
Load Diff
+327
@@ -0,0 +1,327 @@
|
||||
// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com)
|
||||
// 2023 Segev Dagan (https://github.com/segevda)
|
||||
// 2024 Philipp Wolfer (https://github.com/phw)
|
||||
// All rights reserved.
|
||||
// resty source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package resty
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrDigestBadChallenge = errors.New("digest: challenge is bad")
|
||||
ErrDigestCharset = errors.New("digest: unsupported charset")
|
||||
ErrDigestAlgNotSupported = errors.New("digest: algorithm is not supported")
|
||||
ErrDigestQopNotSupported = errors.New("digest: no supported qop in list")
|
||||
ErrDigestNoQop = errors.New("digest: qop must be specified")
|
||||
)
|
||||
|
||||
var hashFuncs = map[string]func() hash.Hash{
|
||||
"": md5.New,
|
||||
"MD5": md5.New,
|
||||
"MD5-sess": md5.New,
|
||||
"SHA-256": sha256.New,
|
||||
"SHA-256-sess": sha256.New,
|
||||
"SHA-512-256": sha512.New,
|
||||
"SHA-512-256-sess": sha512.New,
|
||||
}
|
||||
|
||||
type digestCredentials struct {
|
||||
username, password string
|
||||
}
|
||||
|
||||
type digestTransport struct {
|
||||
digestCredentials
|
||||
transport http.RoundTripper
|
||||
}
|
||||
|
||||
func (dt *digestTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
// Copy the request, so we don't modify the input.
|
||||
req2 := new(http.Request)
|
||||
*req2 = *req
|
||||
req2.Header = make(http.Header)
|
||||
for k, s := range req.Header {
|
||||
req2.Header[k] = s
|
||||
}
|
||||
|
||||
// Fix http: ContentLength=xxx with Body length 0
|
||||
if req2.Body == nil {
|
||||
req2.ContentLength = 0
|
||||
} else if req2.GetBody != nil {
|
||||
var err error
|
||||
req2.Body, err = req2.GetBody()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Make a request to get the 401 that contains the challenge.
|
||||
resp, err := dt.transport.RoundTrip(req)
|
||||
if err != nil || resp.StatusCode != http.StatusUnauthorized {
|
||||
return resp, err
|
||||
}
|
||||
chal := resp.Header.Get(hdrWwwAuthenticateKey)
|
||||
if chal == "" {
|
||||
return resp, ErrDigestBadChallenge
|
||||
}
|
||||
|
||||
c, err := parseChallenge(chal)
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
|
||||
// Form credentials based on the challenge
|
||||
cr := dt.newCredentials(req2, c)
|
||||
auth, err := cr.authorize()
|
||||
if err != nil {
|
||||
return resp, err
|
||||
}
|
||||
err = resp.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make authenticated request
|
||||
req2.Header.Set(hdrAuthorizationKey, auth)
|
||||
return dt.transport.RoundTrip(req2)
|
||||
}
|
||||
|
||||
func (dt *digestTransport) newCredentials(req *http.Request, c *challenge) *credentials {
|
||||
return &credentials{
|
||||
username: dt.username,
|
||||
userhash: c.userhash,
|
||||
realm: c.realm,
|
||||
nonce: c.nonce,
|
||||
digestURI: req.URL.RequestURI(),
|
||||
algorithm: c.algorithm,
|
||||
sessionAlg: strings.HasSuffix(c.algorithm, "-sess"),
|
||||
opaque: c.opaque,
|
||||
messageQop: c.qop,
|
||||
nc: 0,
|
||||
method: req.Method,
|
||||
password: dt.password,
|
||||
}
|
||||
}
|
||||
|
||||
type challenge struct {
|
||||
realm string
|
||||
domain string
|
||||
nonce string
|
||||
opaque string
|
||||
stale string
|
||||
algorithm string
|
||||
qop string
|
||||
userhash string
|
||||
}
|
||||
|
||||
func (c *challenge) setValue(k, v string) error {
|
||||
switch k {
|
||||
case "realm":
|
||||
c.realm = v
|
||||
case "domain":
|
||||
c.domain = v
|
||||
case "nonce":
|
||||
c.nonce = v
|
||||
case "opaque":
|
||||
c.opaque = v
|
||||
case "stale":
|
||||
c.stale = v
|
||||
case "algorithm":
|
||||
c.algorithm = v
|
||||
case "qop":
|
||||
c.qop = v
|
||||
case "charset":
|
||||
if strings.ToUpper(v) != "UTF-8" {
|
||||
return ErrDigestCharset
|
||||
}
|
||||
case "userhash":
|
||||
c.userhash = v
|
||||
default:
|
||||
return ErrDigestBadChallenge
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseChallenge(input string) (*challenge, error) {
|
||||
const ws = " \n\r\t"
|
||||
s := strings.Trim(input, ws)
|
||||
if !strings.HasPrefix(s, "Digest ") {
|
||||
return nil, ErrDigestBadChallenge
|
||||
}
|
||||
s = strings.Trim(s[7:], ws)
|
||||
c := &challenge{}
|
||||
b := strings.Builder{}
|
||||
key := ""
|
||||
quoted := false
|
||||
for _, r := range s {
|
||||
switch r {
|
||||
case '"':
|
||||
quoted = !quoted
|
||||
case ',':
|
||||
if quoted {
|
||||
b.WriteRune(r)
|
||||
} else {
|
||||
val := strings.Trim(b.String(), ws)
|
||||
b.Reset()
|
||||
if err := c.setValue(key, val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key = ""
|
||||
}
|
||||
case '=':
|
||||
if quoted {
|
||||
b.WriteRune(r)
|
||||
} else {
|
||||
key = strings.Trim(b.String(), ws)
|
||||
b.Reset()
|
||||
}
|
||||
default:
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
if quoted || (key == "" && b.Len() > 0) {
|
||||
return nil, ErrDigestBadChallenge
|
||||
}
|
||||
if key != "" {
|
||||
val := strings.Trim(b.String(), ws)
|
||||
if err := c.setValue(key, val); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
type credentials struct {
|
||||
username string
|
||||
userhash string
|
||||
realm string
|
||||
nonce string
|
||||
digestURI string
|
||||
algorithm string
|
||||
sessionAlg bool
|
||||
cNonce string
|
||||
opaque string
|
||||
messageQop string
|
||||
nc int
|
||||
method string
|
||||
password string
|
||||
}
|
||||
|
||||
func (c *credentials) authorize() (string, error) {
|
||||
if _, ok := hashFuncs[c.algorithm]; !ok {
|
||||
return "", ErrDigestAlgNotSupported
|
||||
}
|
||||
|
||||
if err := c.validateQop(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := c.resp()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
sl := make([]string, 0, 10)
|
||||
if c.userhash == "true" {
|
||||
// RFC 7616 3.4.4
|
||||
c.username = c.h(fmt.Sprintf("%s:%s", c.username, c.realm))
|
||||
sl = append(sl, fmt.Sprintf(`userhash=%s`, c.userhash))
|
||||
}
|
||||
sl = append(sl, fmt.Sprintf(`username="%s"`, c.username))
|
||||
sl = append(sl, fmt.Sprintf(`realm="%s"`, c.realm))
|
||||
sl = append(sl, fmt.Sprintf(`nonce="%s"`, c.nonce))
|
||||
sl = append(sl, fmt.Sprintf(`uri="%s"`, c.digestURI))
|
||||
sl = append(sl, fmt.Sprintf(`response="%s"`, resp))
|
||||
sl = append(sl, fmt.Sprintf(`algorithm=%s`, c.algorithm))
|
||||
if c.opaque != "" {
|
||||
sl = append(sl, fmt.Sprintf(`opaque="%s"`, c.opaque))
|
||||
}
|
||||
if c.messageQop != "" {
|
||||
sl = append(sl, fmt.Sprintf("qop=%s", c.messageQop))
|
||||
sl = append(sl, fmt.Sprintf("nc=%08x", c.nc))
|
||||
sl = append(sl, fmt.Sprintf(`cnonce="%s"`, c.cNonce))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("Digest %s", strings.Join(sl, ", ")), nil
|
||||
}
|
||||
|
||||
func (c *credentials) validateQop() error {
|
||||
// Currently only supporting auth quality of protection. TODO: add auth-int support
|
||||
// NOTE: cURL support auth-int qop for requests other than POST and PUT (i.e. w/o body) by hashing an empty string
|
||||
// is this applicable for resty? see: https://github.com/curl/curl/blob/307b7543ea1e73ab04e062bdbe4b5bb409eaba3a/lib/vauth/digest.c#L774
|
||||
if c.messageQop == "" {
|
||||
return ErrDigestNoQop
|
||||
}
|
||||
possibleQops := strings.Split(c.messageQop, ",")
|
||||
var authSupport bool
|
||||
for _, qop := range possibleQops {
|
||||
qop = strings.TrimSpace(qop)
|
||||
if qop == "auth" {
|
||||
authSupport = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !authSupport {
|
||||
return ErrDigestQopNotSupported
|
||||
}
|
||||
|
||||
c.messageQop = "auth"
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *credentials) h(data string) string {
|
||||
hfCtor := hashFuncs[c.algorithm]
|
||||
hf := hfCtor()
|
||||
_, _ = hf.Write([]byte(data)) // Hash.Write never returns an error
|
||||
return fmt.Sprintf("%x", hf.Sum(nil))
|
||||
}
|
||||
|
||||
func (c *credentials) resp() (string, error) {
|
||||
c.nc++
|
||||
|
||||
b := make([]byte, 16)
|
||||
_, err := io.ReadFull(rand.Reader, b)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
c.cNonce = fmt.Sprintf("%x", b)[:32]
|
||||
|
||||
ha1 := c.ha1()
|
||||
ha2 := c.ha2()
|
||||
|
||||
return c.kd(ha1, fmt.Sprintf("%s:%08x:%s:%s:%s",
|
||||
c.nonce, c.nc, c.cNonce, c.messageQop, ha2)), nil
|
||||
}
|
||||
|
||||
func (c *credentials) kd(secret, data string) string {
|
||||
return c.h(fmt.Sprintf("%s:%s", secret, data))
|
||||
}
|
||||
|
||||
// RFC 7616 3.4.2
|
||||
func (c *credentials) ha1() string {
|
||||
ret := c.h(fmt.Sprintf("%s:%s:%s", c.username, c.realm, c.password))
|
||||
if c.sessionAlg {
|
||||
return c.h(fmt.Sprintf("%s:%s:%s", ret, c.nonce, c.cNonce))
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
// RFC 7616 3.4.3
|
||||
func (c *credentials) ha2() string {
|
||||
// currently no auth-int support
|
||||
return c.h(fmt.Sprintf("%s:%s", c.method, c.digestURI))
|
||||
}
|
||||
+621
@@ -0,0 +1,621 @@
|
||||
// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
|
||||
// resty source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package resty
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const debugRequestLogKey = "__restyDebugRequestLog"
|
||||
|
||||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
||||
// Request Middleware(s)
|
||||
//_______________________________________________________________________
|
||||
|
||||
func parseRequestURL(c *Client, r *Request) error {
|
||||
if l := len(c.PathParams) + len(c.RawPathParams) + len(r.PathParams) + len(r.RawPathParams); l > 0 {
|
||||
params := make(map[string]string, l)
|
||||
|
||||
// GitHub #103 Path Params
|
||||
for p, v := range r.PathParams {
|
||||
params[p] = url.PathEscape(v)
|
||||
}
|
||||
for p, v := range c.PathParams {
|
||||
if _, ok := params[p]; !ok {
|
||||
params[p] = url.PathEscape(v)
|
||||
}
|
||||
}
|
||||
|
||||
// GitHub #663 Raw Path Params
|
||||
for p, v := range r.RawPathParams {
|
||||
if _, ok := params[p]; !ok {
|
||||
params[p] = v
|
||||
}
|
||||
}
|
||||
for p, v := range c.RawPathParams {
|
||||
if _, ok := params[p]; !ok {
|
||||
params[p] = v
|
||||
}
|
||||
}
|
||||
|
||||
if len(params) > 0 {
|
||||
var prev int
|
||||
buf := acquireBuffer()
|
||||
defer releaseBuffer(buf)
|
||||
// search for the next or first opened curly bracket
|
||||
for curr := strings.Index(r.URL, "{"); curr == 0 || curr >= prev; curr = prev + strings.Index(r.URL[prev:], "{") {
|
||||
// write everything from the previous position up to the current
|
||||
if curr > prev {
|
||||
buf.WriteString(r.URL[prev:curr])
|
||||
}
|
||||
// search for the closed curly bracket from current position
|
||||
next := curr + strings.Index(r.URL[curr:], "}")
|
||||
// if not found, then write the remainder and exit
|
||||
if next < curr {
|
||||
buf.WriteString(r.URL[curr:])
|
||||
prev = len(r.URL)
|
||||
break
|
||||
}
|
||||
// special case for {}, without parameter's name
|
||||
if next == curr+1 {
|
||||
buf.WriteString("{}")
|
||||
} else {
|
||||
// check for the replacement
|
||||
key := r.URL[curr+1 : next]
|
||||
value, ok := params[key]
|
||||
/// keep the original string if the replacement not found
|
||||
if !ok {
|
||||
value = r.URL[curr : next+1]
|
||||
}
|
||||
buf.WriteString(value)
|
||||
}
|
||||
|
||||
// set the previous position after the closed curly bracket
|
||||
prev = next + 1
|
||||
if prev >= len(r.URL) {
|
||||
break
|
||||
}
|
||||
}
|
||||
if buf.Len() > 0 {
|
||||
// write remainder
|
||||
if prev < len(r.URL) {
|
||||
buf.WriteString(r.URL[prev:])
|
||||
}
|
||||
r.URL = buf.String()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parsing request URL
|
||||
reqURL, err := url.Parse(r.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If Request.URL is relative path then added c.HostURL into
|
||||
// the request URL otherwise Request.URL will be used as-is
|
||||
if !reqURL.IsAbs() {
|
||||
r.URL = reqURL.String()
|
||||
if len(r.URL) > 0 && r.URL[0] != '/' {
|
||||
r.URL = "/" + r.URL
|
||||
}
|
||||
|
||||
// TODO: change to use c.BaseURL only in v3.0.0
|
||||
baseURL := c.BaseURL
|
||||
if len(baseURL) == 0 {
|
||||
baseURL = c.HostURL
|
||||
}
|
||||
reqURL, err = url.Parse(baseURL + r.URL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// GH #407 && #318
|
||||
if reqURL.Scheme == "" && len(c.scheme) > 0 {
|
||||
reqURL.Scheme = c.scheme
|
||||
}
|
||||
|
||||
// Adding Query Param
|
||||
if len(c.QueryParam)+len(r.QueryParam) > 0 {
|
||||
for k, v := range c.QueryParam {
|
||||
// skip query parameter if it was set in request
|
||||
if _, ok := r.QueryParam[k]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
r.QueryParam[k] = v[:]
|
||||
}
|
||||
|
||||
// GitHub #123 Preserve query string order partially.
|
||||
// Since not feasible in `SetQuery*` resty methods, because
|
||||
// standard package `url.Encode(...)` sorts the query params
|
||||
// alphabetically
|
||||
if len(r.QueryParam) > 0 {
|
||||
if IsStringEmpty(reqURL.RawQuery) {
|
||||
reqURL.RawQuery = r.QueryParam.Encode()
|
||||
} else {
|
||||
reqURL.RawQuery = reqURL.RawQuery + "&" + r.QueryParam.Encode()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GH#797 Unescape query parameters
|
||||
if r.unescapeQueryParams && len(reqURL.RawQuery) > 0 {
|
||||
// at this point, all errors caught up in the above operations
|
||||
// so ignore the return error on query unescape; I realized
|
||||
// while writing the unit test
|
||||
unescapedQuery, _ := url.QueryUnescape(reqURL.RawQuery)
|
||||
reqURL.RawQuery = strings.ReplaceAll(unescapedQuery, " ", "+") // otherwise request becomes bad request
|
||||
}
|
||||
|
||||
r.URL = reqURL.String()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseRequestHeader(c *Client, r *Request) error {
|
||||
for k, v := range c.Header {
|
||||
if _, ok := r.Header[k]; ok {
|
||||
continue
|
||||
}
|
||||
r.Header[k] = v[:]
|
||||
}
|
||||
|
||||
if IsStringEmpty(r.Header.Get(hdrUserAgentKey)) {
|
||||
r.Header.Set(hdrUserAgentKey, hdrUserAgentValue)
|
||||
}
|
||||
|
||||
if ct := r.Header.Get(hdrContentTypeKey); IsStringEmpty(r.Header.Get(hdrAcceptKey)) && !IsStringEmpty(ct) && (IsJSONType(ct) || IsXMLType(ct)) {
|
||||
r.Header.Set(hdrAcceptKey, r.Header.Get(hdrContentTypeKey))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseRequestBody(c *Client, r *Request) error {
|
||||
if isPayloadSupported(r.Method, c.AllowGetMethodPayload) {
|
||||
switch {
|
||||
case r.isMultiPart: // Handling Multipart
|
||||
if err := handleMultipart(c, r); err != nil {
|
||||
return err
|
||||
}
|
||||
case len(c.FormData) > 0 || len(r.FormData) > 0: // Handling Form Data
|
||||
handleFormData(c, r)
|
||||
case r.Body == nil && r.bodyBuf == nil: // Handling Request body when nil body
|
||||
// Go http library omits Content-Length if body is nil; use http.NoBody to force it if SetContentLength is true
|
||||
r.Body = http.NoBody
|
||||
fallthrough
|
||||
case r.Body != nil: // Handling Request body
|
||||
handleContentType(c, r)
|
||||
|
||||
if err := handleRequestBody(c, r); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// by default resty won't set content length, you can if you want to :)
|
||||
if c.setContentLength || r.setContentLength {
|
||||
if r.bodyBuf == nil {
|
||||
r.Header.Set(hdrContentLengthKey, "0")
|
||||
} else {
|
||||
r.Header.Set(hdrContentLengthKey, strconv.Itoa(r.bodyBuf.Len()))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createHTTPRequest(c *Client, r *Request) (err error) {
|
||||
if r.bodyBuf == nil {
|
||||
if reader, ok := r.Body.(io.Reader); ok && isPayloadSupported(r.Method, c.AllowGetMethodPayload) {
|
||||
r.RawRequest, err = http.NewRequest(r.Method, r.URL, reader)
|
||||
} else if c.setContentLength || r.setContentLength {
|
||||
r.RawRequest, err = http.NewRequest(r.Method, r.URL, http.NoBody)
|
||||
} else {
|
||||
r.RawRequest, err = http.NewRequest(r.Method, r.URL, nil)
|
||||
}
|
||||
} else {
|
||||
// fix data race: must deep copy.
|
||||
bodyBuf := bytes.NewBuffer(append([]byte{}, r.bodyBuf.Bytes()...))
|
||||
r.RawRequest, err = http.NewRequest(r.Method, r.URL, bodyBuf)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Assign close connection option
|
||||
r.RawRequest.Close = c.closeConnection
|
||||
|
||||
// Add headers into http request
|
||||
r.RawRequest.Header = r.Header.Clone()
|
||||
|
||||
// Add cookies from client instance into http request
|
||||
for _, cookie := range c.Cookies {
|
||||
r.RawRequest.AddCookie(cookie)
|
||||
}
|
||||
|
||||
// Add cookies from request instance into http request
|
||||
for _, cookie := range r.Cookies {
|
||||
r.RawRequest.AddCookie(cookie)
|
||||
}
|
||||
|
||||
// Enable trace
|
||||
if c.trace || r.trace {
|
||||
r.clientTrace = &clientTrace{}
|
||||
r.ctx = r.clientTrace.createContext(r.Context())
|
||||
}
|
||||
|
||||
// Use context if it was specified
|
||||
if r.ctx != nil {
|
||||
r.RawRequest = r.RawRequest.WithContext(r.ctx)
|
||||
}
|
||||
|
||||
// assign get body func for the underlying raw request instance
|
||||
if r.RawRequest.GetBody == nil {
|
||||
bodyCopy, err := getBodyCopy(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if bodyCopy != nil {
|
||||
buf := bodyCopy.Bytes()
|
||||
r.RawRequest.GetBody = func() (io.ReadCloser, error) {
|
||||
b := bytes.NewReader(buf)
|
||||
return io.NopCloser(b), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func addCredentials(c *Client, r *Request) error {
|
||||
var isBasicAuth bool
|
||||
// Basic Auth
|
||||
if r.UserInfo != nil { // takes precedence
|
||||
r.RawRequest.SetBasicAuth(r.UserInfo.Username, r.UserInfo.Password)
|
||||
isBasicAuth = true
|
||||
} else if c.UserInfo != nil {
|
||||
r.RawRequest.SetBasicAuth(c.UserInfo.Username, c.UserInfo.Password)
|
||||
isBasicAuth = true
|
||||
}
|
||||
|
||||
if !c.DisableWarn {
|
||||
if isBasicAuth && !strings.HasPrefix(r.URL, "https") {
|
||||
r.log.Warnf("Using Basic Auth in HTTP mode is not secure, use HTTPS")
|
||||
}
|
||||
}
|
||||
|
||||
// Build the token Auth header
|
||||
if !IsStringEmpty(r.Token) {
|
||||
r.RawRequest.Header.Set(c.HeaderAuthorizationKey, strings.TrimSpace(r.AuthScheme+" "+r.Token))
|
||||
} else if !IsStringEmpty(c.Token) {
|
||||
r.RawRequest.Header.Set(c.HeaderAuthorizationKey, strings.TrimSpace(r.AuthScheme+" "+c.Token))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func createCurlCmd(c *Client, r *Request) (err error) {
|
||||
if r.Debug && r.generateCurlOnDebug {
|
||||
if r.resultCurlCmd == nil {
|
||||
r.resultCurlCmd = new(string)
|
||||
}
|
||||
*r.resultCurlCmd = buildCurlRequest(r.RawRequest, c.httpClient.Jar)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func requestLogger(c *Client, r *Request) error {
|
||||
if r.Debug {
|
||||
rr := r.RawRequest
|
||||
rh := copyHeaders(rr.Header)
|
||||
if c.GetClient().Jar != nil {
|
||||
for _, cookie := range c.GetClient().Jar.Cookies(r.RawRequest.URL) {
|
||||
s := fmt.Sprintf("%s=%s", cookie.Name, cookie.Value)
|
||||
if c := rh.Get("Cookie"); c != "" {
|
||||
rh.Set("Cookie", c+"; "+s)
|
||||
} else {
|
||||
rh.Set("Cookie", s)
|
||||
}
|
||||
}
|
||||
}
|
||||
rl := &RequestLog{Header: rh, Body: r.fmtBodyString(c.debugBodySizeLimit)}
|
||||
if c.requestLog != nil {
|
||||
if err := c.requestLog(rl); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
reqLog := "\n==============================================================================\n"
|
||||
|
||||
if r.Debug && r.generateCurlOnDebug {
|
||||
reqLog += "~~~ REQUEST(CURL) ~~~\n" +
|
||||
fmt.Sprintf(" %v\n", *r.resultCurlCmd)
|
||||
}
|
||||
|
||||
reqLog += "~~~ REQUEST ~~~\n" +
|
||||
fmt.Sprintf("%s %s %s\n", r.Method, rr.URL.RequestURI(), rr.Proto) +
|
||||
fmt.Sprintf("HOST : %s\n", rr.URL.Host) +
|
||||
fmt.Sprintf("HEADERS:\n%s\n", composeHeaders(c, r, rl.Header)) +
|
||||
fmt.Sprintf("BODY :\n%v\n", rl.Body) +
|
||||
"------------------------------------------------------------------------------\n"
|
||||
|
||||
r.initValuesMap()
|
||||
r.values[debugRequestLogKey] = reqLog
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
||||
// Response Middleware(s)
|
||||
//_______________________________________________________________________
|
||||
|
||||
func responseLogger(c *Client, res *Response) error {
|
||||
if res.Request.Debug {
|
||||
rl := &ResponseLog{Header: copyHeaders(res.Header()), Body: res.fmtBodyString(c.debugBodySizeLimit)}
|
||||
if c.responseLog != nil {
|
||||
if err := c.responseLog(rl); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
debugLog := res.Request.values[debugRequestLogKey].(string)
|
||||
debugLog += "~~~ RESPONSE ~~~\n" +
|
||||
fmt.Sprintf("STATUS : %s\n", res.Status()) +
|
||||
fmt.Sprintf("PROTO : %s\n", res.Proto()) +
|
||||
fmt.Sprintf("RECEIVED AT : %v\n", res.ReceivedAt().Format(time.RFC3339Nano)) +
|
||||
fmt.Sprintf("TIME DURATION: %v\n", res.Time()) +
|
||||
"HEADERS :\n" +
|
||||
composeHeaders(c, res.Request, rl.Header) + "\n"
|
||||
if res.Request.isSaveResponse {
|
||||
debugLog += "BODY :\n***** RESPONSE WRITTEN INTO FILE *****\n"
|
||||
} else {
|
||||
debugLog += fmt.Sprintf("BODY :\n%v\n", rl.Body)
|
||||
}
|
||||
debugLog += "==============================================================================\n"
|
||||
|
||||
res.Request.log.Debugf("%s", debugLog)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseResponseBody(c *Client, res *Response) (err error) {
|
||||
if res.StatusCode() == http.StatusNoContent {
|
||||
res.Request.Error = nil
|
||||
return
|
||||
}
|
||||
// Handles only JSON or XML content type
|
||||
ct := firstNonEmpty(res.Request.forceContentType, res.Header().Get(hdrContentTypeKey), res.Request.fallbackContentType)
|
||||
if IsJSONType(ct) || IsXMLType(ct) {
|
||||
// HTTP status code > 199 and < 300, considered as Result
|
||||
if res.IsSuccess() {
|
||||
res.Request.Error = nil
|
||||
if res.Request.Result != nil {
|
||||
err = Unmarshalc(c, ct, res.body, res.Request.Result)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// HTTP status code > 399, considered as Error
|
||||
if res.IsError() {
|
||||
// global error interface
|
||||
if res.Request.Error == nil && c.Error != nil {
|
||||
res.Request.Error = reflect.New(c.Error).Interface()
|
||||
}
|
||||
|
||||
if res.Request.Error != nil {
|
||||
unmarshalErr := Unmarshalc(c, ct, res.body, res.Request.Error)
|
||||
if unmarshalErr != nil {
|
||||
c.log.Warnf("Cannot unmarshal response body: %s", unmarshalErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func handleMultipart(c *Client, r *Request) error {
|
||||
r.bodyBuf = acquireBuffer()
|
||||
w := multipart.NewWriter(r.bodyBuf)
|
||||
|
||||
// Set boundary if not set by user
|
||||
if r.multipartBoundary != "" {
|
||||
if err := w.SetBoundary(r.multipartBoundary); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range c.FormData {
|
||||
for _, iv := range v {
|
||||
if err := w.WriteField(k, iv); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for k, v := range r.FormData {
|
||||
for _, iv := range v {
|
||||
if strings.HasPrefix(k, "@") { // file
|
||||
if err := addFile(w, k[1:], iv); err != nil {
|
||||
return err
|
||||
}
|
||||
} else { // form value
|
||||
if err := w.WriteField(k, iv); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// #21 - adding io.Reader support
|
||||
for _, f := range r.multipartFiles {
|
||||
if err := addFileReader(w, f); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// GitHub #130 adding multipart field support with content type
|
||||
for _, mf := range r.multipartFields {
|
||||
if err := addMultipartFormField(w, mf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
r.Header.Set(hdrContentTypeKey, w.FormDataContentType())
|
||||
return w.Close()
|
||||
}
|
||||
|
||||
func handleFormData(c *Client, r *Request) {
|
||||
for k, v := range c.FormData {
|
||||
if _, ok := r.FormData[k]; ok {
|
||||
continue
|
||||
}
|
||||
r.FormData[k] = v[:]
|
||||
}
|
||||
|
||||
r.bodyBuf = acquireBuffer()
|
||||
r.bodyBuf.WriteString(r.FormData.Encode())
|
||||
r.Header.Set(hdrContentTypeKey, formContentType)
|
||||
r.isFormData = true
|
||||
}
|
||||
|
||||
func handleContentType(c *Client, r *Request) {
|
||||
if r.Body == http.NoBody {
|
||||
return
|
||||
}
|
||||
contentType := r.Header.Get(hdrContentTypeKey)
|
||||
if IsStringEmpty(contentType) {
|
||||
contentType = DetectContentType(r.Body)
|
||||
r.Header.Set(hdrContentTypeKey, contentType)
|
||||
}
|
||||
}
|
||||
|
||||
func handleRequestBody(c *Client, r *Request) error {
|
||||
var bodyBytes []byte
|
||||
r.bodyBuf = nil
|
||||
|
||||
switch body := r.Body.(type) {
|
||||
case io.Reader:
|
||||
if c.setContentLength || r.setContentLength { // keep backward compatibility
|
||||
r.bodyBuf = acquireBuffer()
|
||||
if _, err := r.bodyBuf.ReadFrom(body); err != nil {
|
||||
return err
|
||||
}
|
||||
r.Body = nil
|
||||
} else {
|
||||
// Otherwise buffer less processing for `io.Reader`, sounds good.
|
||||
return nil
|
||||
}
|
||||
case []byte:
|
||||
bodyBytes = body
|
||||
case string:
|
||||
bodyBytes = []byte(body)
|
||||
default:
|
||||
contentType := r.Header.Get(hdrContentTypeKey)
|
||||
kind := kindOf(r.Body)
|
||||
var err error
|
||||
if IsJSONType(contentType) && (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) {
|
||||
r.bodyBuf, err = jsonMarshal(c, r, r.Body)
|
||||
} else if IsXMLType(contentType) && (kind == reflect.Struct) {
|
||||
bodyBytes, err = c.XMLMarshal(r.Body)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if bodyBytes == nil && r.bodyBuf == nil {
|
||||
return errors.New("unsupported 'Body' type/value")
|
||||
}
|
||||
|
||||
// []byte into Buffer
|
||||
if bodyBytes != nil && r.bodyBuf == nil {
|
||||
r.bodyBuf = acquireBuffer()
|
||||
_, _ = r.bodyBuf.Write(bodyBytes)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveResponseIntoFile(c *Client, res *Response) error {
|
||||
if res.Request.isSaveResponse {
|
||||
file := ""
|
||||
|
||||
if len(c.outputDirectory) > 0 && !filepath.IsAbs(res.Request.outputFile) {
|
||||
file += c.outputDirectory + string(filepath.Separator)
|
||||
}
|
||||
|
||||
file = filepath.Clean(file + res.Request.outputFile)
|
||||
if err := createDirectory(filepath.Dir(file)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
outFile, err := os.Create(file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer closeq(outFile)
|
||||
|
||||
// io.Copy reads maximum 32kb size, it is perfect for large file download too
|
||||
defer closeq(res.RawResponse.Body)
|
||||
|
||||
written, err := io.Copy(outFile, res.RawResponse.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
res.size = written
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getBodyCopy(r *Request) (*bytes.Buffer, error) {
|
||||
// If r.bodyBuf present, return the copy
|
||||
if r.bodyBuf != nil {
|
||||
bodyCopy := acquireBuffer()
|
||||
if _, err := io.Copy(bodyCopy, bytes.NewReader(r.bodyBuf.Bytes())); err != nil {
|
||||
// cannot use io.Copy(bodyCopy, r.bodyBuf) because io.Copy reset r.bodyBuf
|
||||
return nil, err
|
||||
}
|
||||
return bodyCopy, nil
|
||||
}
|
||||
|
||||
// Maybe body is `io.Reader`.
|
||||
// Note: Resty user have to watchout for large body size of `io.Reader`
|
||||
if r.RawRequest.Body != nil {
|
||||
b, err := io.ReadAll(r.RawRequest.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Restore the Body
|
||||
closeq(r.RawRequest.Body)
|
||||
r.RawRequest.Body = io.NopCloser(bytes.NewBuffer(b))
|
||||
|
||||
// Return the Body bytes
|
||||
return bytes.NewBuffer(b), nil
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
|
||||
// resty source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package resty
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAutoRedirectDisabled = errors.New("auto redirect is disabled")
|
||||
)
|
||||
|
||||
type (
|
||||
// RedirectPolicy to regulate the redirects in the Resty client.
|
||||
// Objects implementing the [RedirectPolicy] interface can be registered as
|
||||
//
|
||||
// Apply function should return nil to continue the redirect journey; otherwise
|
||||
// return error to stop the redirect.
|
||||
RedirectPolicy interface {
|
||||
Apply(req *http.Request, via []*http.Request) error
|
||||
}
|
||||
|
||||
// The [RedirectPolicyFunc] type is an adapter to allow the use of ordinary
|
||||
// functions as [RedirectPolicy]. If `f` is a function with the appropriate
|
||||
// signature, RedirectPolicyFunc(f) is a RedirectPolicy object that calls `f`.
|
||||
RedirectPolicyFunc func(*http.Request, []*http.Request) error
|
||||
)
|
||||
|
||||
// Apply calls f(req, via).
|
||||
func (f RedirectPolicyFunc) Apply(req *http.Request, via []*http.Request) error {
|
||||
return f(req, via)
|
||||
}
|
||||
|
||||
// NoRedirectPolicy is used to disable redirects in the Resty client
|
||||
//
|
||||
// resty.SetRedirectPolicy(NoRedirectPolicy())
|
||||
func NoRedirectPolicy() RedirectPolicy {
|
||||
return RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
|
||||
return ErrAutoRedirectDisabled
|
||||
})
|
||||
}
|
||||
|
||||
// FlexibleRedirectPolicy method is convenient for creating several redirect policies for Resty clients.
|
||||
//
|
||||
// resty.SetRedirectPolicy(FlexibleRedirectPolicy(20))
|
||||
func FlexibleRedirectPolicy(noOfRedirect int) RedirectPolicy {
|
||||
return RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
|
||||
if len(via) >= noOfRedirect {
|
||||
return fmt.Errorf("stopped after %d redirects", noOfRedirect)
|
||||
}
|
||||
checkHostAndAddHeaders(req, via[0])
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DomainCheckRedirectPolicy method is convenient for defining domain name redirect rules in Resty clients.
|
||||
// Redirect is allowed only for the host mentioned in the policy.
|
||||
//
|
||||
// resty.SetRedirectPolicy(DomainCheckRedirectPolicy("host1.com", "host2.org", "host3.net"))
|
||||
func DomainCheckRedirectPolicy(hostnames ...string) RedirectPolicy {
|
||||
hosts := make(map[string]bool)
|
||||
for _, h := range hostnames {
|
||||
hosts[strings.ToLower(h)] = true
|
||||
}
|
||||
|
||||
fn := RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error {
|
||||
if ok := hosts[getHostname(req.URL.Host)]; !ok {
|
||||
return errors.New("redirect is not allowed as per DomainCheckRedirectPolicy")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return fn
|
||||
}
|
||||
|
||||
func getHostname(host string) (hostname string) {
|
||||
if strings.Index(host, ":") > 0 {
|
||||
host, _, _ = net.SplitHostPort(host)
|
||||
}
|
||||
hostname = strings.ToLower(host)
|
||||
return
|
||||
}
|
||||
|
||||
// By default, Golang will not redirect request headers.
|
||||
// After reading through the various discussion comments from the thread -
|
||||
// https://github.com/golang/go/issues/4800
|
||||
// Resty will add all the headers during a redirect for the same host and
|
||||
// adds library user-agent if the Host is different.
|
||||
func checkHostAndAddHeaders(cur *http.Request, pre *http.Request) {
|
||||
curHostname := getHostname(cur.URL.Host)
|
||||
preHostname := getHostname(pre.URL.Host)
|
||||
if strings.EqualFold(curHostname, preHostname) {
|
||||
for key, val := range pre.Header {
|
||||
cur.Header[key] = val
|
||||
}
|
||||
} else { // only library User-Agent header is added
|
||||
cur.Header.Set(hdrUserAgentKey, hdrUserAgentValue)
|
||||
}
|
||||
}
|
||||
+1209
File diff suppressed because it is too large
Load Diff
+195
@@ -0,0 +1,195 @@
|
||||
// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
|
||||
// resty source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package resty
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
||||
// Response struct and methods
|
||||
//_______________________________________________________________________
|
||||
|
||||
// Response struct holds response values of executed requests.
|
||||
type Response struct {
|
||||
Request *Request
|
||||
RawResponse *http.Response
|
||||
|
||||
body []byte
|
||||
size int64
|
||||
receivedAt time.Time
|
||||
}
|
||||
|
||||
// Body method returns the HTTP response as `[]byte` slice for the executed request.
|
||||
//
|
||||
// NOTE: [Response.Body] might be nil if [Request.SetOutput] is used.
|
||||
// Also see [Request.SetDoNotParseResponse], [Client.SetDoNotParseResponse]
|
||||
func (r *Response) Body() []byte {
|
||||
if r.RawResponse == nil {
|
||||
return []byte{}
|
||||
}
|
||||
return r.body
|
||||
}
|
||||
|
||||
// SetBody method sets [Response] body in byte slice. Typically,
|
||||
// It is helpful for test cases.
|
||||
//
|
||||
// resp.SetBody([]byte("This is test body content"))
|
||||
// resp.SetBody(nil)
|
||||
func (r *Response) SetBody(b []byte) *Response {
|
||||
r.body = b
|
||||
return r
|
||||
}
|
||||
|
||||
// Status method returns the HTTP status string for the executed request.
|
||||
//
|
||||
// Example: 200 OK
|
||||
func (r *Response) Status() string {
|
||||
if r.RawResponse == nil {
|
||||
return ""
|
||||
}
|
||||
return r.RawResponse.Status
|
||||
}
|
||||
|
||||
// StatusCode method returns the HTTP status code for the executed request.
|
||||
//
|
||||
// Example: 200
|
||||
func (r *Response) StatusCode() int {
|
||||
if r.RawResponse == nil {
|
||||
return 0
|
||||
}
|
||||
return r.RawResponse.StatusCode
|
||||
}
|
||||
|
||||
// Proto method returns the HTTP response protocol used for the request.
|
||||
func (r *Response) Proto() string {
|
||||
if r.RawResponse == nil {
|
||||
return ""
|
||||
}
|
||||
return r.RawResponse.Proto
|
||||
}
|
||||
|
||||
// Result method returns the response value as an object if it has one
|
||||
//
|
||||
// See [Request.SetResult]
|
||||
func (r *Response) Result() interface{} {
|
||||
return r.Request.Result
|
||||
}
|
||||
|
||||
// Error method returns the error object if it has one
|
||||
//
|
||||
// See [Request.SetError], [Client.SetError]
|
||||
func (r *Response) Error() interface{} {
|
||||
return r.Request.Error
|
||||
}
|
||||
|
||||
// Header method returns the response headers
|
||||
func (r *Response) Header() http.Header {
|
||||
if r.RawResponse == nil {
|
||||
return http.Header{}
|
||||
}
|
||||
return r.RawResponse.Header
|
||||
}
|
||||
|
||||
// Cookies method to returns all the response cookies
|
||||
func (r *Response) Cookies() []*http.Cookie {
|
||||
if r.RawResponse == nil {
|
||||
return make([]*http.Cookie, 0)
|
||||
}
|
||||
return r.RawResponse.Cookies()
|
||||
}
|
||||
|
||||
// String method returns the body of the HTTP response as a `string`.
|
||||
// It returns an empty string if it is nil or the body is zero length.
|
||||
func (r *Response) String() string {
|
||||
if len(r.body) == 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(r.body))
|
||||
}
|
||||
|
||||
// Time method returns the duration of HTTP response time from the request we sent
|
||||
// and received a request.
|
||||
//
|
||||
// See [Response.ReceivedAt] to know when the client received a response and see
|
||||
// `Response.Request.Time` to know when the client sent a request.
|
||||
func (r *Response) Time() time.Duration {
|
||||
if r.Request.clientTrace != nil {
|
||||
return r.Request.TraceInfo().TotalTime
|
||||
}
|
||||
return r.receivedAt.Sub(r.Request.Time)
|
||||
}
|
||||
|
||||
// ReceivedAt method returns the time we received a response from the server for the request.
|
||||
func (r *Response) ReceivedAt() time.Time {
|
||||
return r.receivedAt
|
||||
}
|
||||
|
||||
// Size method returns the HTTP response size in bytes. Yeah, you can rely on HTTP `Content-Length`
|
||||
// header, however it won't be available for chucked transfer/compressed response.
|
||||
// Since Resty captures response size details when processing the response body
|
||||
// when possible. So that users get the actual size of response bytes.
|
||||
func (r *Response) Size() int64 {
|
||||
return r.size
|
||||
}
|
||||
|
||||
// RawBody method exposes the HTTP raw response body. Use this method in conjunction with
|
||||
// [Client.SetDoNotParseResponse] or [Request.SetDoNotParseResponse]
|
||||
// option; otherwise, you get an error as `read err: http: read on closed response body.`
|
||||
//
|
||||
// Do not forget to close the body, otherwise you might get into connection leaks, no connection reuse.
|
||||
// You have taken over the control of response parsing from Resty.
|
||||
func (r *Response) RawBody() io.ReadCloser {
|
||||
if r.RawResponse == nil {
|
||||
return nil
|
||||
}
|
||||
return r.RawResponse.Body
|
||||
}
|
||||
|
||||
// IsSuccess method returns true if HTTP status `code >= 200 and <= 299` otherwise false.
|
||||
func (r *Response) IsSuccess() bool {
|
||||
return r.StatusCode() > 199 && r.StatusCode() < 300
|
||||
}
|
||||
|
||||
// IsError method returns true if HTTP status `code >= 400` otherwise false.
|
||||
func (r *Response) IsError() bool {
|
||||
return r.StatusCode() > 399
|
||||
}
|
||||
|
||||
func (r *Response) setReceivedAt() {
|
||||
r.receivedAt = time.Now()
|
||||
if r.Request.clientTrace != nil {
|
||||
r.Request.clientTrace.endTime = r.receivedAt
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Response) fmtBodyString(sl int64) string {
|
||||
if r.Request.client.notParseResponse || r.Request.notParseResponse {
|
||||
return "***** DO NOT PARSE RESPONSE - Enabled *****"
|
||||
}
|
||||
if len(r.body) > 0 {
|
||||
if int64(len(r.body)) > sl {
|
||||
return fmt.Sprintf("***** RESPONSE TOO LARGE (size - %d) *****", len(r.body))
|
||||
}
|
||||
ct := r.Header().Get(hdrContentTypeKey)
|
||||
if IsJSONType(ct) {
|
||||
out := acquireBuffer()
|
||||
defer releaseBuffer(out)
|
||||
err := json.Indent(out, r.body, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Sprintf("*** Error: Unable to format response body - \"%s\" ***\n\nLog Body as-is:\n%s", err, r.String())
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
return r.String()
|
||||
}
|
||||
|
||||
return "***** NO CONTENT *****"
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
|
||||
// resty source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package resty provides Simple HTTP and REST client library for Go.
|
||||
package resty
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
|
||||
"golang.org/x/net/publicsuffix"
|
||||
)
|
||||
|
||||
// Version # of resty
|
||||
const Version = "2.17.2"
|
||||
|
||||
// New method creates a new Resty client.
|
||||
func New() *Client {
|
||||
cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
|
||||
return createClient(&http.Client{
|
||||
Jar: cookieJar,
|
||||
})
|
||||
}
|
||||
|
||||
// NewWithClient method creates a new Resty client with given [http.Client].
|
||||
func NewWithClient(hc *http.Client) *Client {
|
||||
return createClient(hc)
|
||||
}
|
||||
|
||||
// NewWithLocalAddr method creates a new Resty client with the given Local Address.
|
||||
// to dial from.
|
||||
func NewWithLocalAddr(localAddr net.Addr) *Client {
|
||||
cookieJar, _ := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List})
|
||||
return createClient(&http.Client{
|
||||
Jar: cookieJar,
|
||||
Transport: createTransport(localAddr),
|
||||
})
|
||||
}
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
|
||||
// resty source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package resty
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"math"
|
||||
"math/rand"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultMaxRetries = 3
|
||||
defaultWaitTime = time.Duration(100) * time.Millisecond
|
||||
defaultMaxWaitTime = time.Duration(2000) * time.Millisecond
|
||||
)
|
||||
|
||||
type (
|
||||
// Option is to create convenient retry options like wait time, max retries, etc.
|
||||
Option func(*Options)
|
||||
|
||||
// RetryConditionFunc type is for the retry condition function
|
||||
// input: non-nil Response OR request execution error
|
||||
RetryConditionFunc func(*Response, error) bool
|
||||
|
||||
// OnRetryFunc is for side-effecting functions triggered on retry
|
||||
OnRetryFunc func(*Response, error)
|
||||
|
||||
// RetryAfterFunc returns time to wait before retry
|
||||
// For example, it can parse HTTP Retry-After header
|
||||
// https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
|
||||
// Non-nil error is returned if it is found that the request is not retryable
|
||||
// (0, nil) is a special result that means 'use default algorithm'
|
||||
RetryAfterFunc func(*Client, *Response) (time.Duration, error)
|
||||
|
||||
// Options struct is used to hold retry settings.
|
||||
Options struct {
|
||||
maxRetries int
|
||||
waitTime time.Duration
|
||||
maxWaitTime time.Duration
|
||||
retryConditions []RetryConditionFunc
|
||||
retryHooks []OnRetryFunc
|
||||
resetReaders bool
|
||||
}
|
||||
)
|
||||
|
||||
// Retries sets the max number of retries
|
||||
func Retries(value int) Option {
|
||||
return func(o *Options) {
|
||||
o.maxRetries = value
|
||||
}
|
||||
}
|
||||
|
||||
// WaitTime sets the default wait time to sleep between requests
|
||||
func WaitTime(value time.Duration) Option {
|
||||
return func(o *Options) {
|
||||
o.waitTime = value
|
||||
}
|
||||
}
|
||||
|
||||
// MaxWaitTime sets the max wait time to sleep between requests
|
||||
func MaxWaitTime(value time.Duration) Option {
|
||||
return func(o *Options) {
|
||||
o.maxWaitTime = value
|
||||
}
|
||||
}
|
||||
|
||||
// RetryConditions sets the conditions that will be checked for retry
|
||||
func RetryConditions(conditions []RetryConditionFunc) Option {
|
||||
return func(o *Options) {
|
||||
o.retryConditions = conditions
|
||||
}
|
||||
}
|
||||
|
||||
// RetryHooks sets the hooks that will be executed after each retry
|
||||
func RetryHooks(hooks []OnRetryFunc) Option {
|
||||
return func(o *Options) {
|
||||
o.retryHooks = hooks
|
||||
}
|
||||
}
|
||||
|
||||
// ResetMultipartReaders sets a boolean value which will lead the start being seeked out
|
||||
// on all multipart file readers if they implement [io.ReadSeeker]
|
||||
func ResetMultipartReaders(value bool) Option {
|
||||
return func(o *Options) {
|
||||
o.resetReaders = value
|
||||
}
|
||||
}
|
||||
|
||||
// Backoff retries with increasing timeout duration up until X amount of retries
|
||||
// (Default is 3 attempts, Override with option Retries(n))
|
||||
func Backoff(operation func() (*Response, error), options ...Option) error {
|
||||
// Defaults
|
||||
opts := Options{
|
||||
maxRetries: defaultMaxRetries,
|
||||
waitTime: defaultWaitTime,
|
||||
maxWaitTime: defaultMaxWaitTime,
|
||||
retryConditions: []RetryConditionFunc{},
|
||||
}
|
||||
|
||||
for _, o := range options {
|
||||
o(&opts)
|
||||
}
|
||||
|
||||
var (
|
||||
resp *Response
|
||||
err error
|
||||
)
|
||||
|
||||
for attempt := 0; attempt <= opts.maxRetries; attempt++ {
|
||||
resp, err = operation()
|
||||
ctx := context.Background()
|
||||
if resp != nil && resp.Request.ctx != nil {
|
||||
ctx = resp.Request.ctx
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err1 := unwrapNoRetryErr(err) // raw error, it used for return users callback.
|
||||
needsRetry := err != nil && err == err1 // retry on a few operation errors by default
|
||||
|
||||
for _, condition := range opts.retryConditions {
|
||||
needsRetry = condition(resp, err1)
|
||||
if needsRetry {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !needsRetry {
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.resetReaders {
|
||||
if err := resetFileReaders(resp.Request.multipartFiles); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := resetFieldReaders(resp.Request.multipartFields); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for _, hook := range opts.retryHooks {
|
||||
hook(resp, err)
|
||||
}
|
||||
|
||||
// Don't need to wait when no retries left.
|
||||
// Still run retry hooks even on last retry to keep compatibility.
|
||||
if attempt == opts.maxRetries {
|
||||
return err
|
||||
}
|
||||
|
||||
waitTime, err2 := sleepDuration(resp, opts.waitTime, opts.maxWaitTime, attempt)
|
||||
if err2 != nil {
|
||||
if err == nil {
|
||||
err = err2
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
select {
|
||||
case <-time.After(waitTime):
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func sleepDuration(resp *Response, min, max time.Duration, attempt int) (time.Duration, error) {
|
||||
const maxInt = 1<<31 - 1 // max int for arch 386
|
||||
if max < 0 {
|
||||
max = maxInt
|
||||
}
|
||||
if resp == nil {
|
||||
return jitterBackoff(min, max, attempt), nil
|
||||
}
|
||||
|
||||
retryAfterFunc := resp.Request.client.RetryAfter
|
||||
|
||||
// Check for custom callback
|
||||
if retryAfterFunc == nil {
|
||||
return jitterBackoff(min, max, attempt), nil
|
||||
}
|
||||
|
||||
result, err := retryAfterFunc(resp.Request.client, resp)
|
||||
if err != nil {
|
||||
return 0, err // i.e. 'API quota exceeded'
|
||||
}
|
||||
if result == 0 {
|
||||
return jitterBackoff(min, max, attempt), nil
|
||||
}
|
||||
if result < 0 || max < result {
|
||||
result = max
|
||||
}
|
||||
if result < min {
|
||||
result = min
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Return capped exponential backoff with jitter
|
||||
// https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
|
||||
func jitterBackoff(min, max time.Duration, attempt int) time.Duration {
|
||||
base := float64(min)
|
||||
capLevel := float64(max)
|
||||
|
||||
temp := math.Min(capLevel, base*math.Exp2(float64(attempt)))
|
||||
ri := time.Duration(temp / 2)
|
||||
if ri == 0 {
|
||||
ri = time.Nanosecond
|
||||
}
|
||||
result := randDuration(ri)
|
||||
|
||||
if result < min {
|
||||
result = min
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
var rnd = newRnd()
|
||||
var rndMu sync.Mutex
|
||||
|
||||
func randDuration(center time.Duration) time.Duration {
|
||||
rndMu.Lock()
|
||||
defer rndMu.Unlock()
|
||||
|
||||
var ri = int64(center)
|
||||
var jitter = rnd.Int63n(ri)
|
||||
return time.Duration(math.Abs(float64(ri + jitter)))
|
||||
}
|
||||
|
||||
func newRnd() *rand.Rand {
|
||||
var seed = time.Now().UnixNano()
|
||||
var src = rand.NewSource(seed)
|
||||
return rand.New(src)
|
||||
}
|
||||
|
||||
func resetFileReaders(files []*File) error {
|
||||
for _, f := range files {
|
||||
if rs, ok := f.Reader.(io.ReadSeeker); ok {
|
||||
if _, err := rs.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func resetFieldReaders(fields []*MultipartField) error {
|
||||
for _, f := range fields {
|
||||
if rs, ok := f.Reader.(io.ReadSeeker); ok {
|
||||
if _, err := rs.Seek(0, io.SeekStart); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "shellescape",
|
||||
srcs = ["shellescape.go"],
|
||||
importpath = "github.com/go-resty/resty/v2/shellescape",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":shellescape",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
// Copyright (c) 2015-present Jeevanandam M (jeeva@myjeeva.com)
|
||||
// 2024 Ahuigo (https://github.com/ahuigo)
|
||||
// All rights reserved.
|
||||
// resty source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
/*
|
||||
Package shellescape provides the methods to escape arbitrary
|
||||
strings for a safe use as command line arguments in the most common
|
||||
POSIX shells.
|
||||
|
||||
The original Python package which this work was inspired by can be found
|
||||
at https://pypi.python.org/pypi/shellescape.
|
||||
*/
|
||||
package shellescape
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var pattern *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
pattern = regexp.MustCompile(`[^\w@%+=:,./-]`)
|
||||
}
|
||||
|
||||
// Quote method returns a shell-escaped version of the string. The returned value
|
||||
// can safely be used as one token in a shell command line.
|
||||
func Quote(s string) string {
|
||||
if len(s) == 0 {
|
||||
return "''"
|
||||
}
|
||||
|
||||
if pattern.MatchString(s) {
|
||||
return "'" + strings.ReplaceAll(s, "'", "'\"'\"'") + "'"
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
|
||||
// resty source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package resty
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"net/http/httptrace"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
||||
// TraceInfo struct
|
||||
//_______________________________________________________________________
|
||||
|
||||
// TraceInfo struct is used to provide request trace info such as DNS lookup
|
||||
// duration, Connection obtain duration, Server processing duration, etc.
|
||||
type TraceInfo struct {
|
||||
// DNSLookup is the duration that transport took to perform
|
||||
// DNS lookup.
|
||||
DNSLookup time.Duration
|
||||
|
||||
// ConnTime is the duration it took to obtain a successful connection.
|
||||
ConnTime time.Duration
|
||||
|
||||
// TCPConnTime is the duration it took to obtain the TCP connection.
|
||||
TCPConnTime time.Duration
|
||||
|
||||
// TLSHandshake is the duration of the TLS handshake.
|
||||
TLSHandshake time.Duration
|
||||
|
||||
// ServerTime is the server's duration for responding to the first byte.
|
||||
ServerTime time.Duration
|
||||
|
||||
// ResponseTime is the duration since the first response byte from the server to
|
||||
// request completion.
|
||||
ResponseTime time.Duration
|
||||
|
||||
// TotalTime is the duration of the total time request taken end-to-end.
|
||||
TotalTime time.Duration
|
||||
|
||||
// IsConnReused is whether this connection has been previously
|
||||
// used for another HTTP request.
|
||||
IsConnReused bool
|
||||
|
||||
// IsConnWasIdle is whether this connection was obtained from an
|
||||
// idle pool.
|
||||
IsConnWasIdle bool
|
||||
|
||||
// ConnIdleTime is the duration how long the connection that was previously
|
||||
// idle, if IsConnWasIdle is true.
|
||||
ConnIdleTime time.Duration
|
||||
|
||||
// RequestAttempt is to represent the request attempt made during a Resty
|
||||
// request execution flow, including retry count.
|
||||
RequestAttempt int
|
||||
|
||||
// RemoteAddr returns the remote network address.
|
||||
RemoteAddr net.Addr
|
||||
}
|
||||
|
||||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
||||
// ClientTrace struct and its methods
|
||||
//_______________________________________________________________________
|
||||
|
||||
// clientTrace struct maps the [httptrace.ClientTrace] hooks into Fields
|
||||
// with the same naming for easy understanding. Plus additional insights
|
||||
// [Request].
|
||||
type clientTrace struct {
|
||||
lock sync.RWMutex
|
||||
getConn time.Time
|
||||
dnsStart time.Time
|
||||
dnsDone time.Time
|
||||
connectDone time.Time
|
||||
tlsHandshakeStart time.Time
|
||||
tlsHandshakeDone time.Time
|
||||
gotConn time.Time
|
||||
gotFirstResponseByte time.Time
|
||||
endTime time.Time
|
||||
gotConnInfo httptrace.GotConnInfo
|
||||
}
|
||||
|
||||
func (t *clientTrace) createContext(ctx context.Context) context.Context {
|
||||
return httptrace.WithClientTrace(
|
||||
ctx,
|
||||
&httptrace.ClientTrace{
|
||||
DNSStart: func(_ httptrace.DNSStartInfo) {
|
||||
t.lock.Lock()
|
||||
t.dnsStart = time.Now()
|
||||
t.lock.Unlock()
|
||||
},
|
||||
DNSDone: func(_ httptrace.DNSDoneInfo) {
|
||||
t.lock.Lock()
|
||||
t.dnsDone = time.Now()
|
||||
t.lock.Unlock()
|
||||
},
|
||||
ConnectStart: func(_, _ string) {
|
||||
t.lock.Lock()
|
||||
if t.dnsDone.IsZero() {
|
||||
t.dnsDone = time.Now()
|
||||
}
|
||||
if t.dnsStart.IsZero() {
|
||||
t.dnsStart = t.dnsDone
|
||||
}
|
||||
t.lock.Unlock()
|
||||
},
|
||||
ConnectDone: func(net, addr string, err error) {
|
||||
t.lock.Lock()
|
||||
t.connectDone = time.Now()
|
||||
t.lock.Unlock()
|
||||
},
|
||||
GetConn: func(_ string) {
|
||||
t.lock.Lock()
|
||||
t.getConn = time.Now()
|
||||
t.lock.Unlock()
|
||||
},
|
||||
GotConn: func(ci httptrace.GotConnInfo) {
|
||||
t.lock.Lock()
|
||||
t.gotConn = time.Now()
|
||||
t.gotConnInfo = ci
|
||||
t.lock.Unlock()
|
||||
},
|
||||
GotFirstResponseByte: func() {
|
||||
t.lock.Lock()
|
||||
t.gotFirstResponseByte = time.Now()
|
||||
t.lock.Unlock()
|
||||
},
|
||||
TLSHandshakeStart: func() {
|
||||
t.lock.Lock()
|
||||
t.tlsHandshakeStart = time.Now()
|
||||
t.lock.Unlock()
|
||||
},
|
||||
TLSHandshakeDone: func(_ tls.ConnectionState, _ error) {
|
||||
t.lock.Lock()
|
||||
t.tlsHandshakeDone = time.Now()
|
||||
t.lock.Unlock()
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
//go:build go1.13
|
||||
// +build go1.13
|
||||
|
||||
// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
|
||||
// resty source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package resty
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
func createTransport(localAddr net.Addr) *http.Transport {
|
||||
dialer := &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}
|
||||
if localAddr != nil {
|
||||
dialer.LocalAddr = localAddr
|
||||
}
|
||||
return &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: transportDialContext(dialer),
|
||||
ForceAttemptHTTP2: true,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1,
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
//go:build !go1.13
|
||||
// +build !go1.13
|
||||
|
||||
// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
|
||||
// resty source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package resty
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
|
||||
func createTransport(localAddr net.Addr) *http.Transport {
|
||||
dialer := &net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
KeepAlive: 30 * time.Second,
|
||||
DualStack: true,
|
||||
}
|
||||
if localAddr != nil {
|
||||
dialer.LocalAddr = localAddr
|
||||
}
|
||||
return &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: dialer.DialContext,
|
||||
MaxIdleConns: 100,
|
||||
IdleConnTimeout: 90 * time.Second,
|
||||
TLSHandshakeTimeout: 10 * time.Second,
|
||||
ExpectContinueTimeout: 1 * time.Second,
|
||||
MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1,
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// Copyright 2021 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build js && wasm
|
||||
// +build js,wasm
|
||||
|
||||
package resty
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
)
|
||||
|
||||
func transportDialContext(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error) {
|
||||
return nil
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// Copyright 2021 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !(js && wasm)
|
||||
// +build !js !wasm
|
||||
|
||||
package resty
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
)
|
||||
|
||||
func transportDialContext(dialer *net.Dialer) func(context.Context, string, string) (net.Conn, error) {
|
||||
return dialer.DialContext
|
||||
}
|
||||
+389
@@ -0,0 +1,389 @@
|
||||
// Copyright (c) 2015-2024 Jeevanandam M (jeeva@myjeeva.com), All rights reserved.
|
||||
// resty source code and usage is governed by a MIT style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package resty
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
||||
// Logger interface
|
||||
//_______________________________________________________________________
|
||||
|
||||
// Logger interface is to abstract the logging from Resty. Gives control to
|
||||
// the Resty users, choice of the logger.
|
||||
type Logger interface {
|
||||
Errorf(format string, v ...interface{})
|
||||
Warnf(format string, v ...interface{})
|
||||
Debugf(format string, v ...interface{})
|
||||
}
|
||||
|
||||
func createLogger() *logger {
|
||||
l := &logger{l: log.New(os.Stderr, "", log.Ldate|log.Lmicroseconds)}
|
||||
return l
|
||||
}
|
||||
|
||||
var _ Logger = (*logger)(nil)
|
||||
|
||||
type logger struct {
|
||||
l *log.Logger
|
||||
}
|
||||
|
||||
func (l *logger) Errorf(format string, v ...interface{}) {
|
||||
l.output("ERROR RESTY "+format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Warnf(format string, v ...interface{}) {
|
||||
l.output("WARN RESTY "+format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) Debugf(format string, v ...interface{}) {
|
||||
l.output("DEBUG RESTY "+format, v...)
|
||||
}
|
||||
|
||||
func (l *logger) output(format string, v ...interface{}) {
|
||||
if len(v) == 0 {
|
||||
l.l.Print(format)
|
||||
return
|
||||
}
|
||||
l.l.Printf(format, v...)
|
||||
}
|
||||
|
||||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
||||
// Rate Limiter interface
|
||||
//_______________________________________________________________________
|
||||
|
||||
type RateLimiter interface {
|
||||
Allow() bool
|
||||
}
|
||||
|
||||
var ErrRateLimitExceeded = errors.New("rate limit exceeded")
|
||||
|
||||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
||||
// Package Helper methods
|
||||
//_______________________________________________________________________
|
||||
|
||||
// IsStringEmpty method tells whether given string is empty or not
|
||||
func IsStringEmpty(str string) bool {
|
||||
return len(strings.TrimSpace(str)) == 0
|
||||
}
|
||||
|
||||
// DetectContentType method is used to figure out `Request.Body` content type for request header
|
||||
func DetectContentType(body interface{}) string {
|
||||
contentType := plainTextType
|
||||
kind := kindOf(body)
|
||||
switch kind {
|
||||
case reflect.Struct, reflect.Map:
|
||||
contentType = jsonContentType
|
||||
case reflect.String:
|
||||
contentType = plainTextType
|
||||
default:
|
||||
if b, ok := body.([]byte); ok {
|
||||
contentType = http.DetectContentType(b)
|
||||
} else if kind == reflect.Slice {
|
||||
contentType = jsonContentType
|
||||
}
|
||||
}
|
||||
|
||||
return contentType
|
||||
}
|
||||
|
||||
// IsJSONType method is to check JSON content type or not
|
||||
func IsJSONType(ct string) bool {
|
||||
return jsonCheck.MatchString(ct)
|
||||
}
|
||||
|
||||
// IsXMLType method is to check XML content type or not
|
||||
func IsXMLType(ct string) bool {
|
||||
return xmlCheck.MatchString(ct)
|
||||
}
|
||||
|
||||
// Unmarshalc content into object from JSON or XML
|
||||
func Unmarshalc(c *Client, ct string, b []byte, d interface{}) (err error) {
|
||||
if IsJSONType(ct) {
|
||||
err = c.JSONUnmarshal(b, d)
|
||||
} else if IsXMLType(ct) {
|
||||
err = c.XMLUnmarshal(b, d)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
|
||||
// RequestLog and ResponseLog type
|
||||
//_______________________________________________________________________
|
||||
|
||||
// RequestLog struct is used to collected information from resty request
|
||||
// instance for debug logging. It sent to request log callback before resty
|
||||
// actually logs the information.
|
||||
type RequestLog struct {
|
||||
Header http.Header
|
||||
Body string
|
||||
}
|
||||
|
||||
// ResponseLog struct is used to collected information from resty response
|
||||
// instance for debug logging. It sent to response log callback before resty
|
||||
// actually logs the information.
|
||||
type ResponseLog struct {
|
||||
Header http.Header
|
||||
Body string
|
||||
}
|
||||
|
||||
// way to disable the HTML escape as opt-in
|
||||
func jsonMarshal(c *Client, r *Request, d interface{}) (*bytes.Buffer, error) {
|
||||
if !r.jsonEscapeHTML || !c.jsonEscapeHTML {
|
||||
return noescapeJSONMarshal(d)
|
||||
}
|
||||
|
||||
data, err := c.JSONMarshal(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
buf := acquireBuffer()
|
||||
_, _ = buf.Write(data)
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
func firstNonEmpty(v ...string) string {
|
||||
for _, s := range v {
|
||||
if !IsStringEmpty(s) {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"")
|
||||
|
||||
func escapeQuotes(s string) string {
|
||||
return quoteEscaper.Replace(s)
|
||||
}
|
||||
|
||||
func createMultipartHeader(param, fileName, contentType string) textproto.MIMEHeader {
|
||||
hdr := make(textproto.MIMEHeader)
|
||||
|
||||
var contentDispositionValue string
|
||||
if IsStringEmpty(fileName) {
|
||||
contentDispositionValue = fmt.Sprintf(`form-data; name="%s"`, param)
|
||||
} else {
|
||||
contentDispositionValue = fmt.Sprintf(`form-data; name="%s"; filename="%s"`,
|
||||
param, escapeQuotes(fileName))
|
||||
}
|
||||
hdr.Set("Content-Disposition", contentDispositionValue)
|
||||
|
||||
if !IsStringEmpty(contentType) {
|
||||
hdr.Set(hdrContentTypeKey, contentType)
|
||||
}
|
||||
return hdr
|
||||
}
|
||||
|
||||
func addMultipartFormField(w *multipart.Writer, mf *MultipartField) error {
|
||||
partWriter, err := w.CreatePart(createMultipartHeader(mf.Param, mf.FileName, mf.ContentType))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(partWriter, mf.Reader)
|
||||
return err
|
||||
}
|
||||
|
||||
func writeMultipartFormFile(w *multipart.Writer, fieldName, fileName string, r io.Reader) error {
|
||||
// Auto detect actual multipart content type
|
||||
cbuf := make([]byte, 512)
|
||||
size, err := r.Read(cbuf)
|
||||
if err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
|
||||
partWriter, err := w.CreatePart(createMultipartHeader(fieldName, fileName, http.DetectContentType(cbuf[:size])))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err = partWriter.Write(cbuf[:size]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(partWriter, r)
|
||||
return err
|
||||
}
|
||||
|
||||
func addFile(w *multipart.Writer, fieldName, path string) error {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer closeq(file)
|
||||
return writeMultipartFormFile(w, fieldName, filepath.Base(path), file)
|
||||
}
|
||||
|
||||
func addFileReader(w *multipart.Writer, f *File) error {
|
||||
return writeMultipartFormFile(w, f.ParamName, f.Name, f.Reader)
|
||||
}
|
||||
|
||||
func getPointer(v interface{}) interface{} {
|
||||
vv := valueOf(v)
|
||||
if vv.Kind() == reflect.Ptr {
|
||||
return v
|
||||
}
|
||||
return reflect.New(vv.Type()).Interface()
|
||||
}
|
||||
|
||||
func isPayloadSupported(m string, allowMethodGet bool) bool {
|
||||
return !(m == MethodHead || m == MethodOptions || (m == MethodGet && !allowMethodGet))
|
||||
}
|
||||
|
||||
func typeOf(i interface{}) reflect.Type {
|
||||
return indirect(valueOf(i)).Type()
|
||||
}
|
||||
|
||||
func valueOf(i interface{}) reflect.Value {
|
||||
return reflect.ValueOf(i)
|
||||
}
|
||||
|
||||
func indirect(v reflect.Value) reflect.Value {
|
||||
return reflect.Indirect(v)
|
||||
}
|
||||
|
||||
func kindOf(v interface{}) reflect.Kind {
|
||||
return typeOf(v).Kind()
|
||||
}
|
||||
|
||||
func createDirectory(dir string) (err error) {
|
||||
if _, err = os.Stat(dir); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
if err = os.MkdirAll(dir, 0755); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func canJSONMarshal(contentType string, kind reflect.Kind) bool {
|
||||
return IsJSONType(contentType) && (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice)
|
||||
}
|
||||
|
||||
func functionName(i interface{}) string {
|
||||
return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name()
|
||||
}
|
||||
|
||||
func acquireBuffer() *bytes.Buffer {
|
||||
buf := bufPool.Get().(*bytes.Buffer)
|
||||
if buf.Len() == 0 {
|
||||
buf.Reset()
|
||||
return buf
|
||||
}
|
||||
bufPool.Put(buf)
|
||||
return new(bytes.Buffer)
|
||||
}
|
||||
|
||||
func releaseBuffer(buf *bytes.Buffer) {
|
||||
if buf != nil {
|
||||
buf.Reset()
|
||||
bufPool.Put(buf)
|
||||
}
|
||||
}
|
||||
|
||||
func backToBufPool(buf *bytes.Buffer) {
|
||||
if buf != nil {
|
||||
bufPool.Put(buf)
|
||||
}
|
||||
}
|
||||
|
||||
func closeq(v interface{}) {
|
||||
if c, ok := v.(io.Closer); ok {
|
||||
silently(c.Close())
|
||||
}
|
||||
}
|
||||
|
||||
func silently(_ ...interface{}) {}
|
||||
|
||||
func composeHeaders(c *Client, r *Request, hdrs http.Header) string {
|
||||
str := make([]string, 0, len(hdrs))
|
||||
for _, k := range sortHeaderKeys(hdrs) {
|
||||
str = append(str, "\t"+strings.TrimSpace(fmt.Sprintf("%25s: %s", k, strings.Join(hdrs[k], ", "))))
|
||||
}
|
||||
return strings.Join(str, "\n")
|
||||
}
|
||||
|
||||
func sortHeaderKeys(hdrs http.Header) []string {
|
||||
keys := make([]string, 0, len(hdrs))
|
||||
for key := range hdrs {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
func copyHeaders(hdrs http.Header) http.Header {
|
||||
nh := http.Header{}
|
||||
for k, v := range hdrs {
|
||||
nh[k] = v
|
||||
}
|
||||
return nh
|
||||
}
|
||||
|
||||
func wrapErrors(n error, inner error) error {
|
||||
if inner == nil {
|
||||
return n
|
||||
}
|
||||
if n == nil {
|
||||
return inner
|
||||
}
|
||||
return &restyError{
|
||||
err: n,
|
||||
inner: inner,
|
||||
}
|
||||
}
|
||||
|
||||
type restyError struct {
|
||||
err error
|
||||
inner error
|
||||
}
|
||||
|
||||
func (e *restyError) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func (e *restyError) Unwrap() error {
|
||||
return e.inner
|
||||
}
|
||||
|
||||
type noRetryErr struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *noRetryErr) Error() string {
|
||||
return e.err.Error()
|
||||
}
|
||||
|
||||
func wrapNoRetryErr(err error) error {
|
||||
if err != nil {
|
||||
err = &noRetryErr{err: err}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func unwrapNoRetryErr(err error) error {
|
||||
if e, ok := err.(*noRetryErr); ok {
|
||||
err = e.err
|
||||
}
|
||||
return err
|
||||
}
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
package resty
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/go-resty/resty/v2/shellescape"
|
||||
)
|
||||
|
||||
func buildCurlRequest(req *http.Request, httpCookiejar http.CookieJar) (curl string) {
|
||||
// 1. Generate curl raw headers
|
||||
|
||||
curl = "curl -X " + req.Method + " "
|
||||
// req.Host + req.URL.Path + "?" + req.URL.RawQuery + " " + req.Proto + " "
|
||||
headers := dumpCurlHeaders(req)
|
||||
for _, kv := range *headers {
|
||||
curl += `-H ` + shellescape.Quote(kv[0]+": "+kv[1]) + ` `
|
||||
}
|
||||
|
||||
// 2. Generate curl cookies
|
||||
// TODO validate this block of code, I think its not required since cookie captured via Headers
|
||||
if cookieJar, ok := httpCookiejar.(*cookiejar.Jar); ok {
|
||||
cookies := cookieJar.Cookies(req.URL)
|
||||
if len(cookies) > 0 {
|
||||
curl += `-H ` + shellescape.Quote(dumpCurlCookies(cookies)) + " "
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Generate curl body
|
||||
if req.Body != nil {
|
||||
buf, _ := io.ReadAll(req.Body)
|
||||
req.Body = io.NopCloser(bytes.NewBuffer(buf)) // important!!
|
||||
curl += `-d ` + shellescape.Quote(string(buf)) + " "
|
||||
}
|
||||
|
||||
urlString := shellescape.Quote(req.URL.String())
|
||||
if urlString == "''" {
|
||||
urlString = "'http://unexecuted-request'"
|
||||
}
|
||||
curl += urlString
|
||||
return curl
|
||||
}
|
||||
|
||||
// dumpCurlCookies dumps cookies to curl format
|
||||
func dumpCurlCookies(cookies []*http.Cookie) string {
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString("Cookie: ")
|
||||
for _, cookie := range cookies {
|
||||
sb.WriteString(cookie.Name + "=" + url.QueryEscape(cookie.Value) + "&")
|
||||
}
|
||||
return strings.TrimRight(sb.String(), "&")
|
||||
}
|
||||
|
||||
// dumpCurlHeaders dumps headers to curl format
|
||||
func dumpCurlHeaders(req *http.Request) *[][2]string {
|
||||
headers := [][2]string{}
|
||||
for k, vs := range req.Header {
|
||||
for _, v := range vs {
|
||||
headers = append(headers, [2]string{k, v})
|
||||
}
|
||||
}
|
||||
n := len(headers)
|
||||
for i := 0; i < n; i++ {
|
||||
for j := n - 1; j > i; j-- {
|
||||
jj := j - 1
|
||||
h1, h2 := headers[j], headers[jj]
|
||||
if h1[0] < h2[0] {
|
||||
headers[jj], headers[j] = headers[j], headers[jj]
|
||||
}
|
||||
}
|
||||
}
|
||||
return &headers
|
||||
}
|
||||
Reference in New Issue
Block a user