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