| @@ -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 | |||
| } | |||