@@ -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 | |||
go.sum | |||
# Exclude intellij IDE folders | |||
.idea/* |
@@ -0,0 +1,21 @@ | |||
language: go | |||
sudo: false | |||
go: # use travis ci resource effectively, keep always latest 2 versions and tip :) | |||
- 1.14.x | |||
- 1.13.x | |||
- tip | |||
install: | |||
- go get -v -t ./... | |||
script: | |||
- go test ./... -race -coverprofile=coverage.txt -covermode=atomic | |||
after_success: | |||
- bash <(curl -s https://codecov.io/bash) | |||
matrix: | |||
allow_failures: | |||
- go: tip |
@@ -0,0 +1,36 @@ | |||
package(default_visibility = ["//visibility:private"]) | |||
load("@bazel_gazelle//:def.bzl", "gazelle") | |||
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") | |||
gazelle( | |||
name = "gazelle", | |||
command = "fix", | |||
prefix = "github.com/go-resty/resty/v2", | |||
) | |||
go_library( | |||
name = "go_default_library", | |||
srcs = glob( | |||
["*.go"], | |||
exclude = ["*_test.go"], | |||
), | |||
importpath = "github.com/go-resty/resty/v2", | |||
visibility = ["//visibility:public"], | |||
deps = ["@org_golang_x_net//publicsuffix:go_default_library"], | |||
) | |||
go_test( | |||
name = "go_default_test", | |||
srcs = | |||
glob( | |||
["*_test.go"], | |||
exclude = ["example_test.go"], | |||
), | |||
data = glob([".testdata/*"]), | |||
embed = [":go_default_library"], | |||
importpath = "github.com/go-resty/resty/v2", | |||
deps = [ | |||
"@org_golang_x_net//proxy:go_default_library", | |||
], | |||
) |
@@ -0,0 +1,21 @@ | |||
The MIT License (MIT) | |||
Copyright (c) 2015-2020 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. |
@@ -0,0 +1,850 @@ | |||
<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://travis-ci.org/go-resty/resty"><img src="https://travis-ci.org/go-resty/resty.svg?branch=master" alt="Build Status"></a> <a href="https://codecov.io/gh/go-resty/resty/branch/master"><img src="https://codecov.io/gh/go-resty/resty/branch/master/graph/badge.svg" alt="Code Coverage"></a> <a href="https://goreportcard.com/report/go-resty/resty"><img src="https://goreportcard.com/badge/go-resty/resty" alt="Go Report Card"></a> <a href="https://github.com/go-resty/resty/releases/latest"><img src="https://img.shields.io/badge/version-2.3.0-blue.svg" alt="Release Version"></a> <a href="https://pkg.go.dev/github.com/go-resty/resty/v2"><img src="https://godoc.org/github.com/go-resty/resty?status.svg" alt="GoDoc"></a> <a href="LICENSE"><img src="https://img.shields.io/github/license/go-resty/resty.svg" alt="License"></a> <a href="https://github.com/avelino/awesome-go"><img src="https://awesome.re/mentioned-badge.svg" alt="Mentioned in Awesome Go"></a></p> | |||
</p> | |||
<p align="center"> | |||
<h4 align="center">Resty Communication Channels</h4> | |||
<p align="center"><a href="https://gitter.im/go_resty/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge"><img src="https://badges.gitter.im/go_resty/community.svg" alt="Chat on Gitter - Resty Community"></a> <a href="https://twitter.com/go_resty"><img src="https://img.shields.io/badge/twitter-@go__resty-55acee.svg" alt="Twitter @go_resty"></a></p> | |||
</p> | |||
## News | |||
* v2.3.0 [released](https://github.com/go-resty/resty/releases/tag/v2.3.0) and tagged on May 20, 2020. | |||
* 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://godoc.org/github.com/go-resty/resty#Request) Body can be `string`, `[]byte`, `struct`, `map`, `slice` and `io.Reader` too | |||
* Auto detects `Content-Type` | |||
* Buffer less processing for `io.Reader` | |||
* Request Body can be read multiple times via `Request.RawRequest.GetBody()` | |||
* [Response](https://godoc.org/github.com/go-resty/resty#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://godoc.org/github.com/go-resty/resty#Request.SetResult) and [Response.Result()](https://godoc.org/github.com/go-resty/resty#Response.Result). | |||
- Error scenario [Request.SetError()](https://godoc.org/github.com/go-resty/resty#Request.SetError) and [Response.Error()](https://godoc.org/github.com/go-resty/resty#Response.Error). | |||
- Supports [RFC7807](https://tools.ietf.org/html/rfc7807) - `application/problem+json` & `application/problem+xml` | |||
* 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://godoc.org/github.com/go-resty/resty#Request.SetPathParams) | |||
* Backoff Retry Mechanism with retry condition function [reference](retry_test.go) | |||
* Resty client HTTP & REST [Request](https://godoc.org/github.com/go-resty/resty#Client.OnBeforeRequest) and [Response](https://godoc.org/github.com/go-resty/resty#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://godoc.org/github.com/go-resty/resty#Client.SetRootCertificate) and Client [Certificates](https://godoc.org/github.com/go-resty/resty#Client.SetCertificates) | |||
* Download/Save HTTP response directly into File, like `curl -o` flag. See [SetOutputDirectory](https://godoc.org/github.com/go-resty/resty#Client.SetOutputDirectory) & [SetOutput](https://godoc.org/github.com/go-resty/resty#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://godoc.org/github.com/go-resty/resty#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 middlewares | |||
* Create Multiple clients if you want to `resty.New()` | |||
* Supports `http.RoundTripper` implementation, see [SetTransport](https://godoc.org/github.com/go-resty/resty#Client.SetTransport) | |||
* goroutine concurrent safe | |||
* Resty Client trace, see [Client.EnableTrace](https://godoc.org/github.com/go-resty/resty#Client.EnableTrace) and [Request.EnableTrace](https://godoc.org/github.com/go-resty/resty#Request.EnableTrace) | |||
* 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` | |||
* [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 | |||
* 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 | |||
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. | |||
* [aah framework](https://aahframework.org) - A secure, flexible, rapid Go web framework. | |||
* [THUMBAI](https://thumbai.app) - Go Mod Repository, Go Vanity Service and Simple Proxy Server. | |||
* [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.3.0 | |||
``` | |||
## 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) | |||
/* Output | |||
Response Info: | |||
Error : <nil> | |||
Status Code: 200 | |||
Status : 200 OK | |||
Proto : HTTP/2.0 | |||
Time : 475.611189ms | |||
Received At: 2020-05-19 00:11:06.828188 -0700 PDT m=+0.476510773 | |||
Body : | |||
{ | |||
"args": {}, | |||
"headers": { | |||
"Accept-Encoding": "gzip", | |||
"Host": "httpbin.org", | |||
"User-Agent": "go-resty/2.3.0 (https://github.com/go-resty/resty)" | |||
}, | |||
"origin": "0.0.0.0", | |||
"url": "https://httpbin.org/get" | |||
} | |||
Request Trace Info: | |||
DNSLookup : 4.870246ms | |||
ConnTime : 393.95373ms | |||
TCPConnTime : 78.360432ms | |||
TLSHandshake : 310.032859ms | |||
ServerTime : 81.648284ms | |||
ResponseTime : 124.266µs | |||
TotalTime : 475.611189ms | |||
IsConnReused : false | |||
IsConnWasIdle: false | |||
ConnIdleTime : 0s | |||
*/ | |||
``` | |||
#### 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") | |||
``` | |||
#### 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, _ := ioutil.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") | |||
``` | |||
### Multipart File(s) upload | |||
#### Using io.Reader | |||
```go | |||
profileImgBytes, _ := ioutil.ReadFile("/Users/jeeva/test-img.png") | |||
notesBytes, _ := ioutil.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 absoule 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 | |||
}) | |||
``` | |||
#### 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 throught 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 - Client as well as at Request Level | |||
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 *Client, resp *Response) (time.Duration, error) { | |||
return 0, errors.New("quota exceeded") | |||
}) | |||
``` | |||
Above setup will result in resty retrying requests returned non nil error up to | |||
3 times with delay increased after each attempt. | |||
You can optionally provide client with custom retry conditions: | |||
```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 | |||
}, | |||
) | |||
``` | |||
Above example will make resty retry requests ended with `429 Too Many Requests` | |||
status code. | |||
Multiple retry conditions can be added. | |||
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.SetHostURL("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_secert": "api-secert", | |||
}) | |||
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").SetHostURL(unixSocket) | |||
// No need to write the host's URL on the request, just the path. | |||
client.R().Get("/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 :go_default_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/teams/core/members) 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. |
@@ -0,0 +1,27 @@ | |||
workspace(name = "resty") | |||
git_repository( | |||
name = "io_bazel_rules_go", | |||
remote = "https://github.com/bazelbuild/rules_go.git", | |||
tag = "0.13.0", | |||
) | |||
git_repository( | |||
name = "bazel_gazelle", | |||
remote = "https://github.com/bazelbuild/bazel-gazelle.git", | |||
tag = "0.13.0", | |||
) | |||
load( | |||
"@io_bazel_rules_go//go:def.bzl", | |||
"go_rules_dependencies", | |||
"go_register_toolchains", | |||
) | |||
go_rules_dependencies() | |||
go_register_toolchains() | |||
load("@bazel_gazelle//:deps.bzl", "gazelle_dependencies") | |||
gazelle_dependencies() |
@@ -0,0 +1,978 @@ | |||
// Copyright (c) 2015-2020 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" | |||
"compress/gzip" | |||
"crypto/tls" | |||
"crypto/x509" | |||
"encoding/json" | |||
"errors" | |||
"fmt" | |||
"io" | |||
"io/ioutil" | |||
"math" | |||
"net/http" | |||
"net/url" | |||
"reflect" | |||
"regexp" | |||
"strings" | |||
"sync" | |||
"time" | |||
) | |||
const ( | |||
// MethodGet HTTP method | |||
MethodGet = "GET" | |||
// MethodPost HTTP method | |||
MethodPost = "POST" | |||
// MethodPut HTTP method | |||
MethodPut = "PUT" | |||
// MethodDelete HTTP method | |||
MethodDelete = "DELETE" | |||
// MethodPatch HTTP method | |||
MethodPatch = "PATCH" | |||
// MethodHead HTTP method | |||
MethodHead = "HEAD" | |||
// MethodOptions HTTP method | |||
MethodOptions = "OPTIONS" | |||
) | |||
var ( | |||
hdrUserAgentKey = http.CanonicalHeaderKey("User-Agent") | |||
hdrAcceptKey = http.CanonicalHeaderKey("Accept") | |||
hdrContentTypeKey = http.CanonicalHeaderKey("Content-Type") | |||
hdrContentLengthKey = http.CanonicalHeaderKey("Content-Length") | |||
hdrContentEncodingKey = http.CanonicalHeaderKey("Content-Encoding") | |||
hdrAuthorizationKey = http.CanonicalHeaderKey("Authorization") | |||
plainTextType = "text/plain; charset=utf-8" | |||
jsonContentType = "application/json" | |||
formContentType = "application/x-www-form-urlencoded" | |||
jsonCheck = regexp.MustCompile(`(?i:(application|text)/(json|.*\+json|json\-.*)(;|$))`) | |||
xmlCheck = regexp.MustCompile(`(?i:(application|text)/(xml|.*\+xml)(;|$))`) | |||
hdrUserAgentValue = "go-resty/" + Version + " (https://github.com/go-resty/resty)" | |||
bufPool = &sync.Pool{New: func() interface{} { return &bytes.Buffer{} }} | |||
) | |||
type ( | |||
// RequestMiddleware type is for request middleware, called before a request is sent | |||
RequestMiddleware func(*Client, *Request) error | |||
// ResponseMiddleware type is for response middleware, called after a response has been received | |||
ResponseMiddleware func(*Client, *Response) error | |||
// PreRequestHook type is for the request hook, called right before the request is sent | |||
PreRequestHook func(*Client, *http.Request) error | |||
// RequestLogCallback type is for request logs, called before the request is logged | |||
RequestLogCallback func(*RequestLog) error | |||
// ResponseLogCallback type is for response logs, called before the response is logged | |||
ResponseLogCallback func(*ResponseLog) error | |||
) | |||
// Client struct is used to create Resty client with client level settings, | |||
// these settings are applicable to all the request raised from the client. | |||
// | |||
// Resty also provides an options to override most of the client settings | |||
// at request level. | |||
type Client struct { | |||
HostURL string | |||
QueryParam url.Values | |||
FormData url.Values | |||
Header http.Header | |||
UserInfo *User | |||
Token string | |||
AuthScheme string | |||
Cookies []*http.Cookie | |||
Error reflect.Type | |||
Debug bool | |||
DisableWarn bool | |||
AllowGetMethodPayload bool | |||
RetryCount int | |||
RetryWaitTime time.Duration | |||
RetryMaxWaitTime time.Duration | |||
RetryConditions []RetryConditionFunc | |||
RetryAfter RetryAfterFunc | |||
JSONMarshal func(v interface{}) ([]byte, error) | |||
JSONUnmarshal func(data []byte, v interface{}) error | |||
jsonEscapeHTML bool | |||
setContentLength bool | |||
closeConnection bool | |||
notParseResponse bool | |||
trace bool | |||
debugBodySizeLimit int64 | |||
outputDirectory string | |||
scheme string | |||
pathParams map[string]string | |||
log Logger | |||
httpClient *http.Client | |||
proxyURL *url.URL | |||
beforeRequest []RequestMiddleware | |||
udBeforeRequest []RequestMiddleware | |||
preReqHook PreRequestHook | |||
afterResponse []ResponseMiddleware | |||
requestLog RequestLogCallback | |||
responseLog ResponseLogCallback | |||
} | |||
// User type is to hold an username and password information | |||
type User struct { | |||
Username, Password string | |||
} | |||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
// Client methods | |||
//___________________________________ | |||
// SetHostURL method is to set Host URL in the client instance. It will be used with request | |||
// raised from this client with relative URL | |||
// // Setting HTTP address | |||
// client.SetHostURL("http://myjeeva.com") | |||
// | |||
// // Setting HTTPS address | |||
// client.SetHostURL("https://myjeeva.com") | |||
func (c *Client) SetHostURL(url string) *Client { | |||
c.HostURL = strings.TrimRight(url, "/") | |||
return c | |||
} | |||
// SetHeader method sets a single header field and its value in the client instance. | |||
// These headers will be applied to all requests raised from this client instance. | |||
// Also it can be overridden at request level header options. | |||
// | |||
// See `Request.SetHeader` or `Request.SetHeaders`. | |||
// | |||
// For Example: To set `Content-Type` and `Accept` as `application/json` | |||
// | |||
// client. | |||
// SetHeader("Content-Type", "application/json"). | |||
// SetHeader("Accept", "application/json") | |||
func (c *Client) SetHeader(header, value string) *Client { | |||
c.Header.Set(header, value) | |||
return c | |||
} | |||
// SetHeaders method sets multiple headers field and its values at one go in the client instance. | |||
// These headers will be applied to all requests raised from this client instance. Also it can be | |||
// overridden at request level headers options. | |||
// | |||
// See `Request.SetHeaders` or `Request.SetHeader`. | |||
// | |||
// For Example: To set `Content-Type` and `Accept` as `application/json` | |||
// | |||
// client.SetHeaders(map[string]string{ | |||
// "Content-Type": "application/json", | |||
// "Accept": "application/json", | |||
// }) | |||
func (c *Client) SetHeaders(headers map[string]string) *Client { | |||
for h, v := range headers { | |||
c.Header.Set(h, v) | |||
} | |||
return c | |||
} | |||
// SetCookieJar method sets custom http.CookieJar in the resty client. Its way to override default. | |||
// | |||
// For Example: sometimes we don't want to save cookies in api contacting, we can remove the default | |||
// CookieJar in resty client. | |||
// | |||
// client.SetCookieJar(nil) | |||
func (c *Client) SetCookieJar(jar http.CookieJar) *Client { | |||
c.httpClient.Jar = jar | |||
return c | |||
} | |||
// SetCookie method appends a single cookie in the client instance. | |||
// These cookies will be added to all the request raised from this client instance. | |||
// client.SetCookie(&http.Cookie{ | |||
// Name:"go-resty", | |||
// Value:"This is cookie value", | |||
// }) | |||
func (c *Client) SetCookie(hc *http.Cookie) *Client { | |||
c.Cookies = append(c.Cookies, hc) | |||
return c | |||
} | |||
// SetCookies method sets an array of cookies in the client instance. | |||
// These cookies will be added to all the request raised from this client instance. | |||
// cookies := []*http.Cookie{ | |||
// &http.Cookie{ | |||
// Name:"go-resty-1", | |||
// Value:"This is cookie 1 value", | |||
// }, | |||
// &http.Cookie{ | |||
// Name:"go-resty-2", | |||
// Value:"This is cookie 2 value", | |||
// }, | |||
// } | |||
// | |||
// // Setting a cookies into resty | |||
// client.SetCookies(cookies) | |||
func (c *Client) SetCookies(cs []*http.Cookie) *Client { | |||
c.Cookies = append(c.Cookies, cs...) | |||
return c | |||
} | |||
// SetQueryParam method sets single parameter and its value in the client instance. | |||
// It will be formed as query string for the request. | |||
// | |||
// For Example: `search=kitchen%20papers&size=large` | |||
// in the URL after `?` mark. These query params will be added to all the request raised from | |||
// this client instance. Also it can be overridden at request level Query Param options. | |||
// | |||
// See `Request.SetQueryParam` or `Request.SetQueryParams`. | |||
// client. | |||
// SetQueryParam("search", "kitchen papers"). | |||
// SetQueryParam("size", "large") | |||
func (c *Client) SetQueryParam(param, value string) *Client { | |||
c.QueryParam.Set(param, value) | |||
return c | |||
} | |||
// SetQueryParams method sets multiple parameters and their values at one go in the client instance. | |||
// It will be formed as query string for the request. | |||
// | |||
// For Example: `search=kitchen%20papers&size=large` | |||
// in the URL after `?` mark. These query params will be added to all the request raised from this | |||
// client instance. Also it can be overridden at request level Query Param options. | |||
// | |||
// See `Request.SetQueryParams` or `Request.SetQueryParam`. | |||
// client.SetQueryParams(map[string]string{ | |||
// "search": "kitchen papers", | |||
// "size": "large", | |||
// }) | |||
func (c *Client) SetQueryParams(params map[string]string) *Client { | |||
for p, v := range params { | |||
c.SetQueryParam(p, v) | |||
} | |||
return c | |||
} | |||
// SetFormData method sets Form parameters and their values in the client instance. | |||
// It's applicable only HTTP method `POST` and `PUT` and requets content type would be set as | |||
// `application/x-www-form-urlencoded`. These form data will be added to all the request raised from | |||
// this client instance. Also it can be overridden at request level form data. | |||
// | |||
// See `Request.SetFormData`. | |||
// client.SetFormData(map[string]string{ | |||
// "access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F", | |||
// "user_id": "3455454545", | |||
// }) | |||
func (c *Client) SetFormData(data map[string]string) *Client { | |||
for k, v := range data { | |||
c.FormData.Set(k, v) | |||
} | |||
return c | |||
} | |||
// SetBasicAuth method sets the basic authentication header in the HTTP request. For Example: | |||
// Authorization: Basic <base64-encoded-value> | |||
// | |||
// For Example: To set the header for username "go-resty" and password "welcome" | |||
// client.SetBasicAuth("go-resty", "welcome") | |||
// | |||
// This basic auth information gets added to all the request rasied from this client instance. | |||
// Also it can be overridden or set one at the request level is supported. | |||
// | |||
// See `Request.SetBasicAuth`. | |||
func (c *Client) SetBasicAuth(username, password string) *Client { | |||
c.UserInfo = &User{Username: username, Password: password} | |||
return c | |||
} | |||
// SetAuthToken method sets the auth token of the `Authorization` header for all HTTP requests. | |||
// The default auth scheme is `Bearer`, it can be customized with the method `SetAuthScheme`. For Example: | |||
// Authorization: <auth-scheme> <auth-token-value> | |||
// | |||
// For Example: To set auth token BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F | |||
// | |||
// client.SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F") | |||
// | |||
// This auth token gets added to all the requests rasied from this client instance. | |||
// Also it can be overridden or set one at the request level is supported. | |||
// | |||
// See `Request.SetAuthToken`. | |||
func (c *Client) SetAuthToken(token string) *Client { | |||
c.Token = token | |||
return c | |||
} | |||
// SetAuthScheme method sets the auth scheme type in the HTTP request. For Example: | |||
// Authorization: <auth-scheme-value> <auth-token-value> | |||
// | |||
// For Example: To set the scheme to use OAuth | |||
// | |||
// client.SetAuthScheme("OAuth") | |||
// | |||
// This auth scheme gets added to all the requests rasied from this client instance. | |||
// Also it can be overridden or set one at the request level is supported. | |||
// | |||
// Information about auth schemes can be found in RFC7235 which is linked to below | |||
// along with the page containing the currently defined official authentication schemes: | |||
// https://tools.ietf.org/html/rfc7235 | |||
// https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes | |||
// | |||
// See `Request.SetAuthToken`. | |||
func (c *Client) SetAuthScheme(scheme string) *Client { | |||
c.AuthScheme = scheme | |||
return c | |||
} | |||
// R method creates a new request instance, its used for Get, Post, Put, Delete, Patch, Head, Options, etc. | |||
func (c *Client) R() *Request { | |||
r := &Request{ | |||
QueryParam: url.Values{}, | |||
FormData: url.Values{}, | |||
Header: http.Header{}, | |||
Cookies: make([]*http.Cookie, 0), | |||
client: c, | |||
multipartFiles: []*File{}, | |||
multipartFields: []*MultipartField{}, | |||
pathParams: map[string]string{}, | |||
jsonEscapeHTML: true, | |||
} | |||
return r | |||
} | |||
// NewRequest is an alias for method `R()`. Creates a new request instance, its used for | |||
// Get, Post, Put, Delete, Patch, Head, Options, etc. | |||
func (c *Client) NewRequest() *Request { | |||
return c.R() | |||
} | |||
// OnBeforeRequest method appends request middleware into the before request chain. | |||
// Its gets applied after default Resty request middlewares and before request | |||
// been sent from Resty to host server. | |||
// client.OnBeforeRequest(func(c *resty.Client, r *resty.Request) error { | |||
// // Now you have access to Client and Request instance | |||
// // manipulate it as per your need | |||
// | |||
// return nil // if its success otherwise return error | |||
// }) | |||
func (c *Client) OnBeforeRequest(m RequestMiddleware) *Client { | |||
c.udBeforeRequest = append(c.udBeforeRequest, m) | |||
return c | |||
} | |||
// OnAfterResponse method appends response middleware into the after response chain. | |||
// Once we receive response from host server, default Resty response middleware | |||
// gets applied and then user assigened response middlewares applied. | |||
// client.OnAfterResponse(func(c *resty.Client, r *resty.Response) error { | |||
// // Now you have access to Client and Response instance | |||
// // manipulate it as per your need | |||
// | |||
// return nil // if its success otherwise return error | |||
// }) | |||
func (c *Client) OnAfterResponse(m ResponseMiddleware) *Client { | |||
c.afterResponse = append(c.afterResponse, m) | |||
return c | |||
} | |||
// SetPreRequestHook method sets the given pre-request function into resty client. | |||
// It is called right before the request is fired. | |||
// | |||
// Note: Only one pre-request hook can be registered. Use `client.OnBeforeRequest` for mutilple. | |||
func (c *Client) SetPreRequestHook(h PreRequestHook) *Client { | |||
if c.preReqHook != nil { | |||
c.log.Warnf("Overwriting an existing pre-request hook: %s", functionName(h)) | |||
} | |||
c.preReqHook = h | |||
return c | |||
} | |||
// SetDebug method enables the debug mode on Resty client. Client logs details of every request and response. | |||
// For `Request` it logs information such as HTTP verb, Relative URL path, Host, Headers, Body if it has one. | |||
// For `Response` it logs information such as Status, Response Time, Headers, Body if it has one. | |||
// client.SetDebug(true) | |||
func (c *Client) SetDebug(d bool) *Client { | |||
c.Debug = d | |||
return c | |||
} | |||
// SetDebugBodyLimit sets the maximum size for which the response and request body will be logged in debug mode. | |||
// client.SetDebugBodyLimit(1000000) | |||
func (c *Client) SetDebugBodyLimit(sl int64) *Client { | |||
c.debugBodySizeLimit = sl | |||
return c | |||
} | |||
// OnRequestLog method used to set request log callback into Resty. Registered callback gets | |||
// called before the resty actually logs the information. | |||
func (c *Client) OnRequestLog(rl RequestLogCallback) *Client { | |||
if c.requestLog != nil { | |||
c.log.Warnf("Overwriting an existing on-request-log callback from=%s to=%s", | |||
functionName(c.requestLog), functionName(rl)) | |||
} | |||
c.requestLog = rl | |||
return c | |||
} | |||
// OnResponseLog method used to set response log callback into Resty. Registered callback gets | |||
// called before the resty actually logs the information. | |||
func (c *Client) OnResponseLog(rl ResponseLogCallback) *Client { | |||
if c.responseLog != nil { | |||
c.log.Warnf("Overwriting an existing on-response-log callback from=%s to=%s", | |||
functionName(c.responseLog), functionName(rl)) | |||
} | |||
c.responseLog = rl | |||
return c | |||
} | |||
// SetDisableWarn method disables the warning message on Resty client. | |||
// | |||
// For Example: Resty warns the user when BasicAuth used on non-TLS mode. | |||
// client.SetDisableWarn(true) | |||
func (c *Client) SetDisableWarn(d bool) *Client { | |||
c.DisableWarn = d | |||
return c | |||
} | |||
// SetAllowGetMethodPayload method allows the GET method with payload on Resty client. | |||
// | |||
// For Example: Resty allows the user sends request with a payload on HTTP GET method. | |||
// client.SetAllowGetMethodPayload(true) | |||
func (c *Client) SetAllowGetMethodPayload(a bool) *Client { | |||
c.AllowGetMethodPayload = a | |||
return c | |||
} | |||
// SetLogger method sets given writer for logging Resty request and response details. | |||
// | |||
// Compliant to interface `resty.Logger`. | |||
func (c *Client) SetLogger(l Logger) *Client { | |||
c.log = l | |||
return c | |||
} | |||
// SetContentLength method enables the HTTP header `Content-Length` value for every request. | |||
// By default Resty won't set `Content-Length`. | |||
// client.SetContentLength(true) | |||
// | |||
// Also you have an option to enable for particular request. See `Request.SetContentLength` | |||
func (c *Client) SetContentLength(l bool) *Client { | |||
c.setContentLength = l | |||
return c | |||
} | |||
// SetTimeout method sets timeout for request raised from client. | |||
// client.SetTimeout(time.Duration(1 * time.Minute)) | |||
func (c *Client) SetTimeout(timeout time.Duration) *Client { | |||
c.httpClient.Timeout = timeout | |||
return c | |||
} | |||
// SetError method is to register the global or client common `Error` object into Resty. | |||
// It is used for automatic unmarshalling if response status code is greater than 399 and | |||
// content type either JSON or XML. Can be pointer or non-pointer. | |||
// client.SetError(&Error{}) | |||
// // OR | |||
// client.SetError(Error{}) | |||
func (c *Client) SetError(err interface{}) *Client { | |||
c.Error = typeOf(err) | |||
return c | |||
} | |||
// SetRedirectPolicy method sets the client redirect poilicy. Resty provides ready to use | |||
// redirect policies. Wanna create one for yourself refer to `redirect.go`. | |||
// | |||
// client.SetRedirectPolicy(FlexibleRedirectPolicy(20)) | |||
// | |||
// // Need multiple redirect policies together | |||
// client.SetRedirectPolicy(FlexibleRedirectPolicy(20), DomainCheckRedirectPolicy("host1.com", "host2.net")) | |||
func (c *Client) SetRedirectPolicy(policies ...interface{}) *Client { | |||
for _, p := range policies { | |||
if _, ok := p.(RedirectPolicy); !ok { | |||
c.log.Errorf("%v does not implement resty.RedirectPolicy (missing Apply method)", | |||
functionName(p)) | |||
} | |||
} | |||
c.httpClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { | |||
for _, p := range policies { | |||
if err := p.(RedirectPolicy).Apply(req, via); err != nil { | |||
return err | |||
} | |||
} | |||
return nil // looks good, go ahead | |||
} | |||
return c | |||
} | |||
// SetRetryCount method enables retry on Resty client and allows you | |||
// to set no. of retry count. Resty uses a Backoff mechanism. | |||
func (c *Client) SetRetryCount(count int) *Client { | |||
c.RetryCount = count | |||
return c | |||
} | |||
// SetRetryWaitTime method sets default wait time to sleep before retrying | |||
// request. | |||
// | |||
// Default is 100 milliseconds. | |||
func (c *Client) SetRetryWaitTime(waitTime time.Duration) *Client { | |||
c.RetryWaitTime = waitTime | |||
return c | |||
} | |||
// SetRetryMaxWaitTime method sets max wait time to sleep before retrying | |||
// request. | |||
// | |||
// Default is 2 seconds. | |||
func (c *Client) SetRetryMaxWaitTime(maxWaitTime time.Duration) *Client { | |||
c.RetryMaxWaitTime = maxWaitTime | |||
return c | |||
} | |||
// SetRetryAfter sets callback to calculate wait time between retries. | |||
// Default (nil) implies exponential backoff with jitter | |||
func (c *Client) SetRetryAfter(callback RetryAfterFunc) *Client { | |||
c.RetryAfter = callback | |||
return c | |||
} | |||
// AddRetryCondition method adds a retry condition function to array of functions | |||
// that are checked to determine if the request is retried. The request will | |||
// retry if any of the functions return true and error is nil. | |||
func (c *Client) AddRetryCondition(condition RetryConditionFunc) *Client { | |||
c.RetryConditions = append(c.RetryConditions, condition) | |||
return c | |||
} | |||
// SetTLSClientConfig method sets TLSClientConfig for underling client Transport. | |||
// | |||
// For Example: | |||
// // 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 }) | |||
// | |||
// Note: This method overwrites existing `TLSClientConfig`. | |||
func (c *Client) SetTLSClientConfig(config *tls.Config) *Client { | |||
transport, err := c.transport() | |||
if err != nil { | |||
c.log.Errorf("%v", err) | |||
return c | |||
} | |||
transport.TLSClientConfig = config | |||
return c | |||
} | |||
// SetProxy method sets the Proxy URL and Port for Resty client. | |||
// client.SetProxy("http://proxyserver:8888") | |||
// | |||
// OR Without this `SetProxy` method, you could also set Proxy via environment variable. | |||
// | |||
// Refer to godoc `http.ProxyFromEnvironment`. | |||
func (c *Client) SetProxy(proxyURL string) *Client { | |||
transport, err := c.transport() | |||
if err != nil { | |||
c.log.Errorf("%v", err) | |||
return c | |||
} | |||
pURL, err := url.Parse(proxyURL) | |||
if err != nil { | |||
c.log.Errorf("%v", err) | |||
return c | |||
} | |||
c.proxyURL = pURL | |||
transport.Proxy = http.ProxyURL(c.proxyURL) | |||
return c | |||
} | |||
// RemoveProxy method removes the proxy configuration from Resty client | |||
// client.RemoveProxy() | |||
func (c *Client) RemoveProxy() *Client { | |||
transport, err := c.transport() | |||
if err != nil { | |||
c.log.Errorf("%v", err) | |||
return c | |||
} | |||
c.proxyURL = nil | |||
transport.Proxy = nil | |||
return c | |||
} | |||
// SetCertificates method helps to set client certificates into Resty conveniently. | |||
func (c *Client) SetCertificates(certs ...tls.Certificate) *Client { | |||
config, err := c.tlsConfig() | |||
if err != nil { | |||
c.log.Errorf("%v", err) | |||
return c | |||
} | |||
config.Certificates = append(config.Certificates, certs...) | |||
return c | |||
} | |||
// SetRootCertificate method helps to add one or more root certificates into Resty client | |||
// client.SetRootCertificate("/path/to/root/pemFile.pem") | |||
func (c *Client) SetRootCertificate(pemFilePath string) *Client { | |||
rootPemData, err := ioutil.ReadFile(pemFilePath) | |||
if err != nil { | |||
c.log.Errorf("%v", err) | |||
return c | |||
} | |||
config, err := c.tlsConfig() | |||
if err != nil { | |||
c.log.Errorf("%v", err) | |||
return c | |||
} | |||
if config.RootCAs == nil { | |||
config.RootCAs = x509.NewCertPool() | |||
} | |||
config.RootCAs.AppendCertsFromPEM(rootPemData) | |||
return c | |||
} | |||
// SetRootCertificateFromString method helps to add one or more root certificates into Resty client | |||
// client.SetRootCertificateFromString("pem file content") | |||
func (c *Client) SetRootCertificateFromString(pemContent string) *Client { | |||
config, err := c.tlsConfig() | |||
if err != nil { | |||
c.log.Errorf("%v", err) | |||
return c | |||
} | |||
if config.RootCAs == nil { | |||
config.RootCAs = x509.NewCertPool() | |||
} | |||
config.RootCAs.AppendCertsFromPEM([]byte(pemContent)) | |||
return c | |||
} | |||
// SetOutputDirectory method sets output directory for saving HTTP response into file. | |||
// If the output directory not exists then resty creates one. This setting is optional one, | |||
// if you're planning using absolute path in `Request.SetOutput` and can used together. | |||
// client.SetOutputDirectory("/save/http/response/here") | |||
func (c *Client) SetOutputDirectory(dirPath string) *Client { | |||
c.outputDirectory = dirPath | |||
return c | |||
} | |||
// SetTransport method sets custom `*http.Transport` or any `http.RoundTripper` | |||
// compatible interface implementation in the resty client. | |||
// | |||
// Note: | |||
// | |||
// - If transport is not type of `*http.Transport` then you may not be able to | |||
// take advantage of some of the Resty client settings. | |||
// | |||
// - It overwrites the Resty client transport instance and it's configurations. | |||
// | |||
// transport := &http.Transport{ | |||
// // somthing like Proxying to httptest.Server, etc... | |||
// Proxy: func(req *http.Request) (*url.URL, error) { | |||
// return url.Parse(server.URL) | |||
// }, | |||
// } | |||
// | |||
// client.SetTransport(transport) | |||
func (c *Client) SetTransport(transport http.RoundTripper) *Client { | |||
if transport != nil { | |||
c.httpClient.Transport = transport | |||
} | |||
return c | |||
} | |||
// SetScheme method sets custom scheme in the Resty client. It's way to override default. | |||
// client.SetScheme("http") | |||
func (c *Client) SetScheme(scheme string) *Client { | |||
if !IsStringEmpty(scheme) { | |||
c.scheme = scheme | |||
} | |||
return c | |||
} | |||
// SetCloseConnection method sets variable `Close` in http request struct with the given | |||
// value. More info: https://golang.org/src/net/http/request.go | |||
func (c *Client) SetCloseConnection(close bool) *Client { | |||
c.closeConnection = close | |||
return c | |||
} | |||
// SetDoNotParseResponse method instructs `Resty` not to parse the response body automatically. | |||
// Resty exposes the raw response body as `io.ReadCloser`. Also do not forget to close the body, | |||
// otherwise you might get into connection leaks, no connection reuse. | |||
// | |||
// Note: Response middlewares are not applicable, if you use this option. Basically you have | |||
// taken over the control of response parsing from `Resty`. | |||
func (c *Client) SetDoNotParseResponse(parse bool) *Client { | |||
c.notParseResponse = parse | |||
return c | |||
} | |||
// SetPathParams method sets multiple URL path key-value pairs at one go in the | |||
// Resty client instance. | |||
// client.SetPathParams(map[string]string{ | |||
// "userId": "sample@sample.com", | |||
// "subAccountId": "100002", | |||
// }) | |||
// | |||
// Result: | |||
// URL - /v1/users/{userId}/{subAccountId}/details | |||
// Composed URL - /v1/users/sample@sample.com/100002/details | |||
// It replace the value of the key while composing request URL. Also it can be | |||
// overridden at request level Path Params options, see `Request.SetPathParams`. | |||
func (c *Client) SetPathParams(params map[string]string) *Client { | |||
for p, v := range params { | |||
c.pathParams[p] = v | |||
} | |||
return c | |||
} | |||
// SetJSONEscapeHTML method is to enable/disable the HTML escape on JSON marshal. | |||
// | |||
// Note: This option only applicable to standard JSON Marshaller. | |||
func (c *Client) SetJSONEscapeHTML(b bool) *Client { | |||
c.jsonEscapeHTML = b | |||
return c | |||
} | |||
// EnableTrace method enables the Resty client trace for the requests fired from | |||
// the client using `httptrace.ClientTrace` and provides insights. | |||
// | |||
// client := resty.New().EnableTrace() | |||
// | |||
// resp, err := client.R().Get("https://httpbin.org/get") | |||
// fmt.Println("Error:", err) | |||
// fmt.Println("Trace Info:", resp.Request.TraceInfo()) | |||
// | |||
// Also `Request.EnableTrace` available too to get trace info for single request. | |||
// | |||
// Since v2.0.0 | |||
func (c *Client) EnableTrace() *Client { | |||
c.trace = true | |||
return c | |||
} | |||
// DisableTrace method disables the Resty client trace. Refer to `Client.EnableTrace`. | |||
// | |||
// Since v2.0.0 | |||
func (c *Client) DisableTrace() *Client { | |||
c.trace = false | |||
return c | |||
} | |||
// IsProxySet method returns the true is proxy is set from resty client otherwise | |||
// false. By default proxy is set from environment, refer to `http.ProxyFromEnvironment`. | |||
func (c *Client) IsProxySet() bool { | |||
return c.proxyURL != nil | |||
} | |||
// GetClient method returns the current `http.Client` used by the resty client. | |||
func (c *Client) GetClient() *http.Client { | |||
return c.httpClient | |||
} | |||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
// Client Unexported methods | |||
//_______________________________________________________________________ | |||
// Executes method executes the given `Request` object and returns response | |||
// error. | |||
func (c *Client) execute(req *Request) (*Response, error) { | |||
defer releaseBuffer(req.bodyBuf) | |||
// Apply Request middleware | |||
var err error | |||
// user defined on before request methods | |||
// to modify the *resty.Request object | |||
for _, f := range c.udBeforeRequest { | |||
if err = f(c, req); err != nil { | |||
return nil, wrapNoRetryErr(err) | |||
} | |||
} | |||
// resty middlewares | |||
for _, f := range c.beforeRequest { | |||
if err = f(c, req); err != nil { | |||
return nil, wrapNoRetryErr(err) | |||
} | |||
} | |||
if hostHeader := req.Header.Get("Host"); hostHeader != "" { | |||
req.RawRequest.Host = hostHeader | |||
} | |||
// call pre-request if defined | |||
if c.preReqHook != nil { | |||
if err = c.preReqHook(c, req.RawRequest); err != nil { | |||
return nil, wrapNoRetryErr(err) | |||
} | |||
} | |||
if err = requestLogger(c, req); err != nil { | |||
return nil, wrapNoRetryErr(err) | |||
} | |||
req.Time = time.Now() | |||
resp, err := c.httpClient.Do(req.RawRequest) | |||
response := &Response{ | |||
Request: req, | |||
RawResponse: resp, | |||
} | |||
if err != nil || req.notParseResponse || c.notParseResponse { | |||
response.setReceivedAt() | |||
return response, err | |||
} | |||
if !req.isSaveResponse { | |||
defer closeq(resp.Body) | |||
body := resp.Body | |||
// GitHub #142 & #187 | |||
if strings.EqualFold(resp.Header.Get(hdrContentEncodingKey), "gzip") && resp.ContentLength != 0 { | |||
if _, ok := body.(*gzip.Reader); !ok { | |||
body, err = gzip.NewReader(body) | |||
if err != nil { | |||
response.setReceivedAt() | |||
return response, err | |||
} | |||
defer closeq(body) | |||
} | |||
} | |||
if response.body, err = ioutil.ReadAll(body); err != nil { | |||
response.setReceivedAt() | |||
return response, err | |||
} | |||
response.setReceivedAt() // after we read the body | |||
response.size = int64(len(response.body)) | |||
} | |||
// Apply Response middleware | |||
for _, f := range c.afterResponse { | |||
if err = f(c, response); err != nil { | |||
break | |||
} | |||
} | |||
return response, wrapNoRetryErr(err) | |||
} | |||
// getting TLS client config if not exists then create one | |||
func (c *Client) tlsConfig() (*tls.Config, error) { | |||
transport, err := c.transport() | |||
if err != nil { | |||
return nil, err | |||
} | |||
if transport.TLSClientConfig == nil { | |||
transport.TLSClientConfig = &tls.Config{} | |||
} | |||
return transport.TLSClientConfig, nil | |||
} | |||
// Transport method returns `*http.Transport` currently in use or error | |||
// in case currently used `transport` is not a `*http.Transport`. | |||
func (c *Client) transport() (*http.Transport, error) { | |||
if transport, ok := c.httpClient.Transport.(*http.Transport); ok { | |||
return transport, nil | |||
} | |||
return nil, errors.New("current transport is not an *http.Transport instance") | |||
} | |||
// just an internal helper method | |||
func (c *Client) outputLogTo(w io.Writer) *Client { | |||
c.log.(*logger).l.SetOutput(w) | |||
return c | |||
} | |||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
// File struct and its methods | |||
//_______________________________________________________________________ | |||
// File struct represent file information for multipart request | |||
type File struct { | |||
Name string | |||
ParamName string | |||
io.Reader | |||
} | |||
// String returns string value of current file details | |||
func (f *File) String() string { | |||
return fmt.Sprintf("ParamName: %v; FileName: %v", f.ParamName, f.Name) | |||
} | |||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
// MultipartField struct | |||
//_______________________________________________________________________ | |||
// MultipartField struct represent custom data part for multipart request | |||
type MultipartField struct { | |||
Param string | |||
FileName string | |||
ContentType string | |||
io.Reader | |||
} | |||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
// Unexported package methods | |||
//_______________________________________________________________________ | |||
func createClient(hc *http.Client) *Client { | |||
if hc.Transport == nil { | |||
hc.Transport = createTransport(nil) | |||
} | |||
c := &Client{ // not setting lang default values | |||
QueryParam: url.Values{}, | |||
FormData: url.Values{}, | |||
Header: http.Header{}, | |||
Cookies: make([]*http.Cookie, 0), | |||
RetryWaitTime: defaultWaitTime, | |||
RetryMaxWaitTime: defaultMaxWaitTime, | |||
JSONMarshal: json.Marshal, | |||
JSONUnmarshal: json.Unmarshal, | |||
jsonEscapeHTML: true, | |||
httpClient: hc, | |||
debugBodySizeLimit: math.MaxInt32, | |||
pathParams: make(map[string]string), | |||
} | |||
// Logger | |||
c.SetLogger(createLogger()) | |||
// default before request middlewares | |||
c.beforeRequest = []RequestMiddleware{ | |||
parseRequestURL, | |||
parseRequestHeader, | |||
parseRequestBody, | |||
createHTTPRequest, | |||
addCredentials, | |||
} | |||
// user defined request middlewares | |||
c.udBeforeRequest = []RequestMiddleware{} | |||
// default after response middlewares | |||
c.afterResponse = []ResponseMiddleware{ | |||
responseLogger, | |||
parseResponseBody, | |||
saveResponseIntoFile, | |||
} | |||
return c | |||
} |
@@ -0,0 +1,5 @@ | |||
module github.com/go-resty/resty/v2 | |||
require golang.org/x/net v0.0.0-20200513185701-a91f0712d120 | |||
go 1.11 |
@@ -0,0 +1,526 @@ | |||
// Copyright (c) 2015-2020 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" | |||
"encoding/xml" | |||
"errors" | |||
"fmt" | |||
"io" | |||
"io/ioutil" | |||
"mime/multipart" | |||
"net/http" | |||
"net/url" | |||
"os" | |||
"path/filepath" | |||
"reflect" | |||
"strings" | |||
"time" | |||
) | |||
const debugRequestLogKey = "__restyDebugRequestLog" | |||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
// Request Middleware(s) | |||
//_______________________________________________________________________ | |||
func parseRequestURL(c *Client, r *Request) error { | |||
// GitHub #103 Path Params | |||
if len(r.pathParams) > 0 { | |||
for p, v := range r.pathParams { | |||
r.URL = strings.Replace(r.URL, "{"+p+"}", url.PathEscape(v), -1) | |||
} | |||
} | |||
if len(c.pathParams) > 0 { | |||
for p, v := range c.pathParams { | |||
r.URL = strings.Replace(r.URL, "{"+p+"}", url.PathEscape(v), -1) | |||
} | |||
} | |||
// 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 | |||
} | |||
reqURL, err = url.Parse(c.HostURL + r.URL) | |||
if err != nil { | |||
return err | |||
} | |||
} | |||
// Adding Query Param | |||
query := make(url.Values) | |||
for k, v := range c.QueryParam { | |||
for _, iv := range v { | |||
query.Add(k, iv) | |||
} | |||
} | |||
for k, v := range r.QueryParam { | |||
// remove query param from client level by key | |||
// since overrides happens for that key in the request | |||
query.Del(k) | |||
for _, iv := range v { | |||
query.Add(k, iv) | |||
} | |||
} | |||
// 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(query) > 0 { | |||
if IsStringEmpty(reqURL.RawQuery) { | |||
reqURL.RawQuery = query.Encode() | |||
} else { | |||
reqURL.RawQuery = reqURL.RawQuery + "&" + query.Encode() | |||
} | |||
} | |||
r.URL = reqURL.String() | |||
return nil | |||
} | |||
func parseRequestHeader(c *Client, r *Request) error { | |||
hdr := make(http.Header) | |||
for k := range c.Header { | |||
hdr[k] = append(hdr[k], c.Header[k]...) | |||
} | |||
for k := range r.Header { | |||
hdr.Del(k) | |||
hdr[k] = append(hdr[k], r.Header[k]...) | |||
} | |||
if IsStringEmpty(hdr.Get(hdrUserAgentKey)) { | |||
hdr.Set(hdrUserAgentKey, hdrUserAgentValue) | |||
} | |||
ct := hdr.Get(hdrContentTypeKey) | |||
if IsStringEmpty(hdr.Get(hdrAcceptKey)) && !IsStringEmpty(ct) && | |||
(IsJSONType(ct) || IsXMLType(ct)) { | |||
hdr.Set(hdrAcceptKey, hdr.Get(hdrContentTypeKey)) | |||
} | |||
r.Header = hdr | |||
return nil | |||
} | |||
func parseRequestBody(c *Client, r *Request) (err error) { | |||
if isPayloadSupported(r.Method, c.AllowGetMethodPayload) { | |||
// Handling Multipart | |||
if r.isMultiPart && !(r.Method == MethodPatch) { | |||
if err = handleMultipart(c, r); err != nil { | |||
return | |||
} | |||
goto CL | |||
} | |||
// Handling Form Data | |||
if len(c.FormData) > 0 || len(r.FormData) > 0 { | |||
handleFormData(c, r) | |||
goto CL | |||
} | |||
// Handling Request body | |||
if r.Body != nil { | |||
handleContentType(c, r) | |||
if err = handleRequestBody(c, r); err != nil { | |||
return | |||
} | |||
} | |||
} | |||
CL: | |||
// by default resty won't set content length, you can if you want to :) | |||
if (c.setContentLength || r.setContentLength) && r.bodyBuf != nil { | |||
r.Header.Set(hdrContentLengthKey, fmt.Sprintf("%d", r.bodyBuf.Len())) | |||
} | |||
return | |||
} | |||
func createHTTPRequest(c *Client, r *Request) (err error) { | |||
if r.bodyBuf == nil { | |||
if reader, ok := r.Body.(io.Reader); ok { | |||
r.RawRequest, err = http.NewRequest(r.Method, r.URL, reader) | |||
} else { | |||
r.RawRequest, err = http.NewRequest(r.Method, r.URL, nil) | |||
} | |||
} else { | |||
r.RawRequest, err = http.NewRequest(r.Method, r.URL, r.bodyBuf) | |||
} | |||
if err != nil { | |||
return | |||
} | |||
// Assign close connection option | |||
r.RawRequest.Close = c.closeConnection | |||
// Add headers into http request | |||
r.RawRequest.Header = r.Header | |||
// 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) | |||
} | |||
// it's for non-http scheme option | |||
if r.RawRequest.URL != nil && r.RawRequest.URL.Scheme == "" { | |||
r.RawRequest.URL.Scheme = c.scheme | |||
r.RawRequest.URL.Host = r.URL | |||
} | |||
// Enable trace | |||
if c.trace || r.trace { | |||
r.clientTrace = &clientTrace{} | |||
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 | |||
r.RawRequest.GetBody = func() (io.ReadCloser, error) { | |||
// If r.bodyBuf present, return the copy | |||
if r.bodyBuf != nil { | |||
return ioutil.NopCloser(bytes.NewReader(r.bodyBuf.Bytes())), 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 := ioutil.ReadAll(r.RawRequest.Body) | |||
if err != nil { | |||
return nil, err | |||
} | |||
// Restore the Body | |||
closeq(r.RawRequest.Body) | |||
r.RawRequest.Body = ioutil.NopCloser(bytes.NewBuffer(b)) | |||
// Return the Body bytes | |||
return ioutil.NopCloser(bytes.NewBuffer(b)), nil | |||
} | |||
return nil, 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") { | |||
c.log.Warnf("Using Basic Auth in HTTP mode is not secure, use HTTPS") | |||
} | |||
} | |||
// Set the Authorization Header Scheme | |||
var authScheme string | |||
if !IsStringEmpty(r.AuthScheme) { | |||
authScheme = r.AuthScheme | |||
} else if !IsStringEmpty(c.AuthScheme) { | |||
authScheme = c.AuthScheme | |||
} else { | |||
authScheme = "Bearer" | |||
} | |||
// Build the Token Auth header | |||
if !IsStringEmpty(r.Token) { // takes precedence | |||
r.RawRequest.Header.Set(hdrAuthorizationKey, authScheme+" "+r.Token) | |||
} else if !IsStringEmpty(c.Token) { | |||
r.RawRequest.Header.Set(hdrAuthorizationKey, authScheme+" "+c.Token) | |||
} | |||
return nil | |||
} | |||
func requestLogger(c *Client, r *Request) error { | |||
if c.Debug { | |||
rr := r.RawRequest | |||
rl := &RequestLog{Header: copyHeaders(rr.Header), Body: r.fmtBodyString(c.debugBodySizeLimit)} | |||
if c.requestLog != nil { | |||
if err := c.requestLog(rl); err != nil { | |||
return err | |||
} | |||
} | |||
// fmt.Sprintf("COOKIES:\n%s\n", composeCookies(c.GetClient().Jar, *rr.URL)) + | |||
reqLog := "\n==============================================================================\n" + | |||
"~~~ 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 c.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.RawResponse.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 += fmt.Sprintf("BODY :\n***** RESPONSE WRITTEN INTO FILE *****\n") | |||
} else { | |||
debugLog += fmt.Sprintf("BODY :\n%v\n", rl.Body) | |||
} | |||
debugLog += "==============================================================================\n" | |||
c.log.Debugf("%s", debugLog) | |||
} | |||
return nil | |||
} | |||
func parseResponseBody(c *Client, res *Response) (err error) { | |||
if res.StatusCode() == http.StatusNoContent { | |||
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 { | |||
err = Unmarshalc(c, ct, res.body, res.Request.Error) | |||
} | |||
} | |||
} | |||
return | |||
} | |||
func handleMultipart(c *Client, r *Request) (err error) { | |||
r.bodyBuf = acquireBuffer() | |||
w := multipart.NewWriter(r.bodyBuf) | |||
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 | |||
err = addFile(w, k[1:], iv) | |||
if err != nil { | |||
return | |||
} | |||
} else { // form value | |||
if err = w.WriteField(k, iv); err != nil { | |||
return err | |||
} | |||
} | |||
} | |||
} | |||
// #21 - adding io.Reader support | |||
if len(r.multipartFiles) > 0 { | |||
for _, f := range r.multipartFiles { | |||
err = addFileReader(w, f) | |||
if err != nil { | |||
return | |||
} | |||
} | |||
} | |||
// GitHub #130 adding multipart field support with content type | |||
if len(r.multipartFields) > 0 { | |||
for _, mf := range r.multipartFields { | |||
if err = addMultipartFormField(w, mf); err != nil { | |||
return | |||
} | |||
} | |||
} | |||
r.Header.Set(hdrContentTypeKey, w.FormDataContentType()) | |||
err = w.Close() | |||
return | |||
} | |||
func handleFormData(c *Client, r *Request) { | |||
formData := url.Values{} | |||
for k, v := range c.FormData { | |||
for _, iv := range v { | |||
formData.Add(k, iv) | |||
} | |||
} | |||
for k, v := range r.FormData { | |||
// remove form data field from client level by key | |||
// since overrides happens for that key in the request | |||
formData.Del(k) | |||
for _, iv := range v { | |||
formData.Add(k, iv) | |||
} | |||
} | |||
r.bodyBuf = bytes.NewBuffer([]byte(formData.Encode())) | |||
r.Header.Set(hdrContentTypeKey, formContentType) | |||
r.isFormData = true | |||
} | |||
func handleContentType(c *Client, r *Request) { | |||
contentType := r.Header.Get(hdrContentTypeKey) | |||
if IsStringEmpty(contentType) { | |||
contentType = DetectContentType(r.Body) | |||
r.Header.Set(hdrContentTypeKey, contentType) | |||
} | |||
} | |||
func handleRequestBody(c *Client, r *Request) (err error) { | |||
var bodyBytes []byte | |||
contentType := r.Header.Get(hdrContentTypeKey) | |||
kind := kindOf(r.Body) | |||
r.bodyBuf = nil | |||
if reader, ok := r.Body.(io.Reader); ok { | |||
if c.setContentLength || r.setContentLength { // keep backward compatibility | |||
r.bodyBuf = acquireBuffer() | |||
_, err = r.bodyBuf.ReadFrom(reader) | |||
r.Body = nil | |||
} else { | |||
// Otherwise buffer less processing for `io.Reader`, sounds good. | |||
return | |||
} | |||
} else if b, ok := r.Body.([]byte); ok { | |||
bodyBytes = b | |||
} else if s, ok := r.Body.(string); ok { | |||
bodyBytes = []byte(s) | |||
} else if IsJSONType(contentType) && | |||
(kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) { | |||
bodyBytes, err = jsonMarshal(c, r, r.Body) | |||
} else if IsXMLType(contentType) && (kind == reflect.Struct) { | |||
bodyBytes, err = xml.Marshal(r.Body) | |||
} | |||
if bodyBytes == nil && r.bodyBuf == nil { | |||
err = errors.New("unsupported 'Body' type/value") | |||
} | |||
// if any errors during body bytes handling, return it | |||
if err != nil { | |||
return | |||
} | |||
// []byte into Buffer | |||
if bodyBytes != nil && r.bodyBuf == nil { | |||
r.bodyBuf = acquireBuffer() | |||
_, _ = r.bodyBuf.Write(bodyBytes) | |||
} | |||
return | |||
} | |||
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 | |||
} |
@@ -0,0 +1,101 @@ | |||
// Copyright (c) 2015-2020 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" | |||
) | |||
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 jounery, 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 HTTP client | |||
// resty.SetRedirectPolicy(NoRedirectPolicy()) | |||
func NoRedirectPolicy() RedirectPolicy { | |||
return RedirectPolicyFunc(func(req *http.Request, via []*http.Request) error { | |||
return errors.New("auto redirect is disabled") | |||
}) | |||
} | |||
// FlexibleRedirectPolicy is convenient method to create No of redirect policy for HTTP client. | |||
// 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 is convenient method to define domain name redirect rule in resty client. | |||
// Redirect is allowed for only mentioned host 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 | |||
} | |||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
// Package Unexported methods | |||
//_______________________________________________________________________ | |||
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 go throughing various discussion comments from thread | |||
// https://github.com/golang/go/issues/4800 | |||
// Resty will add all the headers during a redirect for the same host | |||
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) | |||
} | |||
} |
@@ -0,0 +1,809 @@ | |||
// Copyright (c) 2015-2020 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" | |||
"context" | |||
"encoding/json" | |||
"encoding/xml" | |||
"fmt" | |||
"io" | |||
"net" | |||
"net/http" | |||
"net/url" | |||
"reflect" | |||
"strings" | |||
"time" | |||
) | |||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
// Request struct and methods | |||
//_______________________________________________________________________ | |||
// Request struct is used to compose and fire individual request from | |||
// resty client. Request provides an options to override client level | |||
// settings and also an options for the request composition. | |||
type Request struct { | |||
URL string | |||
Method string | |||
Token string | |||
AuthScheme string | |||
QueryParam url.Values | |||
FormData url.Values | |||
Header http.Header | |||
Time time.Time | |||
Body interface{} | |||
Result interface{} | |||
Error interface{} | |||
RawRequest *http.Request | |||
SRV *SRVRecord | |||
UserInfo *User | |||
Cookies []*http.Cookie | |||
isMultiPart bool | |||
isFormData bool | |||
setContentLength bool | |||
isSaveResponse bool | |||
notParseResponse bool | |||
jsonEscapeHTML bool | |||
trace bool | |||
outputFile string | |||
fallbackContentType string | |||
forceContentType string | |||
ctx context.Context | |||
pathParams map[string]string | |||
values map[string]interface{} | |||
client *Client | |||
bodyBuf *bytes.Buffer | |||
clientTrace *clientTrace | |||
multipartFiles []*File | |||
multipartFields []*MultipartField | |||
} | |||
// Context method returns the Context if its already set in request | |||
// otherwise it creates new one using `context.Background()`. | |||
func (r *Request) Context() context.Context { | |||
if r.ctx == nil { | |||
return context.Background() | |||
} | |||
return r.ctx | |||
} | |||
// SetContext method sets the context.Context for current Request. It allows | |||
// to interrupt the request execution if ctx.Done() channel is closed. | |||
// See https://blog.golang.org/context article and the "context" package | |||
// documentation. | |||
func (r *Request) SetContext(ctx context.Context) *Request { | |||
r.ctx = ctx | |||
return r | |||
} | |||
// SetHeader method is to set a single header field and its value in the current request. | |||
// | |||
// For Example: To set `Content-Type` and `Accept` as `application/json`. | |||
// client.R(). | |||
// SetHeader("Content-Type", "application/json"). | |||
// SetHeader("Accept", "application/json") | |||
// | |||
// Also you can override header value, which was set at client instance level. | |||
func (r *Request) SetHeader(header, value string) *Request { | |||
r.Header.Set(header, value) | |||
return r | |||
} | |||
// SetHeaders method sets multiple headers field and its values at one go in the current request. | |||
// | |||
// For Example: To set `Content-Type` and `Accept` as `application/json` | |||
// | |||
// client.R(). | |||
// SetHeaders(map[string]string{ | |||
// "Content-Type": "application/json", | |||
// "Accept": "application/json", | |||
// }) | |||
// Also you can override header value, which was set at client instance level. | |||
func (r *Request) SetHeaders(headers map[string]string) *Request { | |||
for h, v := range headers { | |||
r.SetHeader(h, v) | |||
} | |||
return r | |||
} | |||
// SetQueryParam method sets single parameter and its value in the current request. | |||
// It will be formed as query string for the request. | |||
// | |||
// For Example: `search=kitchen%20papers&size=large` in the URL after `?` mark. | |||
// client.R(). | |||
// SetQueryParam("search", "kitchen papers"). | |||
// SetQueryParam("size", "large") | |||
// Also you can override query params value, which was set at client instance level. | |||
func (r *Request) SetQueryParam(param, value string) *Request { | |||
r.QueryParam.Set(param, value) | |||
return r | |||
} | |||
// SetQueryParams method sets multiple parameters and its values at one go in the current request. | |||
// It will be formed as query string for the request. | |||
// | |||
// For Example: `search=kitchen%20papers&size=large` in the URL after `?` mark. | |||
// client.R(). | |||
// SetQueryParams(map[string]string{ | |||
// "search": "kitchen papers", | |||
// "size": "large", | |||
// }) | |||
// Also you can override query params value, which was set at client instance level. | |||
func (r *Request) SetQueryParams(params map[string]string) *Request { | |||
for p, v := range params { | |||
r.SetQueryParam(p, v) | |||
} | |||
return r | |||
} | |||
// SetQueryParamsFromValues method appends multiple parameters with multi-value | |||
// (`url.Values`) at one go in the current request. It will be formed as | |||
// query string for the request. | |||
// | |||
// For Example: `status=pending&status=approved&status=open` in the URL after `?` mark. | |||
// client.R(). | |||
// SetQueryParamsFromValues(url.Values{ | |||
// "status": []string{"pending", "approved", "open"}, | |||
// }) | |||
// Also you can override query params value, which was set at client instance level. | |||
func (r *Request) SetQueryParamsFromValues(params url.Values) *Request { | |||
for p, v := range params { | |||
for _, pv := range v { | |||
r.QueryParam.Add(p, pv) | |||
} | |||
} | |||
return r | |||
} | |||
// SetQueryString method provides ability to use string as an input to set URL query string for the request. | |||
// | |||
// Using String as an input | |||
// client.R(). | |||
// SetQueryString("productId=232&template=fresh-sample&cat=resty&source=google&kw=buy a lot more") | |||
func (r *Request) SetQueryString(query string) *Request { | |||
params, err := url.ParseQuery(strings.TrimSpace(query)) | |||
if err == nil { | |||
for p, v := range params { | |||
for _, pv := range v { | |||
r.QueryParam.Add(p, pv) | |||
} | |||
} | |||
} else { | |||
r.client.log.Errorf("%v", err) | |||
} | |||
return r | |||
} | |||
// SetFormData method sets Form parameters and their values in the current request. | |||
// It's applicable only HTTP method `POST` and `PUT` and requests content type would be set as | |||
// `application/x-www-form-urlencoded`. | |||
// client.R(). | |||
// SetFormData(map[string]string{ | |||
// "access_token": "BC594900-518B-4F7E-AC75-BD37F019E08F", | |||
// "user_id": "3455454545", | |||
// }) | |||
// Also you can override form data value, which was set at client instance level. | |||
func (r *Request) SetFormData(data map[string]string) *Request { | |||
for k, v := range data { | |||
r.FormData.Set(k, v) | |||
} | |||
return r | |||
} | |||
// SetFormDataFromValues method appends multiple form parameters with multi-value | |||
// (`url.Values`) at one go in the current request. | |||
// client.R(). | |||
// SetFormDataFromValues(url.Values{ | |||
// "search_criteria": []string{"book", "glass", "pencil"}, | |||
// }) | |||
// Also you can override form data value, which was set at client instance level. | |||
func (r *Request) SetFormDataFromValues(data url.Values) *Request { | |||
for k, v := range data { | |||
for _, kv := range v { | |||
r.FormData.Add(k, kv) | |||
} | |||
} | |||
return r | |||
} | |||
// SetBody method sets the request body for the request. It supports various realtime needs as easy. | |||
// We can say its quite handy or powerful. Supported request body data types is `string`, | |||
// `[]byte`, `struct`, `map`, `slice` and `io.Reader`. Body value can be pointer or non-pointer. | |||
// Automatic marshalling for JSON and XML content type, if it is `struct`, `map`, or `slice`. | |||
// | |||
// Note: `io.Reader` is processed as bufferless mode while sending request. | |||
// | |||
// For Example: Struct as a body input, based on content type, it will be marshalled. | |||
// client.R(). | |||
// SetBody(User{ | |||
// Username: "jeeva@myjeeva.com", | |||
// Password: "welcome2resty", | |||
// }) | |||
// | |||
// Map as a body input, based on content type, it will be marshalled. | |||
// client.R(). | |||
// SetBody(map[string]interface{}{ | |||
// "username": "jeeva@myjeeva.com", | |||
// "password": "welcome2resty", | |||
// "address": &Address{ | |||
// Address1: "1111 This is my street", | |||
// Address2: "Apt 201", | |||
// City: "My City", | |||
// State: "My State", | |||
// ZipCode: 00000, | |||
// }, | |||
// }) | |||
// | |||
// String as a body input. Suitable for any need as a string input. | |||
// client.R(). | |||
// SetBody(`{ | |||
// "username": "jeeva@getrightcare.com", | |||
// "password": "admin" | |||
// }`) | |||
// | |||
// []byte as a body input. Suitable for raw request such as file upload, serialize & deserialize, etc. | |||
// client.R(). | |||
// SetBody([]byte("This is my raw request, sent as-is")) | |||
func (r *Request) SetBody(body interface{}) *Request { | |||
r.Body = body | |||
return r | |||
} | |||
// SetResult method is to register the response `Result` object for automatic unmarshalling for the request, | |||
// if response status code is between 200 and 299 and content type either JSON or XML. | |||
// | |||
// Note: Result object can be pointer or non-pointer. | |||
// client.R().SetResult(&AuthToken{}) | |||
// // OR | |||
// client.R().SetResult(AuthToken{}) | |||
// | |||
// Accessing a result value from response instance. | |||
// response.Result().(*AuthToken) | |||
func (r *Request) SetResult(res interface{}) *Request { | |||
r.Result = getPointer(res) | |||
return r | |||
} | |||
// SetError method is to register the request `Error` object for automatic unmarshalling for the request, | |||
// if response status code is greater than 399 and content type either JSON or XML. | |||
// | |||
// Note: Error object can be pointer or non-pointer. | |||
// client.R().SetError(&AuthError{}) | |||
// // OR | |||
// client.R().SetError(AuthError{}) | |||
// | |||
// Accessing a error value from response instance. | |||
// response.Error().(*AuthError) | |||
func (r *Request) SetError(err interface{}) *Request { | |||
r.Error = getPointer(err) | |||
return r | |||
} | |||
// SetFile method is to set single file field name and its path for multipart upload. | |||
// client.R(). | |||
// SetFile("my_file", "/Users/jeeva/Gas Bill - Sep.pdf") | |||
func (r *Request) SetFile(param, filePath string) *Request { | |||
r.isMultiPart = true | |||
r.FormData.Set("@"+param, filePath) | |||
return r | |||
} | |||
// SetFiles method is to set multiple file field name and its path for multipart upload. | |||
// client.R(). | |||
// SetFiles(map[string]string{ | |||
// "my_file1": "/Users/jeeva/Gas Bill - Sep.pdf", | |||
// "my_file2": "/Users/jeeva/Electricity Bill - Sep.pdf", | |||
// "my_file3": "/Users/jeeva/Water Bill - Sep.pdf", | |||
// }) | |||
func (r *Request) SetFiles(files map[string]string) *Request { | |||
r.isMultiPart = true | |||
for f, fp := range files { | |||
r.FormData.Set("@"+f, fp) | |||
} | |||
return r | |||
} | |||
// SetFileReader method is to set single file using io.Reader for multipart upload. | |||
// client.R(). | |||
// SetFileReader("profile_img", "my-profile-img.png", bytes.NewReader(profileImgBytes)). | |||
// SetFileReader("notes", "user-notes.txt", bytes.NewReader(notesBytes)) | |||
func (r *Request) SetFileReader(param, fileName string, reader io.Reader) *Request { | |||
r.isMultiPart = true | |||
r.multipartFiles = append(r.multipartFiles, &File{ | |||
Name: fileName, | |||
ParamName: param, | |||
Reader: reader, | |||
}) | |||
return r | |||
} | |||
// SetMultipartFormData method allows simple form data to be attached to the request as `multipart:form-data` | |||
func (r *Request) SetMultipartFormData(data map[string]string) *Request { | |||
for k, v := range data { | |||
r = r.SetMultipartField(k, "", "", strings.NewReader(v)) | |||
} | |||
return r | |||
} | |||
// SetMultipartField method is to set custom data using io.Reader for multipart upload. | |||
func (r *Request) SetMultipartField(param, fileName, contentType string, reader io.Reader) *Request { | |||
r.isMultiPart = true | |||
r.multipartFields = append(r.multipartFields, &MultipartField{ | |||
Param: param, | |||
FileName: fileName, | |||
ContentType: contentType, | |||
Reader: reader, | |||
}) | |||
return r | |||
} | |||
// SetMultipartFields method is to set multiple data fields using io.Reader for multipart upload. | |||
// | |||
// For Example: | |||
// client.R().SetMultipartFields( | |||
// &resty.MultipartField{ | |||
// Param: "uploadManifest1", | |||
// FileName: "upload-file-1.json", | |||
// ContentType: "application/json", | |||
// Reader: strings.NewReader(`{"input": {"name": "Uploaded document 1", "_filename" : ["file1.txt"]}}`), | |||
// }, | |||
// &resty.MultipartField{ | |||
// Param: "uploadManifest2", | |||
// FileName: "upload-file-2.json", | |||
// ContentType: "application/json", | |||
// Reader: strings.NewReader(`{"input": {"name": "Uploaded document 2", "_filename" : ["file2.txt"]}}`), | |||
// }) | |||
// | |||
// If you have slice already, then simply call- | |||
// client.R().SetMultipartFields(fields...) | |||
func (r *Request) SetMultipartFields(fields ...*MultipartField) *Request { | |||
r.isMultiPart = true | |||
r.multipartFields = append(r.multipartFields, fields...) | |||
return r | |||
} | |||
// SetContentLength method sets the HTTP header `Content-Length` value for current request. | |||
// By default Resty won't set `Content-Length`. Also you have an option to enable for every | |||
// request. | |||
// | |||
// See `Client.SetContentLength` | |||
// client.R().SetContentLength(true) | |||
func (r *Request) SetContentLength(l bool) *Request { | |||
r.setContentLength = true | |||
return r | |||
} | |||
// SetBasicAuth method sets the basic authentication header in the current HTTP request. | |||
// | |||
// For Example: | |||
// Authorization: Basic <base64-encoded-value> | |||
// | |||
// To set the header for username "go-resty" and password "welcome" | |||
// client.R().SetBasicAuth("go-resty", "welcome") | |||
// | |||
// This method overrides the credentials set by method `Client.SetBasicAuth`. | |||
func (r *Request) SetBasicAuth(username, password string) *Request { | |||
r.UserInfo = &User{Username: username, Password: password} | |||
return r | |||
} | |||
// SetAuthToken method sets the auth token header(Default Scheme: Bearer) in the current HTTP request. Header example: | |||
// Authorization: Bearer <auth-token-value-comes-here> | |||
// | |||
// For Example: To set auth token BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F | |||
// | |||
// client.R().SetAuthToken("BC594900518B4F7EAC75BD37F019E08FBC594900518B4F7EAC75BD37F019E08F") | |||
// | |||
// This method overrides the Auth token set by method `Client.SetAuthToken`. | |||
func (r *Request) SetAuthToken(token string) *Request { | |||
r.Token = token | |||
return r | |||
} | |||
// SetAuthScheme method sets the auth token scheme type in the HTTP request. For Example: | |||
// Authorization: <auth-scheme-value-set-here> <auth-token-value> | |||
// | |||
// For Example: To set the scheme to use OAuth | |||
// | |||
// client.R().SetAuthScheme("OAuth") | |||
// | |||
// This auth header scheme gets added to all the request rasied from this client instance. | |||
// Also it can be overridden or set one at the request level is supported. | |||
// | |||
// Information about Auth schemes can be found in RFC7235 which is linked to below along with the page containing | |||
// the currently defined official authentication schemes: | |||
// https://tools.ietf.org/html/rfc7235 | |||
// https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml#authschemes | |||
// | |||
// This method overrides the Authorization scheme set by method `Client.SetAuthScheme`. | |||
func (r *Request) SetAuthScheme(scheme string) *Request { | |||
r.AuthScheme = scheme | |||
return r | |||
} | |||
// SetOutput method sets the output file for current HTTP request. Current HTTP response will be | |||
// saved into given file. It is similar to `curl -o` flag. Absolute path or relative path can be used. | |||
// If is it relative path then output file goes under the output directory, as mentioned | |||
// in the `Client.SetOutputDirectory`. | |||
// client.R(). | |||
// SetOutput("/Users/jeeva/Downloads/ReplyWithHeader-v5.1-beta.zip"). | |||
// Get("http://bit.ly/1LouEKr") | |||
// | |||
// Note: In this scenario `Response.Body` might be nil. | |||
func (r *Request) SetOutput(file string) *Request { | |||
r.outputFile = file | |||
r.isSaveResponse = true | |||
return r | |||
} | |||
// SetSRV method sets the details to query the service SRV record and execute the | |||
// request. | |||
// client.R(). | |||
// SetSRV(SRVRecord{"web", "testservice.com"}). | |||
// Get("/get") | |||
func (r *Request) SetSRV(srv *SRVRecord) *Request { | |||
r.SRV = srv | |||
return r | |||
} | |||
// SetDoNotParseResponse method instructs `Resty` not to parse the response body automatically. | |||
// Resty exposes the raw response body as `io.ReadCloser`. Also do not forget to close the body, | |||
// otherwise you might get into connection leaks, no connection reuse. | |||
// | |||
// Note: Response middlewares are not applicable, if you use this option. Basically you have | |||
// taken over the control of response parsing from `Resty`. | |||
func (r *Request) SetDoNotParseResponse(parse bool) *Request { | |||
r.notParseResponse = parse | |||
return r | |||
} | |||
// SetPathParams method sets multiple URL path key-value pairs at one go in the | |||
// Resty current request instance. | |||
// client.R().SetPathParams(map[string]string{ | |||
// "userId": "sample@sample.com", | |||
// "subAccountId": "100002", | |||
// }) | |||
// | |||
// Result: | |||
// URL - /v1/users/{userId}/{subAccountId}/details | |||
// Composed URL - /v1/users/sample@sample.com/100002/details | |||
// It replace the value of the key while composing request URL. Also you can | |||
// override Path Params value, which was set at client instance level. | |||
func (r *Request) SetPathParams(params map[string]string) *Request { | |||
for p, v := range params { | |||
r.pathParams[p] = v | |||
} | |||
return r | |||
} | |||
// ExpectContentType method allows to provide fallback `Content-Type` for automatic unmarshalling | |||
// when `Content-Type` response header is unavailable. | |||
func (r *Request) ExpectContentType(contentType string) *Request { | |||
r.fallbackContentType = contentType | |||
return r | |||
} | |||
// ForceContentType method provides a strong sense of response `Content-Type` for automatic unmarshalling. | |||
// Resty will respect it with higher priority; even response `Content-Type` response header value is available. | |||
func (r *Request) ForceContentType(contentType string) *Request { | |||
r.forceContentType = contentType | |||
return r | |||
} | |||
// SetJSONEscapeHTML method is to enable/disable the HTML escape on JSON marshal. | |||
// | |||
// Note: This option only applicable to standard JSON Marshaller. | |||
func (r *Request) SetJSONEscapeHTML(b bool) *Request { | |||
r.jsonEscapeHTML = b | |||
return r | |||
} | |||
// SetCookie method appends a single cookie in the current request instance. | |||
// client.R().SetCookie(&http.Cookie{ | |||
// Name:"go-resty", | |||
// Value:"This is cookie value", | |||
// }) | |||
// | |||
// Note: Method appends the Cookie value into existing Cookie if already existing. | |||
// | |||
// Since v2.1.0 | |||
func (r *Request) SetCookie(hc *http.Cookie) *Request { | |||
r.Cookies = append(r.Cookies, hc) | |||
return r | |||
} | |||
// SetCookies method sets an array of cookies in the current request instance. | |||
// cookies := []*http.Cookie{ | |||
// &http.Cookie{ | |||
// Name:"go-resty-1", | |||
// Value:"This is cookie 1 value", | |||
// }, | |||
// &http.Cookie{ | |||
// Name:"go-resty-2", | |||
// Value:"This is cookie 2 value", | |||
// }, | |||
// } | |||
// | |||
// // Setting a cookies into resty's current request | |||
// client.R().SetCookies(cookies) | |||
// | |||
// Note: Method appends the Cookie value into existing Cookie if already existing. | |||
// | |||
// Since v2.1.0 | |||
func (r *Request) SetCookies(rs []*http.Cookie) *Request { | |||
r.Cookies = append(r.Cookies, rs...) | |||
return r | |||
} | |||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
// HTTP request tracing | |||
//_______________________________________________________________________ | |||
// EnableTrace method enables trace for the current request | |||
// using `httptrace.ClientTrace` and provides insights. | |||
// | |||
// client := resty.New() | |||
// | |||
// resp, err := client.R().EnableTrace().Get("https://httpbin.org/get") | |||
// fmt.Println("Error:", err) | |||
// fmt.Println("Trace Info:", resp.Request.TraceInfo()) | |||
// | |||
// See `Client.EnableTrace` available too to get trace info for all requests. | |||
// | |||
// Since v2.0.0 | |||
func (r *Request) EnableTrace() *Request { | |||
r.trace = true | |||
return r | |||
} | |||
// TraceInfo method returns the trace info for the request. | |||
// If either the Client or Request EnableTrace function has not been called | |||
// prior to the request being made, an empty TraceInfo object will be returned. | |||
// | |||
// Since v2.0.0 | |||
func (r *Request) TraceInfo() TraceInfo { | |||
ct := r.clientTrace | |||
if ct == nil { | |||
return TraceInfo{} | |||
} | |||
ti := TraceInfo{ | |||
DNSLookup: ct.dnsDone.Sub(ct.dnsStart), | |||
TLSHandshake: ct.tlsHandshakeDone.Sub(ct.tlsHandshakeStart), | |||
ServerTime: ct.gotFirstResponseByte.Sub(ct.gotConn), | |||
TotalTime: ct.endTime.Sub(ct.dnsStart), | |||
IsConnReused: ct.gotConnInfo.Reused, | |||
IsConnWasIdle: ct.gotConnInfo.WasIdle, | |||
ConnIdleTime: ct.gotConnInfo.IdleTime, | |||
} | |||
// Only calcuate on successful connections | |||
if !ct.connectDone.IsZero() { | |||
ti.TCPConnTime = ct.connectDone.Sub(ct.dnsDone) | |||
} | |||
// Only calcuate on successful connections | |||
if !ct.gotConn.IsZero() { | |||
ti.ConnTime = ct.gotConn.Sub(ct.getConn) | |||
} | |||
// Only calcuate on successful connections | |||
if !ct.gotFirstResponseByte.IsZero() { | |||
ti.ResponseTime = ct.endTime.Sub(ct.gotFirstResponseByte) | |||
} | |||
return ti | |||
} | |||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
// HTTP verb method starts here | |||
//_______________________________________________________________________ | |||
// Get method does GET HTTP request. It's defined in section 4.3.1 of RFC7231. | |||
func (r *Request) Get(url string) (*Response, error) { | |||
return r.Execute(MethodGet, url) | |||
} | |||
// Head method does HEAD HTTP request. It's defined in section 4.3.2 of RFC7231. | |||
func (r *Request) Head(url string) (*Response, error) { | |||
return r.Execute(MethodHead, url) | |||
} | |||
// Post method does POST HTTP request. It's defined in section 4.3.3 of RFC7231. | |||
func (r *Request) Post(url string) (*Response, error) { | |||
return r.Execute(MethodPost, url) | |||
} | |||
// Put method does PUT HTTP request. It's defined in section 4.3.4 of RFC7231. | |||
func (r *Request) Put(url string) (*Response, error) { | |||
return r.Execute(MethodPut, url) | |||
} | |||
// Delete method does DELETE HTTP request. It's defined in section 4.3.5 of RFC7231. | |||
func (r *Request) Delete(url string) (*Response, error) { | |||
return r.Execute(MethodDelete, url) | |||
} | |||
// Options method does OPTIONS HTTP request. It's defined in section 4.3.7 of RFC7231. | |||
func (r *Request) Options(url string) (*Response, error) { | |||
return r.Execute(MethodOptions, url) | |||
} | |||
// Patch method does PATCH HTTP request. It's defined in section 2 of RFC5789. | |||
func (r *Request) Patch(url string) (*Response, error) { | |||
return r.Execute(MethodPatch, url) | |||
} | |||
// Send method performs the HTTP request using the method and URL already defined | |||
// for current `Request`. | |||
// req := client.R() | |||
// req.Method = resty.GET | |||
// req.URL = "http://httpbin.org/get" | |||
// resp, err := client.R().Send() | |||
func (r *Request) Send() (*Response, error) { | |||
return r.Execute(r.Method, r.URL) | |||
} | |||
// Execute method performs the HTTP request with given HTTP method and URL | |||
// for current `Request`. | |||
// resp, err := client.R().Execute(resty.GET, "http://httpbin.org/get") | |||
func (r *Request) Execute(method, url string) (*Response, error) { | |||
var addrs []*net.SRV | |||
var resp *Response | |||
var err error | |||
if r.isMultiPart && !(method == MethodPost || method == MethodPut || method == MethodPatch) { | |||
return nil, fmt.Errorf("multipart content is not allowed in HTTP verb [%v]", method) | |||
} | |||
if r.SRV != nil { | |||
_, addrs, err = net.LookupSRV(r.SRV.Service, "tcp", r.SRV.Domain) | |||
if err != nil { | |||
return nil, err | |||
} | |||
} | |||
r.Method = method | |||
r.URL = r.selectAddr(addrs, url, 0) | |||
if r.client.RetryCount == 0 { | |||
resp, err = r.client.execute(r) | |||
return resp, unwrapNoRetryErr(err) | |||
} | |||
attempt := 0 | |||
err = Backoff( | |||
func() (*Response, error) { | |||
attempt++ | |||
r.URL = r.selectAddr(addrs, url, attempt) | |||
resp, err = r.client.execute(r) | |||
if err != nil { | |||
r.client.log.Errorf("%v, Attempt %v", err, attempt) | |||
} | |||
return resp, err | |||
}, | |||
Retries(r.client.RetryCount), | |||
WaitTime(r.client.RetryWaitTime), | |||
MaxWaitTime(r.client.RetryMaxWaitTime), | |||
RetryConditions(r.client.RetryConditions), | |||
) | |||
return resp, unwrapNoRetryErr(err) | |||
} | |||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
// SRVRecord struct | |||
//_______________________________________________________________________ | |||
// SRVRecord struct holds the data to query the SRV record for the | |||
// following service. | |||
type SRVRecord struct { | |||
Service string | |||
Domain string | |||
} | |||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
// Request Unexported methods | |||
//_______________________________________________________________________ | |||
func (r *Request) fmtBodyString(sl int64) (body string) { | |||
body = "***** NO CONTENT *****" | |||
if !isPayloadSupported(r.Method, r.client.AllowGetMethodPayload) { | |||
return | |||
} | |||
if _, ok := r.Body.(io.Reader); ok { | |||
body = "***** BODY IS io.Reader *****" | |||
return | |||
} | |||
// multipart or form-data | |||
if r.isMultiPart || r.isFormData { | |||
bodySize := int64(r.bodyBuf.Len()) | |||
if bodySize > sl { | |||
body = fmt.Sprintf("***** REQUEST TOO LARGE (size - %d) *****", bodySize) | |||
return | |||
} | |||
body = r.bodyBuf.String() | |||
return | |||
} | |||
// request body data | |||
if r.Body == nil { | |||
return | |||
} | |||
var prtBodyBytes []byte | |||
var err error | |||
contentType := r.Header.Get(hdrContentTypeKey) | |||
kind := kindOf(r.Body) | |||
if canJSONMarshal(contentType, kind) { | |||
prtBodyBytes, err = json.MarshalIndent(&r.Body, "", " ") | |||
} else if IsXMLType(contentType) && (kind == reflect.Struct) { | |||
prtBodyBytes, err = xml.MarshalIndent(&r.Body, "", " ") | |||
} else if b, ok := r.Body.(string); ok { | |||
if IsJSONType(contentType) { | |||
bodyBytes := []byte(b) | |||
out := acquireBuffer() | |||
defer releaseBuffer(out) | |||
if err = json.Indent(out, bodyBytes, "", " "); err == nil { | |||
prtBodyBytes = out.Bytes() | |||
} | |||
} else { | |||
body = b | |||
} | |||
} else if b, ok := r.Body.([]byte); ok { | |||
body = fmt.Sprintf("***** BODY IS byte(s) (size - %d) *****", len(b)) | |||
return | |||
} | |||
if prtBodyBytes != nil && err == nil { | |||
body = string(prtBodyBytes) | |||
} | |||
if len(body) > 0 { | |||
bodySize := int64(len([]byte(body))) | |||
if bodySize > sl { | |||
body = fmt.Sprintf("***** REQUEST TOO LARGE (size - %d) *****", bodySize) | |||
} | |||
} | |||
return | |||
} | |||
func (r *Request) selectAddr(addrs []*net.SRV, path string, attempt int) string { | |||
if addrs == nil { | |||
return path | |||
} | |||
idx := attempt % len(addrs) | |||
domain := strings.TrimRight(addrs[idx].Target, ".") | |||
path = strings.TrimLeft(path, "/") | |||
return fmt.Sprintf("%s://%s:%d/%s", r.client.scheme, domain, addrs[idx].Port, path) | |||
} | |||
func (r *Request) initValuesMap() { | |||
if r.values == nil { | |||
r.values = make(map[string]interface{}) | |||
} | |||
} | |||
var noescapeJSONMarshal = func(v interface{}) ([]byte, error) { | |||
buf := acquireBuffer() | |||
defer releaseBuffer(buf) | |||
encoder := json.NewEncoder(buf) | |||
encoder.SetEscapeHTML(false) | |||
err := encoder.Encode(v) | |||
return buf.Bytes(), err | |||
} |
@@ -0,0 +1,175 @@ | |||
// Copyright (c) 2015-2020 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 request. | |||
type Response struct { | |||
Request *Request | |||
RawResponse *http.Response | |||
body []byte | |||
size int64 | |||
receivedAt time.Time | |||
} | |||
// Body method returns HTTP response as []byte array for the executed request. | |||
// | |||
// Note: `Response.Body` might be nil, if `Request.SetOutput` is used. | |||
func (r *Response) Body() []byte { | |||
if r.RawResponse == nil { | |||
return []byte{} | |||
} | |||
return r.body | |||
} | |||
// 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 | |||
func (r *Response) Result() interface{} { | |||
return r.Request.Result | |||
} | |||
// Error method returns the error object if it has one | |||
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 access 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 server response as String. | |||
func (r *Response) String() string { | |||
if r.body == nil { | |||
return "" | |||
} | |||
return strings.TrimSpace(string(r.body)) | |||
} | |||
// Time method returns the time of HTTP response time that from request we sent and received a request. | |||
// | |||
// See `Response.ReceivedAt` to know when client recevied response and see `Response.Request.Time` to know | |||
// when 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 when response got recevied from server for the request. | |||
func (r *Response) ReceivedAt() time.Time { | |||
return r.receivedAt | |||
} | |||
// Size method returns the HTTP response size in bytes. Ya, you can relay on HTTP `Content-Length` header, | |||
// however it won't be good for chucked transfer/compressed response. Since Resty calculates response size | |||
// at the client end. You will get actual size of the http response. | |||
func (r *Response) Size() int64 { | |||
return r.size | |||
} | |||
// RawBody method exposes the HTTP raw response body. Use this method in-conjunction with `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. | |||
// Basically 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 | |||
} | |||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
// Response Unexported methods | |||
//_______________________________________________________________________ | |||
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.body != nil { | |||
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 *****" | |||
} |
@@ -0,0 +1,40 @@ | |||
// Copyright (c) 2015-2020 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.3.0" | |||
// 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 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), | |||
}) | |||
} |
@@ -0,0 +1,181 @@ | |||
// Copyright (c) 2015-2020 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" | |||
"math" | |||
"math/rand" | |||
"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 retry condition function | |||
// input: non-nil Response OR request execution error | |||
RetryConditionFunc func(*Response, error) bool | |||
// 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 request is not retryable | |||
// (0, nil) is a special result 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 | |||
} | |||
) | |||
// 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 | |||
} | |||
} | |||
// 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 | |||
} | |||
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 { | |||
goto defaultCase | |||
} | |||
// 1. Check for custom callback | |||
if retryAfterFunc := resp.Request.client.RetryAfter; retryAfterFunc != nil { | |||
result, err := retryAfterFunc(resp.Request.client, resp) | |||
if err != nil { | |||
return 0, err // i.e. 'API quota exceeded' | |||
} | |||
if result == 0 { | |||
goto defaultCase | |||
} | |||
if result < 0 || max < result { | |||
result = max | |||
} | |||
if result < min { | |||
result = min | |||
} | |||
return result, nil | |||
} | |||
// 2. Return capped exponential backoff with jitter | |||
// http://www.awsarchitectureblog.com/2015/03/backoff.html | |||
defaultCase: | |||
base := float64(min) | |||
capLevel := float64(max) | |||
temp := math.Min(capLevel, base*math.Exp2(float64(attempt))) | |||
ri := int(temp / 2) | |||
if ri <= 0 { | |||
ri = maxInt // max int for arch 386 | |||
} | |||
result := time.Duration(math.Abs(float64(ri + rand.Intn(ri)))) | |||
if result < min { | |||
result = min | |||
} | |||
return result, nil | |||
} |
@@ -0,0 +1,122 @@ | |||
// Copyright (c) 2015-2020 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/http/httptrace" | |||
"time" | |||
) | |||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
// TraceInfo struct | |||
//_______________________________________________________________________ | |||
// TraceInfo struct is used provide request trace info such as DNS lookup | |||
// duration, Connection obtain duration, Server processing duration, etc. | |||
// | |||
// Since v2.0.0 | |||
type TraceInfo struct { | |||
// DNSLookup is a duration that transport took to perform | |||
// DNS lookup. | |||
DNSLookup time.Duration | |||
// ConnTime is a duration that took to obtain a successful connection. | |||
ConnTime time.Duration | |||
// TCPConnTime is a duration that took to obtain the TCP connection. | |||
TCPConnTime time.Duration | |||
// TLSHandshake is a duration that TLS handshake took place. | |||
TLSHandshake time.Duration | |||
// ServerTime is a duration that server took to respond first byte. | |||
ServerTime time.Duration | |||
// ResponseTime is a duration since first response byte from server to | |||
// request completion. | |||
ResponseTime time.Duration | |||
// TotalTime is a duration that total request took 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 a duration how long the connection was previously | |||
// idle, if IsConnWasIdle is true. | |||
ConnIdleTime time.Duration | |||
} | |||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
// CientTrace struct and its methods | |||
//_______________________________________________________________________ | |||
// tracer struct maps the `httptrace.ClientTrace` hooks into Fields | |||
// with same naming for easy understanding. Plus additional insights | |||
// Request. | |||
type clientTrace struct { | |||
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 | |||
} | |||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
// Trace unexported methods | |||
//_______________________________________________________________________ | |||
func (t *clientTrace) createContext(ctx context.Context) context.Context { | |||
return httptrace.WithClientTrace( | |||
ctx, | |||
&httptrace.ClientTrace{ | |||
DNSStart: func(_ httptrace.DNSStartInfo) { | |||
t.dnsStart = time.Now() | |||
}, | |||
DNSDone: func(_ httptrace.DNSDoneInfo) { | |||
t.dnsDone = time.Now() | |||
}, | |||
ConnectStart: func(_, _ string) { | |||
if t.dnsDone.IsZero() { | |||
t.dnsDone = time.Now() | |||
} | |||
if t.dnsStart.IsZero() { | |||
t.dnsStart = t.dnsDone | |||
} | |||
}, | |||
ConnectDone: func(net, addr string, err error) { | |||
t.connectDone = time.Now() | |||
}, | |||
GetConn: func(_ string) { | |||
t.getConn = time.Now() | |||
}, | |||
GotConn: func(ci httptrace.GotConnInfo) { | |||
t.gotConn = time.Now() | |||
t.gotConnInfo = ci | |||
}, | |||
GotFirstResponseByte: func() { | |||
t.gotFirstResponseByte = time.Now() | |||
}, | |||
TLSHandshakeStart: func() { | |||
t.tlsHandshakeStart = time.Now() | |||
}, | |||
TLSHandshakeDone: func(_ tls.ConnectionState, _ error) { | |||
t.tlsHandshakeDone = time.Now() | |||
}, | |||
}, | |||
) | |||
} |
@@ -0,0 +1,35 @@ | |||
// +build go1.13 | |||
// Copyright (c) 2015-2020 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, | |||
ForceAttemptHTTP2: true, | |||
MaxIdleConns: 100, | |||
IdleConnTimeout: 90 * time.Second, | |||
TLSHandshakeTimeout: 10 * time.Second, | |||
ExpectContinueTimeout: 1 * time.Second, | |||
MaxIdleConnsPerHost: runtime.GOMAXPROCS(0) + 1, | |||
} | |||
} |
@@ -0,0 +1,34 @@ | |||
// +build !go1.13 | |||
// Copyright (c) 2015-2020 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, | |||
} | |||
} |
@@ -0,0 +1,357 @@ | |||
// Copyright (c) 2015-2020 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" | |||
"encoding/xml" | |||
"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...) | |||
} | |||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
// 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 = xml.Unmarshal(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 | |||
} | |||
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾ | |||
// Package Unexported methods | |||
//_______________________________________________________________________ | |||
// way to disable the HTML escape as opt-in | |||
func jsonMarshal(c *Client, r *Request, d interface{}) ([]byte, error) { | |||
if !r.jsonEscapeHTML { | |||
return noescapeJSONMarshal(d) | |||
} else if !c.jsonEscapeHTML { | |||
return noescapeJSONMarshal(d) | |||
} | |||
return c.JSONMarshal(d) | |||
} | |||
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 { | |||
return err | |||
} | |||
partWriter, err := w.CreatePart(createMultipartHeader(fieldName, fileName, http.DetectContentType(cbuf))) | |||
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 { | |||
return bufPool.Get().(*bytes.Buffer) | |||
} | |||
func releaseBuffer(buf *bytes.Buffer) { | |||
if buf != nil { | |||
buf.Reset() | |||
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) { | |||
var v string | |||
if k == "Cookie" { | |||
cv := strings.TrimSpace(strings.Join(hdrs[k], ", ")) | |||
if c.GetClient().Jar != nil { | |||
for _, c := range c.GetClient().Jar.Cookies(r.RawRequest.URL) { | |||
if cv != "" { | |||
cv = cv + "; " + c.String() | |||
} else { | |||
cv = c.String() | |||
} | |||
} | |||
} | |||
v = strings.TrimSpace(fmt.Sprintf("%25s: %s", k, cv)) | |||
} else { | |||
v = strings.TrimSpace(fmt.Sprintf("%25s: %s", k, strings.Join(hdrs[k], ", "))) | |||
} | |||
if v != "" { | |||
str = append(str, "\t"+v) | |||
} | |||
} | |||
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 | |||
} | |||
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 | |||
} |