├── go.mod ├── .travis.yml ├── default_client_test.go ├── example └── main.go ├── LICENSE ├── CHANGELOG.md ├── default_client.go ├── error.go ├── util.go ├── README.md ├── httpclient_test.go └── httpclient.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/ddliu/go-httpclient 2 | 3 | go 1.13 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.7 5 | - tip 6 | before_install: 7 | - go install github.com/mattn/goveralls@latest 8 | script: 9 | - $GOPATH/bin/goveralls -service=travis-ci -------------------------------------------------------------------------------- /default_client_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014-2019 Liu Dong . 2 | // Licensed under the MIT license. 3 | 4 | package httpclient 5 | 6 | import ( 7 | "testing" 8 | ) 9 | 10 | func TestDefaultClient(t *testing.T) { 11 | res, err := Get("http://httpbin.org/get") 12 | 13 | if err != nil { 14 | t.Error("get failed", err) 15 | } 16 | 17 | if res.StatusCode != 200 { 18 | t.Error("Status Code not 200") 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014-2019 Liu Dong . 2 | // Licensed under the MIT license. 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "github.com/ddliu/go-httpclient" 9 | "net/http" 10 | ) 11 | 12 | const ( 13 | USERAGENT = "my awsome httpclient" 14 | TIMEOUT = 30 15 | CONNECT_TIMEOUT = 5 16 | SERVER = "https://github.com" 17 | ) 18 | 19 | func main() { 20 | httpclient.Defaults(httpclient.Map{ 21 | "opt_useragent": USERAGENT, 22 | "opt_timeout": TIMEOUT, 23 | "Accept-Encoding": "gzip,deflate,sdch", 24 | }) 25 | 26 | res, _ := httpclient. 27 | WithHeader("Accept-Language", "en-us"). 28 | WithCookie(&http.Cookie{ 29 | Name: "name", 30 | Value: "github", 31 | }). 32 | WithHeader("Referer", "http://google.com"). 33 | Get(SERVER) 34 | 35 | fmt.Println("Cookies:") 36 | for k, v := range httpclient.CookieValues(SERVER) { 37 | fmt.Println(k, ":", v) 38 | } 39 | 40 | fmt.Println("Response:") 41 | fmt.Println(res.ToString()) 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | MIT License 3 | =========== 4 | 5 | Copyright (c) 2014-2018 Liu Dong 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a 8 | copy of this software and associated documentation files (the "Software"), 9 | to deal in the Software without restriction, including without limitation 10 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | and/or sell copies of the Software, and to permit persons to whom the 12 | Software is furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 20 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Changelog 2 | 3 | ### v0.6.4 (2020-01-03) 4 | 5 | Support context(cancel) 6 | 7 | ### v0.6.2 (2019-03-08) 8 | 9 | Fix concurrency issue. 10 | 11 | ### v0.6.0 (2018-02-23) 12 | 13 | More request method support. 14 | 15 | Json request support. 16 | 17 | ### v0.5.1 (2015-12-31) 18 | 19 | Add `OPT_DEBUG` option. 20 | 21 | ### v0.5.0 (2015-01-07) 22 | 23 | Remove constructor params, add `Defaults` method to set default options. 24 | 25 | Make default client as primary to keep things simple. 26 | 27 | ### v0.4.1 (2015-01-07) 28 | 29 | Add default client for convience. 30 | 31 | ### v0.4.0 (2014-09-29) 32 | 33 | Fix gzip. 34 | 35 | Improve constructor: support both default options and headers. 36 | 37 | ### v0.3.3 (2014-05-25) 38 | 39 | Pass through useragent during redirects. 40 | 41 | Support error checking. 42 | 43 | ### v0.3.2 (2014-05-21) 44 | 45 | Fix cookie, add cookie retrieving methods 46 | 47 | ### v0.3.1 (2014-05-20) 48 | 49 | Add shortcut for response 50 | 51 | ### v0.3.0 (2014-05-20) 52 | 53 | API improvements 54 | 55 | Cookie support 56 | 57 | Concurrent safe 58 | 59 | ### v0.2.1 (2014-05-18) 60 | 61 | Make `http.Client` reusable 62 | 63 | ### v0.2.0 (2014-02-17) 64 | 65 | Rewrite API, make it simple 66 | 67 | ### v0.1.0 (2014-02-14) 68 | 69 | Initial release -------------------------------------------------------------------------------- /default_client.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014-2019 Liu Dong . 2 | // Licensed under the MIT license. 3 | 4 | // Powerful and easy to use http client 5 | package httpclient 6 | 7 | import "sync" 8 | 9 | // The default client for convenience 10 | var defaultClient = &HttpClient{ 11 | reuseTransport: true, 12 | reuseJar: true, 13 | lock: new(sync.Mutex), 14 | } 15 | 16 | var Defaults = defaultClient.Defaults 17 | var Begin = defaultClient.Begin 18 | var Do = defaultClient.Do 19 | var Get = defaultClient.Get 20 | var Delete = defaultClient.Delete 21 | var Head = defaultClient.Head 22 | var Post = defaultClient.Post 23 | var PostJson = defaultClient.PostJson 24 | var PostMultipart = defaultClient.PostMultipart 25 | var Put = defaultClient.Put 26 | var PutJson = defaultClient.PutJson 27 | var PatchJson = defaultClient.PatchJson 28 | var Options = defaultClient.Options 29 | var Connect = defaultClient.Connect 30 | var Trace = defaultClient.Trace 31 | var Patch = defaultClient.Patch 32 | var WithOption = defaultClient.WithOption 33 | var WithOptions = defaultClient.WithOptions 34 | var WithHeader = defaultClient.WithHeader 35 | var WithHeaders = defaultClient.WithHeaders 36 | var WithCookie = defaultClient.WithCookie 37 | var Cookies = defaultClient.Cookies 38 | var CookieValues = defaultClient.CookieValues 39 | var CookieValue = defaultClient.CookieValue 40 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014-2019 Liu Dong . 2 | // Licensed under the MIT license. 3 | 4 | package httpclient 5 | 6 | import ( 7 | "fmt" 8 | "net" 9 | "strings" 10 | ) 11 | 12 | // Package errors 13 | const ( 14 | _ = iota 15 | ERR_DEFAULT 16 | ERR_TIMEOUT 17 | ERR_REDIRECT_POLICY 18 | ) 19 | 20 | // Custom error 21 | type Error struct { 22 | Code int 23 | Message string 24 | } 25 | 26 | // Implement the error interface 27 | func (this Error) Error() string { 28 | return fmt.Sprintf("httpclient #%d: %s", this.Code, this.Message) 29 | } 30 | 31 | func getErrorCode(err error) int { 32 | if err == nil { 33 | return 0 34 | } 35 | 36 | if e, ok := err.(*Error); ok { 37 | return e.Code 38 | } 39 | 40 | return ERR_DEFAULT 41 | } 42 | 43 | // Check a timeout error. 44 | func IsTimeoutError(err error) bool { 45 | if err == nil { 46 | return false 47 | } 48 | 49 | // TODO: does not work? 50 | if e, ok := err.(net.Error); ok && e.Timeout() { 51 | return true 52 | } 53 | 54 | // TODO: make it reliable 55 | if strings.Contains(strings.ToLower(err.Error()), "timeout") { 56 | return true 57 | } 58 | 59 | return false 60 | } 61 | 62 | // Check a redirect error 63 | func IsRedirectError(err error) bool { 64 | if err == nil { 65 | return false 66 | } 67 | 68 | // TODO: does not work? 69 | if getErrorCode(err) == ERR_REDIRECT_POLICY { 70 | return true 71 | } 72 | 73 | // TODO: make it reliable 74 | if strings.Contains(err.Error(), "redirect") { 75 | return true 76 | } 77 | 78 | return false 79 | } 80 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014-2019 Liu Dong . 2 | // Licensed under the MIT license. 3 | 4 | package httpclient 5 | 6 | import ( 7 | "bytes" 8 | "io" 9 | "mime/multipart" 10 | "net/url" 11 | "os" 12 | "path/filepath" 13 | "strings" 14 | ) 15 | 16 | // Add params to a url string. 17 | func addParams(url_ string, params url.Values) string { 18 | if len(params) == 0 { 19 | return url_ 20 | } 21 | 22 | if !strings.Contains(url_, "?") { 23 | url_ += "?" 24 | } 25 | 26 | if strings.HasSuffix(url_, "?") || strings.HasSuffix(url_, "&") { 27 | url_ += params.Encode() 28 | } else { 29 | url_ += "&" + params.Encode() 30 | } 31 | 32 | return url_ 33 | } 34 | 35 | // Add a file to a multipart writer. 36 | func addFormFile(writer *multipart.Writer, name, path string) error { 37 | file, err := os.Open(path) 38 | if err != nil { 39 | return err 40 | } 41 | defer file.Close() 42 | part, err := writer.CreateFormFile(name, filepath.Base(path)) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | _, err = io.Copy(part, file) 48 | 49 | return err 50 | } 51 | 52 | // Convert options with string keys to desired format. 53 | func Option(o map[string]interface{}) map[int]interface{} { 54 | rst := make(map[int]interface{}) 55 | for k, v := range o { 56 | k := "OPT_" + strings.ToUpper(k) 57 | if num, ok := CONST[k]; ok { 58 | rst[num] = v 59 | } 60 | } 61 | 62 | return rst 63 | } 64 | 65 | // Merge options(latter ones have higher priority) 66 | func mergeOptions(options ...map[int]interface{}) map[int]interface{} { 67 | rst := make(map[int]interface{}) 68 | 69 | for _, m := range options { 70 | for k, v := range m { 71 | rst[k] = v 72 | } 73 | } 74 | 75 | return rst 76 | } 77 | 78 | // Merge headers(latter ones have higher priority) 79 | func mergeHeaders(headers ...map[string]string) map[string]string { 80 | rst := make(map[string]string) 81 | 82 | for _, m := range headers { 83 | for k, v := range m { 84 | rst[k] = v 85 | } 86 | } 87 | 88 | return rst 89 | } 90 | 91 | // Does the params contain a file? 92 | func checkParamFile(params url.Values) bool { 93 | for k, _ := range params { 94 | if k[0] == '@' { 95 | return true 96 | } 97 | } 98 | 99 | return false 100 | } 101 | 102 | // Is opt in options? 103 | func hasOption(opt int, options []int) bool { 104 | for _, v := range options { 105 | if opt == v { 106 | return true 107 | } 108 | } 109 | 110 | return false 111 | } 112 | 113 | // Map is a mixed structure with options and headers 114 | type Map map[interface{}]interface{} 115 | 116 | // Parse the Map, return options and headers 117 | func parseMap(m Map) (map[int]interface{}, map[string]string) { 118 | var options = make(map[int]interface{}) 119 | var headers = make(map[string]string) 120 | 121 | if m == nil { 122 | return options, headers 123 | } 124 | 125 | for k, v := range m { 126 | // integer is option 127 | if kInt, ok := k.(int); ok { 128 | // don't need to validate 129 | options[kInt] = v 130 | } else if kString, ok := k.(string); ok { 131 | kStringUpper := strings.ToUpper(kString) 132 | if kInt, ok := CONST[kStringUpper]; ok { 133 | options[kInt] = v 134 | } else { 135 | // it should be header, but we still need to validate it's type 136 | if vString, ok := v.(string); ok { 137 | headers[kString] = vString 138 | } 139 | } 140 | } 141 | } 142 | 143 | return options, headers 144 | } 145 | 146 | func toUrlValues(v interface{}) url.Values { 147 | switch t := v.(type) { 148 | case url.Values: 149 | return t 150 | case map[string][]string: 151 | return url.Values(t) 152 | case map[string]string: 153 | rst := make(url.Values) 154 | for k, v := range t { 155 | rst.Add(k, v) 156 | } 157 | return rst 158 | case nil: 159 | return make(url.Values) 160 | default: 161 | panic("Invalid value") 162 | } 163 | } 164 | 165 | func checkParamsType(v interface{}) int { 166 | switch v.(type) { 167 | case url.Values, map[string][]string, map[string]string: 168 | return 1 169 | case []byte, string, *bytes.Reader: 170 | return 2 171 | case nil: 172 | return 0 173 | default: 174 | return 3 175 | } 176 | } 177 | 178 | func toReader(v interface{}) *bytes.Reader { 179 | switch t := v.(type) { 180 | case []byte: 181 | return bytes.NewReader(t) 182 | case string: 183 | return bytes.NewReader([]byte(t)) 184 | case *bytes.Reader: 185 | return t 186 | case nil: 187 | return bytes.NewReader(nil) 188 | default: 189 | panic("Invalid value") 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-httpclient 2 | 3 | [![Travis](https://img.shields.io/travis/ddliu/go-httpclient.svg?style=flat-square)](https://travis-ci.org/ddliu/go-httpclient) 4 | [![godoc](https://img.shields.io/badge/godoc-reference-blue.svg?style=flat-square)](https://godoc.org/github.com/ddliu/go-httpclient) 5 | [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE) 6 | [![Go Report Card](https://goreportcard.com/badge/github.com/ddliu/go-httpclient)](https://goreportcard.com/report/github.com/ddliu/go-httpclient) 7 | [![Coverage Status](https://coveralls.io/repos/github/ddliu/go-httpclient/badge.svg?branch=master)](https://coveralls.io/github/ddliu/go-httpclient?branch=master) 8 | 9 | Advanced HTTP client for golang. 10 | 11 | ## Features 12 | 13 | - Chainable API 14 | - Direct file upload 15 | - Timeout 16 | - HTTP Proxy 17 | - Cookie 18 | - GZIP 19 | - Redirect Policy 20 | - Cancel(with context) 21 | 22 | ## Installation 23 | 24 | ```bash 25 | go get github.com/ddliu/go-httpclient 26 | ``` 27 | 28 | ## Quick Start 29 | 30 | ```go 31 | package main 32 | 33 | import ( 34 | "github.com/ddliu/go-httpclient" 35 | ) 36 | 37 | func main() { 38 | httpclient.Defaults(httpclient.Map { 39 | httpclient.OPT_USERAGENT: "my awsome httpclient", 40 | "Accept-Language": "en-us", 41 | }) 42 | 43 | res, err := httpclient.Get("http://google.com/search", map[string]string{ 44 | "q": "news", 45 | }) 46 | 47 | println(res.StatusCode, err) 48 | } 49 | ``` 50 | 51 | ## Usage 52 | 53 | ### Setup 54 | 55 | Use `httpclient.Defaults` to setup default behaviors of the HTTP client. 56 | 57 | ```go 58 | httpclient.Defaults(httpclient.Map { 59 | httpclient.OPT_USERAGENT: "my awsome httpclient", 60 | "Accept-Language": "en-us", 61 | }) 62 | ``` 63 | 64 | The `OPT_XXX` options define basic behaviours of this client, other values are 65 | default request headers of this request. They are shared between different HTTP 66 | requests. 67 | 68 | 69 | ### Sending Request 70 | 71 | ```go 72 | // get 73 | httpclient.Get("http://httpbin.org/get", map[string]string{ 74 | "q": "news", 75 | }) 76 | 77 | // get with url.Values 78 | httpclient.Get("http://httpbin.org/get", url.Values{ 79 | "q": []string{"news", "today"} 80 | }) 81 | 82 | // post 83 | httpclient.Post("http://httpbin.org/post", map[string]string { 84 | "name": "value" 85 | }) 86 | 87 | // post file(multipart) 88 | httpclient.Post("http://httpbin.org/multipart", map[string]string { 89 | "@file": "/tmp/hello.pdf", 90 | }) 91 | 92 | // put json 93 | httpclient.PutJson("http://httpbin.org/put", 94 | `{ 95 | "name": "hello", 96 | }`) 97 | 98 | // delete 99 | httpclient.Delete("http://httpbin.org/delete") 100 | 101 | // options 102 | httpclient.Options("http://httpbin.org") 103 | 104 | // head 105 | httpclient.Head("http://httpbin.org/get") 106 | ``` 107 | 108 | ### Customize Request 109 | 110 | Before you start a new HTTP request with `Get` or `Post` method, you can specify 111 | temporary options, headers or cookies for current request. 112 | 113 | ```go 114 | httpclient. 115 | WithHeader("User-Agent", "Super Robot"). 116 | WithHeader("custom-header", "value"). 117 | WithHeaders(map[string]string { 118 | "another-header": "another-value", 119 | "and-another-header": "another-value", 120 | }). 121 | WithOption(httpclient.OPT_TIMEOUT, 60). 122 | WithCookie(&http.Cookie{ 123 | Name: "uid", 124 | Value: "123", 125 | }). 126 | Get("http://github.com") 127 | ``` 128 | 129 | ### Response 130 | 131 | The `httpclient.Response` is a thin wrap of `http.Response`. 132 | 133 | ```go 134 | // traditional 135 | res, err := httpclient.Get("http://google.com") 136 | bodyBytes, err := ioutil.ReadAll(res.Body) 137 | res.Body.Close() 138 | 139 | // ToString 140 | res, err = httpclient.Get("http://google.com") 141 | bodyString, err := res.ToString() 142 | 143 | // ReadAll 144 | res, err = httpclient.Get("http://google.com") 145 | bodyBytes, err := res.ReadAll() 146 | ``` 147 | 148 | ### Handle Cookies 149 | 150 | ```go 151 | url := "http://github.com" 152 | httpclient. 153 | WithCookie(&http.Cookie{ 154 | Name: "uid", 155 | Value: "123", 156 | }). 157 | Get(url) 158 | 159 | for _, cookie := range httpclient.Cookies() { 160 | fmt.Println(cookie.Name, cookie.Value) 161 | } 162 | 163 | for k, v := range httpclient.CookieValues() { 164 | fmt.Println(k, v) 165 | } 166 | 167 | fmt.Println(httpclient.CookieValue("uid")) 168 | ``` 169 | 170 | ### Concurrent Safe 171 | 172 | If you want to start many requests concurrently, remember to call the `Begin` 173 | method when you begin: 174 | 175 | ```go 176 | go func() { 177 | httpclient. 178 | Begin(). 179 | WithHeader("Req-A", "a"). 180 | Get("http://google.com") 181 | }() 182 | go func() { 183 | httpclient. 184 | Begin(). 185 | WithHeader("Req-B", "b"). 186 | Get("http://google.com") 187 | }() 188 | 189 | ``` 190 | 191 | ### Error Checking 192 | 193 | You can use `httpclient.IsTimeoutError` to check for timeout error: 194 | 195 | ```go 196 | res, err := httpclient.Get("http://google.com") 197 | if httpclient.IsTimeoutError(err) { 198 | // do something 199 | } 200 | ``` 201 | 202 | ### Full Example 203 | 204 | See `examples/main.go` 205 | 206 | ## Options 207 | 208 | Available options as below: 209 | 210 | - `OPT_FOLLOWLOCATION`: TRUE to follow any "Location: " header that the server sends as part of the HTTP header. Default to `true`. 211 | - `OPT_CONNECTTIMEOUT`: The number of seconds or interval (with time.Duration) to wait while trying to connect. Use 0 to wait indefinitely. 212 | - `OPT_CONNECTTIMEOUT_MS`: The number of milliseconds to wait while trying to connect. Use 0 to wait indefinitely. 213 | - `OPT_MAXREDIRS`: The maximum amount of HTTP redirections to follow. Use this option alongside `OPT_FOLLOWLOCATION`. 214 | - `OPT_PROXYTYPE`: Specify the proxy type. Valid options are `PROXY_HTTP`, `PROXY_SOCKS4`, `PROXY_SOCKS5`, `PROXY_SOCKS4A`. Only `PROXY_HTTP` is supported currently. 215 | - `OPT_TIMEOUT`: The maximum number of seconds or interval (with time.Duration) to allow httpclient functions to execute. 216 | - `OPT_TIMEOUT_MS`: The maximum number of milliseconds to allow httpclient functions to execute. 217 | - `OPT_COOKIEJAR`: Set to `true` to enable the default cookiejar, or you can set to a `http.CookieJar` instance to use a customized jar. Default to `true`. 218 | - `OPT_INTERFACE`: TODO 219 | - `OPT_PROXY`: Proxy host and port(127.0.0.1:1080). 220 | - `OPT_REFERER`: The `Referer` header of the request. 221 | - `OPT_USERAGENT`: The `User-Agent` header of the request. Default to "go-httpclient v{{VERSION}}". 222 | - `OPT_REDIRECT_POLICY`: Function to check redirect. 223 | - `OPT_PROXY_FUNC`: Function to specify proxy. 224 | - `OPT_UNSAFE_TLS`: Set to `true` to disable TLS certificate checking. 225 | - `OPT_DEBUG`: Print request info. 226 | - `OPT_CONTEXT`: Set `context.context` (can be used to cancel request). 227 | - `OPT_BEFORE_REQUEST_FUNC`: Function to call before request is sent, option should be type `func(*http.Client, *http.Request)`. 228 | 229 | ## Seperate Clients 230 | 231 | By using the `httpclient.Get`, `httpclient.Post` methods etc, you are using a 232 | default shared HTTP client. 233 | 234 | If you need more than one client in a single programme. Just create and use them 235 | seperately. 236 | 237 | ```go 238 | c1 := httpclient.NewHttpClient().Defaults(httpclient.Map { 239 | httpclient.OPT_USERAGENT: "browser1", 240 | }) 241 | 242 | c1.Get("http://google.com/") 243 | 244 | c2 := httpclient.NewHttpClient().Defaults(httpclient.Map { 245 | httpclient.OPT_USERAGENT: "browser2", 246 | }) 247 | 248 | c2.Get("http://google.com/") 249 | 250 | ``` 251 | -------------------------------------------------------------------------------- /httpclient_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014-2019 Liu Dong . 2 | // Licensed under the MIT license. 3 | 4 | // Test httpclient with httpbin(http://httpbin.org) 5 | package httpclient 6 | 7 | import ( 8 | "context" 9 | "encoding/json" 10 | "fmt" 11 | "io/ioutil" 12 | "net/http" 13 | "strings" 14 | "testing" 15 | "time" 16 | ) 17 | 18 | // common response format on httpbin.org 19 | type ResponseInfo struct { 20 | Gzipped bool `json:"gzipped"` 21 | Method string `json:"method"` 22 | Origin string `json:"origin"` 23 | Useragent string `json:"user-agent"` // http://httpbin.org/user-agent 24 | Form map[string]string `json:"form"` 25 | Files map[string]string `json:"files"` 26 | Headers map[string]string `json:"headers"` 27 | Cookies map[string]string `json:"cookies"` 28 | } 29 | 30 | func TestRequest(t *testing.T) { 31 | // get 32 | res, err := NewHttpClient(). 33 | Get("http://httpbin.org/get") 34 | 35 | if err != nil { 36 | t.Error("get failed", err) 37 | } 38 | 39 | if res.StatusCode != 200 { 40 | t.Error("Status Code not 200") 41 | } 42 | 43 | // post 44 | res, err = NewHttpClient(). 45 | Post("http://httpbin.org/post", map[string]string{ 46 | "username": "dong", 47 | "password": "******", 48 | }) 49 | 50 | if err != nil { 51 | t.Error("post failed", err) 52 | } 53 | 54 | if res.StatusCode != 200 { 55 | t.Error("Status Code not 200") 56 | } 57 | 58 | body, err := res.ReadAll() 59 | 60 | if err != nil { 61 | t.Error(err) 62 | } 63 | 64 | var info ResponseInfo 65 | 66 | err = json.Unmarshal(body, &info) 67 | 68 | if err != nil { 69 | t.Error(err) 70 | } 71 | 72 | if username, ok := info.Form["username"]; !ok || username != "dong" { 73 | t.Error("form data is not set properly") 74 | } 75 | 76 | // post, multipart 77 | res, err = NewHttpClient(). 78 | Post("http://httpbin.org/post", map[string]string{ 79 | "message": "Hello world!", 80 | "@image": "README.md", 81 | }) 82 | 83 | if err != nil { 84 | t.Error(err) 85 | } 86 | 87 | if res.StatusCode != 200 { 88 | t.Error("Status Code is not 200") 89 | } 90 | 91 | body, err = res.ReadAll() 92 | 93 | if err != nil { 94 | t.Error(err) 95 | } 96 | 97 | err = json.Unmarshal(body, &info) 98 | 99 | if err != nil { 100 | t.Error(err) 101 | } 102 | 103 | image, ok := info.Files["image"] 104 | if !ok { 105 | t.Error("file not uploaded") 106 | } 107 | 108 | imageContent, err := ioutil.ReadFile("README.md") 109 | if err != nil { 110 | t.Error(err) 111 | } 112 | 113 | if string(imageContent) != image { 114 | t.Error("file is not uploaded properly") 115 | } 116 | } 117 | 118 | func TestResponse(t *testing.T) { 119 | c := NewHttpClient() 120 | res, err := c. 121 | Get("http://httpbin.org/user-agent") 122 | 123 | if err != nil { 124 | t.Error(err) 125 | } 126 | 127 | // read with ioutil 128 | defer res.Body.Close() 129 | body1, err := ioutil.ReadAll(res.Body) 130 | 131 | if err != nil { 132 | t.Error(err) 133 | } 134 | 135 | res, err = c. 136 | Get("http://httpbin.org/user-agent") 137 | 138 | if err != nil { 139 | t.Error(err) 140 | } 141 | 142 | body2, err := res.ReadAll() 143 | 144 | res, err = c. 145 | Get("http://httpbin.org/user-agent") 146 | 147 | if err != nil { 148 | t.Error(err) 149 | } 150 | 151 | body3, err := res.ToString() 152 | 153 | if err != nil { 154 | t.Error(err) 155 | } 156 | if string(body1) != string(body2) || string(body1) != body3 { 157 | t.Error("Error response body") 158 | } 159 | } 160 | 161 | func TestHead(t *testing.T) { 162 | c := NewHttpClient() 163 | res, err := c.Head("http://httpbin.org/get") 164 | if err != nil { 165 | t.Error(err) 166 | } 167 | 168 | if res.StatusCode != 200 { 169 | t.Error("Status code is not 200") 170 | } 171 | 172 | if body, err := res.ToString(); err != nil || body != "" { 173 | t.Error("HEAD should not get body") 174 | } 175 | 176 | } 177 | 178 | func TestDelete(t *testing.T) { 179 | c := NewHttpClient() 180 | res, err := c.Delete("http://httpbin.org/delete") 181 | if err != nil { 182 | t.Error(err) 183 | } 184 | 185 | if res.StatusCode != 200 { 186 | t.Error("Status code is not 200") 187 | } 188 | } 189 | 190 | func TestOptions(t *testing.T) { 191 | c := NewHttpClient() 192 | res, err := c.Options("http://httpbin.org") 193 | if err != nil { 194 | t.Error(err) 195 | } 196 | 197 | if res.StatusCode != 200 { 198 | t.Errorf("Status code is not 200: %d", res.StatusCode) 199 | } 200 | } 201 | 202 | func TestPatch(t *testing.T) { 203 | c := NewHttpClient() 204 | res, err := c.Patch("http://httpbin.org/patch") 205 | if err != nil { 206 | t.Error(err) 207 | } 208 | 209 | if res.StatusCode != 200 { 210 | t.Errorf("Status code is not 200: %d", res.StatusCode) 211 | } 212 | } 213 | 214 | func TestPostJson(t *testing.T) { 215 | c := NewHttpClient() 216 | type jsonDataType struct { 217 | Name string 218 | } 219 | 220 | jsonData := jsonDataType{ 221 | Name: "httpclient", 222 | } 223 | 224 | res, err := c.PostJson("http://httpbin.org/post", jsonData) 225 | if err != nil { 226 | t.Error(err) 227 | } 228 | 229 | if res.StatusCode != 200 { 230 | t.Error("Status code is not 200") 231 | } 232 | } 233 | 234 | func TestPostText(t *testing.T) { 235 | c := NewHttpClient() 236 | 237 | res, err := c.Post("http://httpbin.org/post", "hello") 238 | if err != nil { 239 | t.Error(err) 240 | } 241 | 242 | if res.StatusCode != 200 { 243 | t.Error("Status code is not 200") 244 | } 245 | } 246 | 247 | func TestPutJson(t *testing.T) { 248 | c := NewHttpClient() 249 | type jsonDataType struct { 250 | Name string 251 | } 252 | 253 | jsonData := jsonDataType{ 254 | Name: "httpclient", 255 | } 256 | 257 | res, err := c.PutJson("http://httpbin.org/put", jsonData) 258 | if err != nil { 259 | t.Error(err) 260 | } 261 | 262 | if res.StatusCode != 200 { 263 | t.Error("Status code is not 200") 264 | } 265 | } 266 | 267 | func TestPatchJson(t *testing.T) { 268 | c := NewHttpClient() 269 | type jsonDataType struct { 270 | Name string 271 | } 272 | 273 | jsonData := jsonDataType{ 274 | Name: "httpclient", 275 | } 276 | 277 | res, err := c.PatchJson("http://httpbin.org/patch", jsonData) 278 | if err != nil { 279 | t.Error(err) 280 | } 281 | 282 | if res.StatusCode != 200 { 283 | t.Error("Status code is not 200") 284 | } 285 | } 286 | 287 | func TestHeaders(t *testing.T) { 288 | // set referer in options 289 | res, err := NewHttpClient(). 290 | WithHeader("header1", "value1"). 291 | WithOption(OPT_REFERER, "http://google.com"). 292 | Get("http://httpbin.org/get") 293 | 294 | if err != nil { 295 | t.Error(err) 296 | } 297 | 298 | var info ResponseInfo 299 | 300 | body, err := res.ReadAll() 301 | 302 | if err != nil { 303 | t.Error(err) 304 | } 305 | 306 | err = json.Unmarshal(body, &info) 307 | 308 | if err != nil { 309 | t.Error(err) 310 | } 311 | 312 | referer, ok := info.Headers["Referer"] 313 | if !ok || referer != "http://google.com" { 314 | t.Error("referer is not set properly") 315 | } 316 | 317 | useragent, ok := info.Headers["User-Agent"] 318 | if !ok || useragent != USERAGENT { 319 | t.Error("useragent is not set properly") 320 | } 321 | 322 | value, ok := info.Headers["Header1"] 323 | if !ok || value != "value1" { 324 | t.Error("custom header is not set properly") 325 | } 326 | } 327 | 328 | func _TestProxy(t *testing.T) { 329 | proxy := "127.0.0.1:1080" 330 | 331 | res, err := NewHttpClient(). 332 | WithOption(OPT_PROXY, proxy). 333 | Get("http://httpbin.org/get") 334 | 335 | if err != nil { 336 | t.Error(err) 337 | } 338 | 339 | if res.StatusCode != 200 { 340 | t.Error("StatusCode is not 200") 341 | } 342 | 343 | res, err = NewHttpClient(). 344 | WithOption(OPT_PROXY_FUNC, func(*http.Request) (int, string, error) { 345 | return PROXY_HTTP, proxy, nil 346 | }). 347 | Get("http://httpbin.org/get") 348 | 349 | if err != nil { 350 | t.Error(err) 351 | } 352 | 353 | if res.StatusCode != 200 { 354 | t.Error("StatusCode is not 200") 355 | } 356 | } 357 | 358 | func TestTimeout(t *testing.T) { 359 | // connect timeout 360 | res, err := NewHttpClient(). 361 | WithOption(OPT_CONNECTTIMEOUT_MS, 1). 362 | Get("http://httpbin.org/get") 363 | 364 | if err == nil { 365 | t.Error("OPT_CONNECTTIMEOUT_MS does not work") 366 | } 367 | 368 | if !IsTimeoutError(err) { 369 | t.Error("Maybe it's not a timeout error?", err) 370 | } 371 | 372 | res, err = NewHttpClient(). 373 | WithOption(OPT_CONNECTTIMEOUT, time.Millisecond). 374 | Get("http://httpbin.org/get") 375 | 376 | if err == nil { 377 | t.Error("OPT_CONNECTTIMEOUT (time.Duration) does not work") 378 | } 379 | 380 | if !IsTimeoutError(err) { 381 | t.Error("Maybe it's not a timeout error?", err) 382 | } 383 | 384 | // timeout 385 | res, err = NewHttpClient(). 386 | WithOption(OPT_TIMEOUT, 3). 387 | Get("http://httpbin.org/delay/3") 388 | 389 | if err == nil { 390 | t.Error("OPT_TIMEOUT does not work") 391 | } 392 | 393 | if !IsTimeoutError(err) { 394 | t.Error("Maybe it's not a timeout error?", err) 395 | } 396 | 397 | res, err = NewHttpClient(). 398 | WithOption(OPT_TIMEOUT, 3*time.Second). 399 | Get("http://httpbin.org/delay/3") 400 | 401 | if err == nil { 402 | t.Error("OPT_TIMEOUT (time.Duration) does not work") 403 | } 404 | 405 | if !IsTimeoutError(err) { 406 | t.Error("Maybe it's not a timeout error?", err) 407 | } 408 | 409 | // no timeout 410 | res, err = NewHttpClient(). 411 | WithOption(OPT_TIMEOUT, 100). 412 | Get("http://httpbin.org/delay/3") 413 | 414 | if err != nil { 415 | t.Error("OPT_TIMEOUT does not work properly") 416 | } 417 | 418 | if res.StatusCode != 200 { 419 | t.Error("StatusCode is not 200") 420 | } 421 | } 422 | 423 | // Disabled because of the redirection issue of httpbin: https://github.com/postmanlabs/httpbin/issues/617 424 | func _TestRedirect(t *testing.T) { 425 | c := NewHttpClient().Defaults(Map{ 426 | OPT_USERAGENT: "test redirect", 427 | }) 428 | // follow locatioin 429 | res, err := c. 430 | WithOptions(Map{ 431 | OPT_FOLLOWLOCATION: true, 432 | OPT_MAXREDIRS: 10, 433 | }). 434 | Get("http://httpbin.org/redirect/3") 435 | 436 | if err != nil { 437 | t.Error(err) 438 | } 439 | 440 | if res.StatusCode != 200 || res.Request.URL.String() != "http://httpbin.org/get" { 441 | t.Error("Redirect failed") 442 | } 443 | 444 | // should keep useragent 445 | var info ResponseInfo 446 | 447 | body, err := res.ReadAll() 448 | 449 | if err != nil { 450 | t.Error(err) 451 | } 452 | 453 | err = json.Unmarshal(body, &info) 454 | 455 | if err != nil { 456 | t.Error(err) 457 | } 458 | 459 | if useragent, ok := info.Headers["User-Agent"]; !ok || useragent != "test redirect" { 460 | t.Error("Useragent is not passed through") 461 | } 462 | 463 | // no follow 464 | res, err = c. 465 | WithOption(OPT_FOLLOWLOCATION, false). 466 | Get("http://httpbin.org/relative-redirect/3") 467 | 468 | if err == nil { 469 | t.Error("Must not follow location") 470 | } 471 | 472 | if !strings.Contains(err.Error(), "redirect not allowed") { 473 | t.Error(err) 474 | } 475 | 476 | if res.StatusCode != 302 || res.Header.Get("Location") != "/relative-redirect/2" { 477 | t.Error("Redirect failed: ", res.StatusCode, res.Header.Get("Location")) 478 | } 479 | 480 | // maxredirs 481 | res, err = c. 482 | WithOption(OPT_MAXREDIRS, 2). 483 | Get("http://httpbin.org/relative-redirect/3") 484 | 485 | if err == nil { 486 | t.Error("Must not follow through") 487 | } 488 | 489 | if !IsRedirectError(err) { 490 | t.Error("Not a redirect error", err) 491 | } 492 | 493 | if !strings.Contains(err.Error(), "stopped after 2 redirects") { 494 | t.Error(err) 495 | } 496 | 497 | if res.StatusCode != 302 || res.Header.Get("Location") != "/relative-redirect/1" { 498 | t.Error("OPT_MAXREDIRS does not work properly") 499 | } 500 | 501 | // custom redirect policy 502 | res, err = c. 503 | WithOption(OPT_REDIRECT_POLICY, func(req *http.Request, via []*http.Request) error { 504 | if req.URL.String() == "http://httpbin.org/relative-redirect/1" { 505 | return fmt.Errorf("should stop here") 506 | } 507 | 508 | return nil 509 | }). 510 | Get("http://httpbin.org/relative-redirect/3") 511 | 512 | if err == nil { 513 | t.Error("Must not follow through") 514 | } 515 | 516 | if !strings.Contains(err.Error(), "should stop here") { 517 | t.Error(err) 518 | } 519 | 520 | if res.StatusCode != 302 || res.Header.Get("Location") != "/relative-redirect/1" { 521 | t.Error("OPT_REDIRECT_POLICY does not work properly") 522 | } 523 | } 524 | 525 | func TestCookie(t *testing.T) { 526 | c := NewHttpClient() 527 | 528 | res, err := c. 529 | WithCookie(&http.Cookie{ 530 | Name: "username", 531 | Value: "dong", 532 | }). 533 | Get("http://httpbin.org/cookies") 534 | 535 | if err != nil { 536 | t.Error(err) 537 | } 538 | 539 | body, err := res.ReadAll() 540 | 541 | if err != nil { 542 | t.Error(err) 543 | } 544 | 545 | var info ResponseInfo 546 | 547 | err = json.Unmarshal(body, &info) 548 | 549 | if err != nil { 550 | t.Error(err) 551 | } 552 | 553 | if username, ok := info.Cookies["username"]; !ok || username != "dong" { 554 | t.Error("cookie is not set properly") 555 | } 556 | 557 | if c.CookieValue("http://httpbin.org/cookies", "username") != "dong" { 558 | t.Error("cookie is not set properly") 559 | } 560 | 561 | // get old cookie 562 | res, err = c. 563 | Get("http://httpbin.org/cookies", nil) 564 | 565 | if err != nil { 566 | t.Error(err) 567 | } 568 | 569 | body, err = res.ReadAll() 570 | 571 | if err != nil { 572 | t.Error(err) 573 | } 574 | 575 | err = json.Unmarshal(body, &info) 576 | 577 | if err != nil { 578 | t.Error(err) 579 | } 580 | 581 | if username, ok := info.Cookies["username"]; !ok || username != "dong" { 582 | t.Error("cookie lost") 583 | } 584 | 585 | if c.CookieValue("http://httpbin.org/cookies", "username") != "dong" { 586 | t.Error("cookie lost") 587 | } 588 | 589 | // update cookie 590 | res, err = c. 591 | WithCookie(&http.Cookie{ 592 | Name: "username", 593 | Value: "octcat", 594 | }). 595 | Get("http://httpbin.org/cookies") 596 | 597 | if err != nil { 598 | t.Error(err) 599 | } 600 | 601 | body, err = res.ReadAll() 602 | 603 | if err != nil { 604 | t.Error(err) 605 | } 606 | 607 | err = json.Unmarshal(body, &info) 608 | 609 | if err != nil { 610 | t.Error(err) 611 | } 612 | 613 | if username, ok := info.Cookies["username"]; !ok || username != "octcat" { 614 | t.Error("cookie update failed") 615 | } 616 | 617 | if c.CookieValue("http://httpbin.org/cookies", "username") != "octcat" { 618 | t.Error("cookie update failed") 619 | } 620 | } 621 | 622 | func TestGzip(t *testing.T) { 623 | c := NewHttpClient() 624 | res, err := c. 625 | WithHeader("Accept-Encoding", "gzip, deflate"). 626 | Get("http://httpbin.org/gzip") 627 | 628 | if err != nil { 629 | t.Error(err) 630 | } 631 | 632 | body, err := res.ReadAll() 633 | 634 | if err != nil { 635 | t.Error(err) 636 | } 637 | 638 | var info ResponseInfo 639 | 640 | err = json.Unmarshal(body, &info) 641 | 642 | if err != nil { 643 | t.Error(err) 644 | } 645 | 646 | if !info.Gzipped { 647 | t.Error("Parse gzip failed") 648 | } 649 | } 650 | 651 | func _TestCurrentUA(ch chan bool, t *testing.T, c *HttpClient, ua string) { 652 | res, err := c. 653 | Begin(). 654 | WithOption(OPT_USERAGENT, ua). 655 | Get("http://httpbin.org/headers") 656 | 657 | if err != nil { 658 | t.Error(err) 659 | } 660 | 661 | body, err := res.ReadAll() 662 | 663 | if err != nil { 664 | t.Error(err) 665 | } 666 | 667 | var info ResponseInfo 668 | err = json.Unmarshal(body, &info) 669 | 670 | if err != nil { 671 | t.Error(err) 672 | } 673 | 674 | if resUA, ok := info.Headers["User-Agent"]; !ok || resUA != ua { 675 | t.Error("TestCurrentUA failed") 676 | } 677 | 678 | ch <- true 679 | } 680 | 681 | func TestConcurrent(t *testing.T) { 682 | total := 100 683 | chs := make([]chan bool, total) 684 | c := NewHttpClient() 685 | for i := 0; i < total; i++ { 686 | chs[i] = make(chan bool) 687 | go _TestCurrentUA(chs[i], t, c, fmt.Sprint("go-httpclient UA-", i)) 688 | } 689 | 690 | for _, ch := range chs { 691 | <-ch 692 | } 693 | } 694 | 695 | func TestIssue10(t *testing.T) { 696 | var testString = "gpThzrynEC1MdenWgAILwvL2CYuNGO9RwtbH1NZJ1GE31ywFOCY%2BLCctUl86jBi8TccpdPI5ppZ%2Bgss%2BNjqGHg==" 697 | c := NewHttpClient() 698 | res, err := c.Post("http://httpbin.org/post", map[string]string{ 699 | "a": "a", 700 | "b": "b", 701 | "c": testString, 702 | "d": "d", 703 | }) 704 | 705 | if err != nil { 706 | t.Error(err) 707 | } 708 | 709 | body, err := res.ReadAll() 710 | 711 | if err != nil { 712 | t.Error(err) 713 | } 714 | 715 | var info ResponseInfo 716 | 717 | err = json.Unmarshal(body, &info) 718 | 719 | if err != nil { 720 | t.Error(err) 721 | } 722 | 723 | if info.Form["c"] != testString { 724 | t.Error("error") 725 | } 726 | } 727 | 728 | func TestOptDebug(t *testing.T) { 729 | c := NewHttpClient() 730 | c. 731 | WithOption(OPT_DEBUG, true). 732 | Get("http://httpbin.org/get") 733 | } 734 | 735 | func TestUnsafeTLS(t *testing.T) { 736 | unsafeUrl := "https://expired.badssl.com/" 737 | c := NewHttpClient() 738 | _, err := c. 739 | Get(unsafeUrl, nil) 740 | if err == nil { 741 | t.Error("Unexcepted unsafe url:" + unsafeUrl) 742 | } 743 | 744 | res, err := c. 745 | WithOption(OPT_UNSAFE_TLS, true). 746 | Get(unsafeUrl) 747 | if err != nil { 748 | t.Error(err) 749 | } 750 | 751 | if res.StatusCode != 200 { 752 | t.Error("OPT_UNSAFE_TLS error") 753 | } 754 | } 755 | 756 | func TestPutJsonWithCharset(t *testing.T) { 757 | c := NewHttpClient() 758 | type jsonDataType struct { 759 | Name string 760 | } 761 | 762 | jsonData := jsonDataType{ 763 | Name: "httpclient", 764 | } 765 | 766 | contentType := "application/json; charset=utf-8" 767 | res, err := c. 768 | WithHeader("Content-Type", contentType). 769 | PutJson("http://httpbin.org/put", jsonData) 770 | if err != nil { 771 | t.Error(err) 772 | } 773 | 774 | body, err := res.ReadAll() 775 | 776 | if err != nil { 777 | t.Error(err) 778 | } 779 | 780 | var info ResponseInfo 781 | 782 | err = json.Unmarshal(body, &info) 783 | 784 | if err != nil { 785 | t.Error(err) 786 | } 787 | 788 | if info.Headers["Content-Type"] != contentType { 789 | t.Error("Setting charset not working: " + info.Headers["Content-Type"]) 790 | } 791 | } 792 | 793 | func TestCancel(t *testing.T) { 794 | ctx, cancel := context.WithCancel(context.Background()) 795 | c := NewHttpClient() 796 | 797 | ch := make(chan error) 798 | go func() { 799 | _, err := c.Begin(). 800 | WithOption(OPT_CONTEXT, ctx). 801 | Get("http://httpbin.org/delay/3") 802 | ch <- err 803 | }() 804 | 805 | time.Sleep(1 * time.Second) 806 | cancel() 807 | 808 | err := <-ch 809 | 810 | if err == nil || !strings.Contains(err.Error(), "cancel") { 811 | t.Error("Cancel error") 812 | } 813 | } 814 | 815 | func TestIssue41(t *testing.T) { 816 | c := NewHttpClient() 817 | c.Begin().Get("http://httpbin.org") 818 | c.Get("http://httpbin.org") 819 | } 820 | 821 | func TestBeforeRequestFunc(t *testing.T) { 822 | c := NewHttpClient() 823 | res, err := c.Begin().WithOption(OPT_BEFORE_REQUEST_FUNC, func(c *http.Client, r *http.Request) { 824 | r.Header.Add("test", "test") 825 | }).Get("http://httpbin.org/get") 826 | 827 | if err != nil { 828 | t.Error(err) 829 | } 830 | 831 | var info ResponseInfo 832 | 833 | body, err := res.ReadAll() 834 | 835 | if err != nil { 836 | t.Error(err) 837 | } 838 | 839 | err = json.Unmarshal(body, &info) 840 | 841 | if info.Headers["Test"] != "test" { 842 | t.Error("header not added") 843 | } 844 | } 845 | 846 | // #50 deadlock 847 | func TestIssue50(t *testing.T) { 848 | c := NewHttpClient() 849 | 850 | type Node struct { 851 | Name string `json:"name"` 852 | Next *Node `json:"next"` 853 | } 854 | n1 := Node{Name: "1", Next: nil} 855 | n2 := Node{Name: "2", Next: &n1} 856 | n1.Next = &n2 857 | // send an object that can't be marshalled. 858 | _, err := c.Begin().PostJson("http://httpbin.org/post", n2) 859 | if err == nil { 860 | t.Fatal("json marshal unexcepted") 861 | } 862 | // block here as Begin() can not require the lock. 863 | _, err = c.Begin().Get("http://httpbin.org/get") 864 | if err != nil { 865 | t.Fatal(err) 866 | } 867 | } 868 | -------------------------------------------------------------------------------- /httpclient.go: -------------------------------------------------------------------------------- 1 | // Copyright 2014-2019 Liu Dong . 2 | // Licensed under the MIT license. 3 | 4 | // Powerful and easy to use http client 5 | package httpclient 6 | 7 | import ( 8 | "context" 9 | "fmt" 10 | 11 | "bytes" 12 | "strings" 13 | 14 | "time" 15 | 16 | "io" 17 | "io/ioutil" 18 | "sync" 19 | 20 | "net" 21 | "net/http" 22 | "net/http/cookiejar" 23 | "net/http/httputil" 24 | "net/url" 25 | 26 | "crypto/tls" 27 | 28 | "compress/gzip" 29 | 30 | "encoding/json" 31 | "mime/multipart" 32 | ) 33 | 34 | // Constants definations 35 | // CURL options, see https://github.com/bagder/curl/blob/169fedbdce93ecf14befb6e0e1ce6a2d480252a3/packages/OS400/curl.inc.in 36 | const ( 37 | VERSION = "0.7.1" 38 | USERAGENT = "go-httpclient v" + VERSION 39 | ) 40 | 41 | const ( 42 | PROXY_HTTP int = iota 43 | PROXY_SOCKS4 44 | PROXY_SOCKS5 45 | PROXY_SOCKS4A 46 | 47 | // CURL like OPT 48 | OPT_AUTOREFERER 49 | OPT_FOLLOWLOCATION 50 | OPT_CONNECTTIMEOUT 51 | OPT_CONNECTTIMEOUT_MS 52 | OPT_MAXREDIRS 53 | OPT_PROXYTYPE 54 | OPT_TIMEOUT 55 | OPT_TIMEOUT_MS 56 | OPT_COOKIEJAR 57 | OPT_INTERFACE 58 | OPT_PROXY 59 | OPT_REFERER 60 | OPT_USERAGENT 61 | 62 | // Other OPT 63 | OPT_REDIRECT_POLICY 64 | OPT_PROXY_FUNC 65 | OPT_DEBUG 66 | OPT_UNSAFE_TLS 67 | 68 | OPT_CONTEXT 69 | 70 | OPT_BEFORE_REQUEST_FUNC 71 | ) 72 | 73 | // String map of options 74 | var CONST = map[string]int{ 75 | "OPT_AUTOREFERER": OPT_AUTOREFERER, 76 | "OPT_FOLLOWLOCATION": OPT_FOLLOWLOCATION, 77 | "OPT_CONNECTTIMEOUT": OPT_CONNECTTIMEOUT, 78 | "OPT_CONNECTTIMEOUT_MS": OPT_CONNECTTIMEOUT_MS, 79 | "OPT_MAXREDIRS": OPT_MAXREDIRS, 80 | "OPT_PROXYTYPE": OPT_PROXYTYPE, 81 | "OPT_TIMEOUT": OPT_TIMEOUT, 82 | "OPT_TIMEOUT_MS": OPT_TIMEOUT_MS, 83 | "OPT_COOKIEJAR": OPT_COOKIEJAR, 84 | "OPT_INTERFACE": OPT_INTERFACE, 85 | "OPT_PROXY": OPT_PROXY, 86 | "OPT_REFERER": OPT_REFERER, 87 | "OPT_USERAGENT": OPT_USERAGENT, 88 | "OPT_REDIRECT_POLICY": OPT_REDIRECT_POLICY, 89 | "OPT_PROXY_FUNC": OPT_PROXY_FUNC, 90 | "OPT_DEBUG": OPT_DEBUG, 91 | "OPT_UNSAFE_TLS": OPT_UNSAFE_TLS, 92 | "OPT_CONTEXT": OPT_CONTEXT, 93 | "OPT_BEFORE_REQUEST_FUNC": OPT_BEFORE_REQUEST_FUNC, 94 | } 95 | 96 | // Default options for any clients. 97 | var defaultOptions = map[int]interface{}{ 98 | OPT_FOLLOWLOCATION: true, 99 | OPT_MAXREDIRS: 10, 100 | OPT_AUTOREFERER: true, 101 | OPT_USERAGENT: USERAGENT, 102 | OPT_COOKIEJAR: true, 103 | OPT_DEBUG: false, 104 | } 105 | 106 | // These options affect transport, transport may not be reused if you change any 107 | // of these options during a request. 108 | var transportOptions = []int{ 109 | OPT_CONNECTTIMEOUT, 110 | OPT_CONNECTTIMEOUT_MS, 111 | OPT_PROXYTYPE, 112 | OPT_TIMEOUT, 113 | OPT_TIMEOUT_MS, 114 | OPT_INTERFACE, 115 | OPT_PROXY, 116 | OPT_PROXY_FUNC, 117 | OPT_UNSAFE_TLS, 118 | } 119 | 120 | // These options affect cookie jar, jar may not be reused if you change any of 121 | // these options during a request. 122 | var jarOptions = []int{ 123 | OPT_COOKIEJAR, 124 | } 125 | 126 | // Thin wrapper of http.Response(can also be used as http.Response). 127 | type Response struct { 128 | *http.Response 129 | } 130 | 131 | // Read response body into a byte slice. 132 | func (this *Response) ReadAll() ([]byte, error) { 133 | var reader io.ReadCloser 134 | var err error 135 | switch this.Header.Get("Content-Encoding") { 136 | case "gzip": 137 | reader, err = gzip.NewReader(this.Body) 138 | if err != nil { 139 | return nil, err 140 | } 141 | default: 142 | reader = this.Body 143 | } 144 | 145 | defer reader.Close() 146 | return ioutil.ReadAll(reader) 147 | } 148 | 149 | // Read response body into string. 150 | func (this *Response) ToString() (string, error) { 151 | bytes, err := this.ReadAll() 152 | if err != nil { 153 | return "", err 154 | } 155 | 156 | return string(bytes), nil 157 | } 158 | 159 | // Prepare a request. 160 | func prepareRequest(method string, url_ string, headers map[string]string, 161 | body io.Reader, options map[int]interface{}) (*http.Request, error) { 162 | req, err := http.NewRequest(method, url_, body) 163 | 164 | if err != nil { 165 | return nil, err 166 | } 167 | 168 | // OPT_REFERER 169 | if referer, ok := options[OPT_REFERER]; ok { 170 | if refererStr, ok := referer.(string); ok { 171 | req.Header.Set("Referer", refererStr) 172 | } 173 | } 174 | 175 | // OPT_USERAGENT 176 | if useragent, ok := options[OPT_USERAGENT]; ok { 177 | if useragentStr, ok := useragent.(string); ok { 178 | req.Header.Set("User-Agent", useragentStr) 179 | } 180 | } 181 | 182 | for k, v := range headers { 183 | req.Header.Set(k, v) 184 | } 185 | 186 | return req, nil 187 | } 188 | 189 | func prepareTimeout(options map[int]interface{}) (time.Duration, error) { 190 | var timeout time.Duration 191 | 192 | if timeoutMS_, ok := options[OPT_TIMEOUT_MS]; ok { 193 | if timeoutMS, ok := timeoutMS_.(int); ok { 194 | timeout = time.Duration(timeoutMS) * time.Millisecond 195 | } else { 196 | return 0, fmt.Errorf("OPT_TIMEOUT_MS must be int") 197 | } 198 | } else if timeout_, ok := options[OPT_TIMEOUT]; ok { 199 | if timeout, ok = timeout_.(time.Duration); !ok { 200 | if timeoutS, ok := timeout_.(int); ok { 201 | timeout = time.Duration(timeoutS) * time.Second 202 | } else { 203 | return 0, fmt.Errorf("OPT_TIMEOUT must be int or time.Duration") 204 | } 205 | } 206 | } 207 | 208 | return timeout, nil 209 | } 210 | 211 | func prepareConnTimeout(options map[int]interface{}) (time.Duration, error) { 212 | var connectTimeout time.Duration 213 | 214 | if connectTimeoutMS_, ok := options[OPT_CONNECTTIMEOUT_MS]; ok { 215 | if connectTimeoutMS, ok := connectTimeoutMS_.(int); ok { 216 | connectTimeout = time.Duration(connectTimeoutMS) * time.Millisecond 217 | } else { 218 | return 0, fmt.Errorf("OPT_CONNECTTIMEOUT_MS must be int") 219 | } 220 | } else if connectTimeout_, ok := options[OPT_CONNECTTIMEOUT]; ok { 221 | if connectTimeout, ok = connectTimeout_.(time.Duration); !ok { 222 | if connectTimeoutS, ok := connectTimeout_.(int); ok { 223 | connectTimeout = time.Duration(connectTimeoutS) * time.Second 224 | } else { 225 | return 0, fmt.Errorf("OPT_CONNECTTIMEOUT must be int or time.Duration") 226 | } 227 | } 228 | } 229 | 230 | timeout, err := prepareTimeout(options) 231 | if err != nil { 232 | return 0, err 233 | } 234 | 235 | // fix connect timeout(important, or it might cause a long time wait during 236 | //connection) 237 | if timeout > 0 && (connectTimeout > timeout || connectTimeout == 0) { 238 | connectTimeout = timeout 239 | } 240 | 241 | return connectTimeout, nil 242 | } 243 | 244 | // Prepare a transport. 245 | // 246 | // Handles timemout, proxy and maybe other transport related options here. 247 | func prepareTransport(options map[int]interface{}) (http.RoundTripper, error) { 248 | transport := &http.Transport{} 249 | 250 | connectTimeout, err := prepareConnTimeout(options) 251 | if err != nil { 252 | return nil, err 253 | } 254 | 255 | transport.Dial = func(network, addr string) (net.Conn, error) { 256 | var conn net.Conn 257 | var err error 258 | if connectTimeout > 0 { 259 | conn, err = net.DialTimeout(network, addr, connectTimeout) 260 | if err != nil { 261 | return nil, err 262 | } 263 | } else { 264 | conn, err = net.Dial(network, addr) 265 | if err != nil { 266 | return nil, err 267 | } 268 | } 269 | 270 | return conn, nil 271 | } 272 | 273 | // proxy 274 | if proxyFunc_, ok := options[OPT_PROXY_FUNC]; ok { 275 | if proxyFunc, ok := proxyFunc_.(func(*http.Request) (int, string, error)); ok { 276 | transport.Proxy = func(req *http.Request) (*url.URL, error) { 277 | proxyType, u_, err := proxyFunc(req) 278 | if err != nil { 279 | return nil, err 280 | } 281 | 282 | if proxyType != PROXY_HTTP { 283 | return nil, fmt.Errorf("only PROXY_HTTP is currently supported") 284 | } 285 | 286 | u_ = "http://" + u_ 287 | 288 | u, err := url.Parse(u_) 289 | 290 | if err != nil { 291 | return nil, err 292 | } 293 | 294 | return u, nil 295 | } 296 | } else { 297 | return nil, fmt.Errorf("OPT_PROXY_FUNC is not a desired function") 298 | } 299 | } else { 300 | var proxytype int 301 | if proxytype_, ok := options[OPT_PROXYTYPE]; ok { 302 | if proxytype, ok = proxytype_.(int); !ok || proxytype != PROXY_HTTP { 303 | return nil, fmt.Errorf("OPT_PROXYTYPE must be int, and only PROXY_HTTP is currently supported") 304 | } 305 | } 306 | 307 | var proxy string 308 | if proxy_, ok := options[OPT_PROXY]; ok { 309 | if proxy, ok = proxy_.(string); !ok { 310 | return nil, fmt.Errorf("OPT_PROXY must be string") 311 | } 312 | 313 | if !strings.Contains(proxy, "://") { 314 | proxy = "http://" + proxy 315 | } 316 | proxyUrl, err := url.Parse(proxy) 317 | if err != nil { 318 | return nil, err 319 | } 320 | transport.Proxy = http.ProxyURL(proxyUrl) 321 | } 322 | } 323 | 324 | // TLS 325 | if unsafe_tls_, found := options[OPT_UNSAFE_TLS]; found { 326 | var unsafe_tls, _ = unsafe_tls_.(bool) 327 | var tls_config = transport.TLSClientConfig 328 | if tls_config == nil { 329 | tls_config = &tls.Config{} 330 | transport.TLSClientConfig = tls_config 331 | } 332 | tls_config.InsecureSkipVerify = unsafe_tls 333 | } 334 | 335 | return transport, nil 336 | } 337 | 338 | // Prepare a redirect policy. 339 | func prepareRedirect(options map[int]interface{}) (func(req *http.Request, via []*http.Request) error, error) { 340 | var redirectPolicy func(req *http.Request, via []*http.Request) error 341 | 342 | if redirectPolicy_, ok := options[OPT_REDIRECT_POLICY]; ok { 343 | if redirectPolicy, ok = redirectPolicy_.(func(*http.Request, []*http.Request) error); !ok { 344 | return nil, fmt.Errorf("OPT_REDIRECT_POLICY is not a desired function") 345 | } 346 | } else { 347 | var followlocation bool 348 | if followlocation_, ok := options[OPT_FOLLOWLOCATION]; ok { 349 | if followlocation, ok = followlocation_.(bool); !ok { 350 | return nil, fmt.Errorf("OPT_FOLLOWLOCATION must be bool") 351 | } 352 | } 353 | 354 | var maxredirs int 355 | if maxredirs_, ok := options[OPT_MAXREDIRS]; ok { 356 | if maxredirs, ok = maxredirs_.(int); !ok { 357 | return nil, fmt.Errorf("OPT_MAXREDIRS must be int") 358 | } 359 | } 360 | 361 | redirectPolicy = func(req *http.Request, via []*http.Request) error { 362 | // no follow 363 | if !followlocation || maxredirs <= 0 { 364 | return &Error{ 365 | Code: ERR_REDIRECT_POLICY, 366 | Message: fmt.Sprintf("redirect not allowed"), 367 | } 368 | } 369 | 370 | if len(via) >= maxredirs { 371 | return &Error{ 372 | Code: ERR_REDIRECT_POLICY, 373 | Message: fmt.Sprintf("stopped after %d redirects", len(via)), 374 | } 375 | } 376 | 377 | last := via[len(via)-1] 378 | // keep necessary headers 379 | // TODO: pass all headers or add other headers? 380 | if useragent := last.Header.Get("User-Agent"); useragent != "" { 381 | req.Header.Set("User-Agent", useragent) 382 | } 383 | 384 | return nil 385 | } 386 | } 387 | 388 | return redirectPolicy, nil 389 | } 390 | 391 | // Prepare a cookie jar. 392 | func prepareJar(options map[int]interface{}) (http.CookieJar, error) { 393 | var jar http.CookieJar 394 | var err error 395 | if optCookieJar_, ok := options[OPT_COOKIEJAR]; ok { 396 | // is bool 397 | if optCookieJar, ok := optCookieJar_.(bool); ok { 398 | // default jar 399 | if optCookieJar { 400 | // TODO: PublicSuffixList 401 | jar, err = cookiejar.New(nil) 402 | if err != nil { 403 | return nil, err 404 | } 405 | } 406 | } else if optCookieJar, ok := optCookieJar_.(http.CookieJar); ok { 407 | jar = optCookieJar 408 | } else { 409 | return nil, fmt.Errorf("invalid cookiejar") 410 | } 411 | } 412 | 413 | return jar, nil 414 | } 415 | 416 | // Create an HTTP client. 417 | func NewHttpClient() *HttpClient { 418 | c := &HttpClient{ 419 | reuseTransport: true, 420 | reuseJar: true, 421 | lock: new(sync.Mutex), 422 | } 423 | 424 | return c 425 | } 426 | 427 | // Powerful and easy to use HTTP client. 428 | type HttpClient struct { 429 | // Default options of this client. 430 | options map[int]interface{} 431 | 432 | // Default headers of this client. 433 | Headers map[string]string 434 | 435 | // Options of current request. 436 | oneTimeOptions map[int]interface{} 437 | 438 | // Headers of current request. 439 | oneTimeHeaders map[string]string 440 | 441 | // Cookies of current request. 442 | oneTimeCookies []*http.Cookie 443 | 444 | // Global transport of this client, might be shared between different 445 | // requests. 446 | transport http.RoundTripper 447 | 448 | // Global cookie jar of this client, might be shared between different 449 | // requests. 450 | jar http.CookieJar 451 | 452 | // Whether current request should reuse the transport or not. 453 | reuseTransport bool 454 | 455 | // Whether current request should reuse the cookie jar or not. 456 | reuseJar bool 457 | 458 | // Make requests of one client concurrent safe. 459 | lock *sync.Mutex 460 | 461 | withLock bool 462 | } 463 | 464 | // Set default options and headers. 465 | func (this *HttpClient) Defaults(defaults Map) *HttpClient { 466 | options, headers := parseMap(defaults) 467 | 468 | // merge options 469 | if this.options == nil { 470 | this.options = options 471 | } else { 472 | for k, v := range options { 473 | this.options[k] = v 474 | } 475 | } 476 | 477 | // merge headers 478 | if this.Headers == nil { 479 | this.Headers = headers 480 | } else { 481 | for k, v := range headers { 482 | this.Headers[k] = v 483 | } 484 | } 485 | 486 | return this 487 | } 488 | 489 | // Begin marks the begining of a request, it's necessary for concurrent 490 | // requests. 491 | func (this *HttpClient) Begin() *HttpClient { 492 | this.lock.Lock() 493 | this.withLock = true 494 | 495 | return this 496 | } 497 | 498 | // Reset the client state so that other requests can begin. 499 | func (this *HttpClient) reset() { 500 | this.oneTimeOptions = nil 501 | this.oneTimeHeaders = nil 502 | this.oneTimeCookies = nil 503 | this.reuseTransport = true 504 | this.reuseJar = true 505 | 506 | // nil means the Begin has not been called, asume requests are not 507 | // concurrent. 508 | if this.withLock { 509 | this.withLock = false 510 | this.lock.Unlock() 511 | } 512 | } 513 | 514 | // Temporarily specify an option of the current request. 515 | func (this *HttpClient) WithOption(k int, v interface{}) *HttpClient { 516 | if this.oneTimeOptions == nil { 517 | this.oneTimeOptions = make(map[int]interface{}) 518 | } 519 | this.oneTimeOptions[k] = v 520 | 521 | // Conditions we cann't reuse the transport. 522 | if hasOption(k, transportOptions) { 523 | this.reuseTransport = false 524 | } 525 | 526 | // Conditions we cann't reuse the cookie jar. 527 | if hasOption(k, jarOptions) { 528 | this.reuseJar = false 529 | } 530 | 531 | return this 532 | } 533 | 534 | // Temporarily specify multiple options of the current request. 535 | func (this *HttpClient) WithOptions(m Map) *HttpClient { 536 | options, _ := parseMap(m) 537 | for k, v := range options { 538 | this.WithOption(k, v) 539 | } 540 | 541 | return this 542 | } 543 | 544 | // Temporarily specify a header of the current request. 545 | func (this *HttpClient) WithHeader(k string, v string) *HttpClient { 546 | if this.oneTimeHeaders == nil { 547 | this.oneTimeHeaders = make(map[string]string) 548 | } 549 | this.oneTimeHeaders[k] = v 550 | 551 | return this 552 | } 553 | 554 | // Temporarily specify multiple headers of the current request. 555 | func (this *HttpClient) WithHeaders(m map[string]string) *HttpClient { 556 | for k, v := range m { 557 | this.WithHeader(k, v) 558 | } 559 | 560 | return this 561 | } 562 | 563 | // Specify cookies of the current request. 564 | func (this *HttpClient) WithCookie(cookies ...*http.Cookie) *HttpClient { 565 | this.oneTimeCookies = append(this.oneTimeCookies, cookies...) 566 | 567 | return this 568 | } 569 | 570 | // Start a request, and get the response. 571 | // 572 | // Usually we just need the Get and Post method. 573 | func (this *HttpClient) Do(method string, url string, headers map[string]string, 574 | body io.Reader) (*Response, error) { 575 | options := mergeOptions(defaultOptions, this.options, this.oneTimeOptions) 576 | headers = mergeHeaders(this.Headers, headers, this.oneTimeHeaders) 577 | cookies := this.oneTimeCookies 578 | 579 | var transport http.RoundTripper 580 | var jar http.CookieJar 581 | var err error 582 | 583 | // transport 584 | if this.transport == nil || !this.reuseTransport { 585 | transport, err = prepareTransport(options) 586 | if err != nil { 587 | this.reset() 588 | return nil, err 589 | } 590 | 591 | if this.reuseTransport { 592 | this.transport = transport 593 | } 594 | } else { 595 | transport = this.transport 596 | } 597 | 598 | // jar 599 | if this.jar == nil || !this.reuseJar { 600 | jar, err = prepareJar(options) 601 | if err != nil { 602 | this.reset() 603 | return nil, err 604 | } 605 | 606 | if this.reuseJar { 607 | this.jar = jar 608 | } 609 | } else { 610 | jar = this.jar 611 | } 612 | 613 | // timeout 614 | timeout, err := prepareTimeout(options) 615 | if err != nil { 616 | this.reset() 617 | return nil, err 618 | } 619 | 620 | redirect, err := prepareRedirect(options) 621 | if err != nil { 622 | this.reset() 623 | return nil, err 624 | } 625 | 626 | req, err := prepareRequest(method, url, headers, body, options) 627 | if err != nil { 628 | this.reset() 629 | return nil, err 630 | } 631 | 632 | if debugEnabled, ok := options[OPT_DEBUG]; ok { 633 | if debugEnabled.(bool) { 634 | dump, err := httputil.DumpRequestOut(req, true) 635 | if err == nil { 636 | fmt.Printf("%s\n", dump) 637 | } 638 | } 639 | } 640 | 641 | if jar != nil { 642 | jar.SetCookies(req.URL, cookies) 643 | } else { 644 | for _, cookie := range cookies { 645 | req.AddCookie(cookie) 646 | } 647 | } 648 | 649 | if ctx, ok := options[OPT_CONTEXT]; ok { 650 | if c, ok := ctx.(context.Context); ok { 651 | req = req.WithContext(c) 652 | } 653 | } 654 | 655 | beforeReqFunc := options[OPT_BEFORE_REQUEST_FUNC] 656 | 657 | // release lock 658 | this.reset() 659 | 660 | c := &http.Client{ 661 | Transport: transport, 662 | CheckRedirect: redirect, 663 | Jar: jar, 664 | Timeout: timeout, 665 | } 666 | 667 | if beforeReqFunc != nil { 668 | if f, ok := beforeReqFunc.(func(c *http.Client, r *http.Request)); ok { 669 | f(c, req) 670 | } 671 | } 672 | 673 | res, err := c.Do(req) 674 | 675 | return &Response{res}, err 676 | } 677 | 678 | // The HEAD request 679 | func (this *HttpClient) Head(url string) (*Response, error) { 680 | return this.Do("HEAD", url, nil, nil) 681 | } 682 | 683 | // The GET request 684 | func (this *HttpClient) Get(url string, params ...interface{}) (*Response, error) { 685 | for _, p := range params { 686 | url = addParams(url, toUrlValues(p)) 687 | } 688 | 689 | return this.Do("GET", url, nil, nil) 690 | } 691 | 692 | // The DELETE request 693 | func (this *HttpClient) Delete(url string, params ...interface{}) (*Response, error) { 694 | for _, p := range params { 695 | url = addParams(url, toUrlValues(p)) 696 | } 697 | 698 | return this.Do("DELETE", url, nil, nil) 699 | } 700 | 701 | // The POST request 702 | // 703 | // With multipart set to true, the request will be encoded as 704 | // "multipart/form-data". 705 | // 706 | // If any of the params key starts with "@", it is considered as a form file 707 | // (similar to CURL but different). 708 | func (this *HttpClient) Post(url string, params interface{}) (*Response, 709 | error) { 710 | t := checkParamsType(params) 711 | if t == 2 { 712 | return this.Do("POST", url, nil, toReader(params)) 713 | } 714 | 715 | paramsValues := toUrlValues(params) 716 | // Post with files should be sent as multipart. 717 | if checkParamFile(paramsValues) { 718 | return this.PostMultipart(url, params) 719 | } 720 | 721 | headers := make(map[string]string) 722 | headers["Content-Type"] = "application/x-www-form-urlencoded" 723 | body := strings.NewReader(paramsValues.Encode()) 724 | 725 | return this.Do("POST", url, headers, body) 726 | } 727 | 728 | // Post with the request encoded as "multipart/form-data". 729 | func (this *HttpClient) PostMultipart(url string, params interface{}) ( 730 | *Response, error) { 731 | body := &bytes.Buffer{} 732 | writer := multipart.NewWriter(body) 733 | 734 | paramsValues := toUrlValues(params) 735 | // check files 736 | for k, v := range paramsValues { 737 | for _, vv := range v { 738 | // is file 739 | if k[0] == '@' { 740 | err := addFormFile(writer, k[1:], vv) 741 | if err != nil { 742 | this.reset() 743 | return nil, err 744 | } 745 | } else { 746 | writer.WriteField(k, vv) 747 | } 748 | } 749 | } 750 | headers := make(map[string]string) 751 | 752 | headers["Content-Type"] = writer.FormDataContentType() 753 | err := writer.Close() 754 | if err != nil { 755 | this.reset() 756 | return nil, err 757 | } 758 | 759 | return this.Do("POST", url, headers, body) 760 | } 761 | 762 | func (this *HttpClient) sendJson(method string, url string, data interface{}) (*Response, error) { 763 | headers := make(map[string]string) 764 | headers["Content-Type"] = "application/json" 765 | 766 | var body []byte 767 | switch t := data.(type) { 768 | case []byte: 769 | body = t 770 | case string: 771 | body = []byte(t) 772 | default: 773 | var err error 774 | body, err = json.Marshal(data) 775 | if err != nil { 776 | this.reset() 777 | return nil, err 778 | } 779 | } 780 | 781 | return this.Do(method, url, headers, bytes.NewReader(body)) 782 | } 783 | 784 | func (this *HttpClient) PostJson(url string, data interface{}) (*Response, error) { 785 | return this.sendJson("POST", url, data) 786 | } 787 | 788 | // The PUT request 789 | func (this *HttpClient) Put(url string, body io.Reader) (*Response, error) { 790 | return this.Do("PUT", url, nil, body) 791 | } 792 | 793 | // Put json data 794 | func (this *HttpClient) PutJson(url string, data interface{}) (*Response, error) { 795 | return this.sendJson("PUT", url, data) 796 | } 797 | 798 | // Patch json data 799 | func (this *HttpClient) PatchJson(url string, data interface{}) (*Response, error) { 800 | return this.sendJson("PATCH", url, data) 801 | } 802 | 803 | // The OPTIONS request 804 | func (this *HttpClient) Options(url string, params ...map[string]string) (*Response, error) { 805 | for _, p := range params { 806 | url = addParams(url, toUrlValues(p)) 807 | } 808 | 809 | return this.Do("OPTIONS", url, nil, nil) 810 | } 811 | 812 | // The CONNECT request 813 | func (this *HttpClient) Connect(url string, params ...map[string]string) (*Response, error) { 814 | for _, p := range params { 815 | url = addParams(url, toUrlValues(p)) 816 | } 817 | 818 | return this.Do("CONNECT", url, nil, nil) 819 | } 820 | 821 | // The TRACE request 822 | func (this *HttpClient) Trace(url string, params ...map[string]string) (*Response, error) { 823 | for _, p := range params { 824 | url = addParams(url, toUrlValues(p)) 825 | } 826 | 827 | return this.Do("TRACE", url, nil, nil) 828 | } 829 | 830 | // The PATCH request 831 | func (this *HttpClient) Patch(url string, params ...map[string]string) (*Response, error) { 832 | for _, p := range params { 833 | url = addParams(url, toUrlValues(p)) 834 | } 835 | 836 | return this.Do("PATCH", url, nil, nil) 837 | } 838 | 839 | // Get cookies of the client jar. 840 | func (this *HttpClient) Cookies(url_ string) []*http.Cookie { 841 | if this.jar != nil { 842 | u, _ := url.Parse(url_) 843 | return this.jar.Cookies(u) 844 | } 845 | 846 | return nil 847 | } 848 | 849 | // Get cookie values(k-v map) of the client jar. 850 | func (this *HttpClient) CookieValues(url_ string) map[string]string { 851 | m := make(map[string]string) 852 | 853 | for _, c := range this.Cookies(url_) { 854 | m[c.Name] = c.Value 855 | } 856 | 857 | return m 858 | } 859 | 860 | // Get cookie value of a specified cookie name. 861 | func (this *HttpClient) CookieValue(url_ string, key string) string { 862 | for _, c := range this.Cookies(url_) { 863 | if c.Name == key { 864 | return c.Value 865 | } 866 | } 867 | 868 | return "" 869 | } 870 | --------------------------------------------------------------------------------