Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+31
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
File diff suppressed because it is too large Load Diff
+327
View File
@@ -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
View File
@@ -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
View File
@@ -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)
}
}
File diff suppressed because it is too large Load Diff
+195
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
}