├── .gitignore ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── goreq.go ├── goreq_test.go └── tags.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | 24 | src 25 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.9.2 4 | - tip 5 | notifications: 6 | email: 7 | - ionathan@gmail.com 8 | - marcosnils@gmail.com 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Jonathan Leibiusky and Marcos Lilljedahl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go get -v -d -t ./... 3 | go test -v 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://img.shields.io/travis/franela/goreq/master.svg)](https://travis-ci.org/franela/goreq) 2 | [![GoDoc](https://godoc.org/github.com/franela/goreq?status.svg)](https://godoc.org/github.com/franela/goreq) 3 | 4 | GoReq 5 | ======= 6 | 7 | Simple and sane HTTP request library for Go language. 8 | 9 | 10 | 11 | **Table of Contents** 12 | 13 | - [Why GoReq?](#user-content-why-goreq) 14 | - [How do I install it?](#user-content-how-do-i-install-it) 15 | - [What can I do with it?](#user-content-what-can-i-do-with-it) 16 | - [Making requests with different methods](#user-content-making-requests-with-different-methods) 17 | - [GET](#user-content-get) 18 | - [Tags](#user-content-tags) 19 | - [POST](#user-content-post) 20 | - [Sending payloads in the Body](#user-content-sending-payloads-in-the-body) 21 | - [Specifiying request headers](#user-content-specifiying-request-headers) 22 | - [Sending Cookies](#cookie-support) 23 | - [Setting timeouts](#user-content-setting-timeouts) 24 | - [Using the Response and Error](#user-content-using-the-response-and-error) 25 | - [Receiving JSON](#user-content-receiving-json) 26 | - [Sending/Receiving Compressed Payloads](#user-content-sendingreceiving-compressed-payloads) 27 | - [Using gzip compression:](#user-content-using-gzip-compression) 28 | - [Using deflate compression:](#user-content-using-deflate-compression) 29 | - [Using compressed responses:](#user-content-using-compressed-responses) 30 | - [Proxy](#proxy) 31 | - [Debugging requests](#debug) 32 | - [Getting raw Request & Response](#getting-raw-request--response) 33 | - [TODO:](#user-content-todo) 34 | 35 | 36 | 37 | Why GoReq? 38 | ========== 39 | 40 | Go has very nice native libraries that allows you to do lots of cool things. But sometimes those libraries are too low level, which means that to do a simple thing, like an HTTP Request, it takes some time. And if you want to do something as simple as adding a timeout to a request, you will end up writing several lines of code. 41 | 42 | This is why we think GoReq is useful. Because you can do all your HTTP requests in a very simple and comprehensive way, while enabling you to do more advanced stuff by giving you access to the native API. 43 | 44 | How do I install it? 45 | ==================== 46 | 47 | ```bash 48 | go get github.com/franela/goreq 49 | ``` 50 | 51 | What can I do with it? 52 | ====================== 53 | 54 | ## Making requests with different methods 55 | 56 | #### GET 57 | ```go 58 | res, err := goreq.Request{ Uri: "http://www.google.com" }.Do() 59 | ``` 60 | 61 | GoReq default method is GET. 62 | 63 | You can also set value to GET method easily 64 | 65 | ```go 66 | type Item struct { 67 | Limit int 68 | Skip int 69 | Fields string 70 | } 71 | 72 | item := Item { 73 | Limit: 3, 74 | Skip: 5, 75 | Fields: "Value", 76 | } 77 | 78 | res, err := goreq.Request{ 79 | Uri: "http://localhost:3000/", 80 | QueryString: item, 81 | }.Do() 82 | ``` 83 | The sample above will send `http://localhost:3000/?limit=3&skip=5&fields=Value` 84 | 85 | Alternatively the `url` tag can be used in struct fields to customize encoding properties 86 | 87 | ```go 88 | type Item struct { 89 | TheLimit int `url:"the_limit"` 90 | TheSkip string `url:"the_skip,omitempty"` 91 | TheFields string `url:"-"` 92 | } 93 | 94 | item := Item { 95 | TheLimit: 3, 96 | TheSkip: "", 97 | TheFields: "Value", 98 | } 99 | 100 | res, err := goreq.Request{ 101 | Uri: "http://localhost:3000/", 102 | QueryString: item, 103 | }.Do() 104 | ``` 105 | The sample above will send `http://localhost:3000/?the_limit=3` 106 | 107 | 108 | QueryString also support url.Values 109 | 110 | ```go 111 | item := url.Values{} 112 | item.Set("Limit", 3) 113 | item.Add("Field", "somefield") 114 | item.Add("Field", "someotherfield") 115 | 116 | res, err := goreq.Request{ 117 | Uri: "http://localhost:3000/", 118 | QueryString: item, 119 | }.Do() 120 | ``` 121 | 122 | The sample above will send `http://localhost:3000/?limit=3&field=somefield&field=someotherfield` 123 | 124 | ### Tags 125 | 126 | Struct field `url` tag is mainly used as the request parameter name. 127 | Tags can be comma separated multiple values, 1st value is for naming and rest has special meanings. 128 | 129 | - special tag for 1st value 130 | - `-`: value is ignored if set this 131 | 132 | - special tag for rest 2nd value 133 | - `omitempty`: zero-value is ignored if set this 134 | - `squash`: the fields of embedded struct is used for parameter 135 | 136 | #### Tag Examples 137 | 138 | ```go 139 | type Place struct { 140 | Country string `url:"country"` 141 | City string `url:"city"` 142 | ZipCode string `url:"zipcode,omitempty"` 143 | } 144 | 145 | type Person struct { 146 | Place `url:",squash"` 147 | 148 | FirstName string `url:"first_name"` 149 | LastName string `url:"last_name"` 150 | Age string `url:"age,omitempty"` 151 | Password string `url:"-"` 152 | } 153 | 154 | johnbull := Person{ 155 | Place: Place{ // squash the embedded struct value 156 | Country: "UK", 157 | City: "London", 158 | ZipCode: "SW1", 159 | }, 160 | FirstName: "John", 161 | LastName: "Doe", 162 | Age: "35", 163 | Password: "my-secret", // ignored for parameter 164 | } 165 | 166 | goreq.Request{ 167 | Uri: "http://localhost/", 168 | QueryString: johnbull, 169 | }.Do() 170 | // => `http://localhost/?first_name=John&last_name=Doe&age=35&country=UK&city=London&zip_code=SW1` 171 | 172 | 173 | // age and zipcode will be ignored because of `omitempty` 174 | // but firstname isn't. 175 | samurai := Person{ 176 | Place: Place{ // squash the embedded struct value 177 | Country: "Japan", 178 | City: "Tokyo", 179 | }, 180 | LastName: "Yagyu", 181 | } 182 | 183 | goreq.Request{ 184 | Uri: "http://localhost/", 185 | QueryString: samurai, 186 | }.Do() 187 | // => `http://localhost/?first_name=&last_name=yagyu&country=Japan&city=Tokyo` 188 | ``` 189 | 190 | 191 | #### POST 192 | 193 | ```go 194 | res, err := goreq.Request{ Method: "POST", Uri: "http://www.google.com" }.Do() 195 | ``` 196 | 197 | ## Sending payloads in the Body 198 | 199 | You can send ```string```, ```Reader``` or ```interface{}``` in the body. The first two will be sent as text. The last one will be marshalled to JSON, if possible. 200 | 201 | ```go 202 | type Item struct { 203 | Id int 204 | Name string 205 | } 206 | 207 | item := Item{ Id: 1111, Name: "foobar" } 208 | 209 | res, err := goreq.Request{ 210 | Method: "POST", 211 | Uri: "http://www.google.com", 212 | Body: item, 213 | }.Do() 214 | ``` 215 | 216 | ## Specifiying request headers 217 | 218 | We think that most of the times the request headers that you use are: ```Host```, ```Content-Type```, ```Accept``` and ```User-Agent```. This is why we decided to make it very easy to set these headers. 219 | 220 | ```go 221 | res, err := goreq.Request{ 222 | Uri: "http://www.google.com", 223 | Host: "foobar.com", 224 | Accept: "application/json", 225 | ContentType: "application/json", 226 | UserAgent: "goreq", 227 | }.Do() 228 | ``` 229 | 230 | But sometimes you need to set other headers. You can still do it. 231 | 232 | ```go 233 | req := goreq.Request{ Uri: "http://www.google.com" } 234 | 235 | req.AddHeader("X-Custom", "somevalue") 236 | 237 | req.Do() 238 | ``` 239 | 240 | Alternatively you can use the `WithHeader` function to keep the syntax short 241 | 242 | ```go 243 | res, err = goreq.Request{ Uri: "http://www.google.com" }.WithHeader("X-Custom", "somevalue").Do() 244 | ``` 245 | 246 | ## Cookie support 247 | 248 | Cookies can be either set at the request level by sending a [CookieJar](http://golang.org/pkg/net/http/cookiejar/) in the `CookieJar` request field 249 | or you can use goreq's one-liner WithCookie method as shown below 250 | 251 | ```go 252 | res, err := goreq.Request{ 253 | Uri: "http://www.google.com", 254 | }. 255 | WithCookie(&http.Cookie{Name: "c1", Value: "v1"}). 256 | Do() 257 | ``` 258 | 259 | ## Setting timeouts 260 | 261 | GoReq supports 2 kind of timeouts. A general connection timeout and a request specific one. By default the connection timeout is of 1 second. There is no default for request timeout, which means it will wait forever. 262 | 263 | You can change the connection timeout doing: 264 | 265 | ```go 266 | goreq.SetConnectTimeout(100 * time.Millisecond) 267 | ``` 268 | 269 | And specify the request timeout doing: 270 | 271 | ```go 272 | res, err := goreq.Request{ 273 | Uri: "http://www.google.com", 274 | Timeout: 500 * time.Millisecond, 275 | }.Do() 276 | ``` 277 | 278 | ## Using the Response and Error 279 | 280 | GoReq will always return 2 values: a ```Response``` and an ```Error```. 281 | If ```Error``` is not ```nil``` it means that an error happened while doing the request and you shouldn't use the ```Response``` in any way. 282 | You can check what happened by getting the error message: 283 | 284 | ```go 285 | fmt.Println(err.Error()) 286 | ``` 287 | And to make it easy to know if it was a timeout error, you can ask the error or return it: 288 | 289 | ```go 290 | if serr, ok := err.(*goreq.Error); ok { 291 | if serr.Timeout() { 292 | ... 293 | } 294 | } 295 | return err 296 | ``` 297 | 298 | If you don't get an error, you can safely use the ```Response```. 299 | 300 | ```go 301 | res.Uri // return final URL location of the response (fulfilled after redirect was made) 302 | res.StatusCode // return the status code of the response 303 | res.Body // gives you access to the body 304 | res.Body.ToString() // will return the body as a string 305 | res.Header.Get("Content-Type") // gives you access to all the response headers 306 | ``` 307 | Remember that you should **always** close `res.Body` if it's not `nil` 308 | 309 | ## Receiving JSON 310 | 311 | GoReq will help you to receive and unmarshal JSON. 312 | 313 | ```go 314 | type Item struct { 315 | Id int 316 | Name string 317 | } 318 | 319 | var item Item 320 | 321 | res.Body.FromJsonTo(&item) 322 | ``` 323 | 324 | ## Sending/Receiving Compressed Payloads 325 | GoReq supports gzip, deflate and zlib compression of requests' body and transparent decompression of responses provided they have a correct `Content-Encoding` header. 326 | 327 | ##### Using gzip compression: 328 | ```go 329 | res, err := goreq.Request{ 330 | Method: "POST", 331 | Uri: "http://www.google.com", 332 | Body: item, 333 | Compression: goreq.Gzip(), 334 | }.Do() 335 | ``` 336 | ##### Using deflate/zlib compression: 337 | ```go 338 | res, err := goreq.Request{ 339 | Method: "POST", 340 | Uri: "http://www.google.com", 341 | Body: item, 342 | Compression: goreq.Deflate(), 343 | }.Do() 344 | ``` 345 | ##### Using compressed responses: 346 | If servers replies a correct and matching `Content-Encoding` header (gzip requires `Content-Encoding: gzip` and deflate `Content-Encoding: deflate`) goreq transparently decompresses the response so the previous example should always work: 347 | ```go 348 | type Item struct { 349 | Id int 350 | Name string 351 | } 352 | res, err := goreq.Request{ 353 | Method: "POST", 354 | Uri: "http://www.google.com", 355 | Body: item, 356 | Compression: goreq.Gzip(), 357 | }.Do() 358 | var item Item 359 | res.Body.FromJsonTo(&item) 360 | ``` 361 | If no `Content-Encoding` header is replied by the server GoReq will return the crude response. 362 | 363 | ## Proxy 364 | If you need to use a proxy for your requests GoReq supports the standard `http_proxy` env variable as well as manually setting the proxy for each request 365 | 366 | ```go 367 | res, err := goreq.Request{ 368 | Method: "GET", 369 | Proxy: "http://myproxy:myproxyport", 370 | Uri: "http://www.google.com", 371 | }.Do() 372 | ``` 373 | 374 | ### Proxy basic auth is also supported 375 | 376 | ```go 377 | res, err := goreq.Request{ 378 | Method: "GET", 379 | Proxy: "http://user:pass@myproxy:myproxyport", 380 | Uri: "http://www.google.com", 381 | }.Do() 382 | ``` 383 | 384 | ## Debug 385 | If you need to debug your http requests, it can print the http request detail. 386 | 387 | ```go 388 | res, err := goreq.Request{ 389 | Method: "GET", 390 | Uri: "http://www.google.com", 391 | Compression: goreq.Gzip(), 392 | ShowDebug: true, 393 | }.Do() 394 | fmt.Println(res, err) 395 | ``` 396 | 397 | and it will print the log: 398 | ``` 399 | GET / HTTP/1.1 400 | Host: www.google.com 401 | Accept: 402 | Accept-Encoding: gzip 403 | Content-Encoding: gzip 404 | Content-Type: 405 | ``` 406 | 407 | 408 | ### Getting raw Request & Response 409 | 410 | To get the Request: 411 | 412 | ```go 413 | req := goreq.Request{ 414 | Host: "foobar.com", 415 | } 416 | 417 | //req.Request will return a new instance of an http.Request so you can safely use it for something else 418 | request, _ := req.NewRequest() 419 | 420 | ``` 421 | 422 | 423 | To get the Response: 424 | 425 | ```go 426 | res, err := goreq.Request{ 427 | Method: "GET", 428 | Uri: "http://www.google.com", 429 | Compression: goreq.Gzip(), 430 | ShowDebug: true, 431 | }.Do() 432 | 433 | // res.Response will contain the original http.Response structure 434 | fmt.Println(res.Response, err) 435 | ``` 436 | 437 | 438 | 439 | 440 | TODO: 441 | ----- 442 | 443 | We do have a couple of [issues](https://github.com/franela/goreq/issues) pending we'll be addressing soon. But feel free to 444 | contribute and send us PRs (with tests please :smile:). 445 | -------------------------------------------------------------------------------- /goreq.go: -------------------------------------------------------------------------------- 1 | package goreq 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "compress/gzip" 7 | "compress/zlib" 8 | "crypto/tls" 9 | "encoding/json" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "io/ioutil" 14 | "log" 15 | "net" 16 | "net/http" 17 | "net/http/httputil" 18 | "net/url" 19 | "reflect" 20 | "strings" 21 | "time" 22 | ) 23 | 24 | type itimeout interface { 25 | Timeout() bool 26 | } 27 | type Request struct { 28 | headers []headerTuple 29 | cookies []*http.Cookie 30 | Method string 31 | Uri string 32 | Body interface{} 33 | QueryString interface{} 34 | Timeout time.Duration 35 | ContentType string 36 | Accept string 37 | Host string 38 | UserAgent string 39 | Insecure bool 40 | MaxRedirects int 41 | RedirectHeaders bool 42 | Proxy string 43 | proxyConnectHeaders []headerTuple 44 | Compression *compression 45 | BasicAuthUsername string 46 | BasicAuthPassword string 47 | CookieJar http.CookieJar 48 | ShowDebug bool 49 | OnBeforeRequest func(goreq *Request, httpreq *http.Request) 50 | } 51 | 52 | type compression struct { 53 | writer func(buffer io.Writer) (io.WriteCloser, error) 54 | reader func(buffer io.Reader) (io.ReadCloser, error) 55 | ContentEncoding string 56 | } 57 | 58 | type Response struct { 59 | *http.Response 60 | Uri string 61 | Body *Body 62 | req *http.Request 63 | } 64 | 65 | func (r Response) CancelRequest() { 66 | cancelRequest(DefaultTransport, r.req) 67 | 68 | } 69 | 70 | func cancelRequest(transport interface{}, r *http.Request) { 71 | if tp, ok := transport.(transportRequestCanceler); ok { 72 | tp.CancelRequest(r) 73 | } 74 | } 75 | 76 | type headerTuple struct { 77 | name string 78 | value string 79 | } 80 | 81 | type Body struct { 82 | reader io.ReadCloser 83 | compressedReader io.ReadCloser 84 | } 85 | 86 | type Error struct { 87 | timeout bool 88 | Err error 89 | } 90 | 91 | type transportRequestCanceler interface { 92 | CancelRequest(*http.Request) 93 | } 94 | 95 | func (e *Error) Timeout() bool { 96 | return e.timeout 97 | } 98 | 99 | func (e *Error) Error() string { 100 | return e.Err.Error() 101 | } 102 | 103 | func (b *Body) Read(p []byte) (int, error) { 104 | if b.compressedReader != nil { 105 | return b.compressedReader.Read(p) 106 | } 107 | return b.reader.Read(p) 108 | } 109 | 110 | func (b *Body) Close() error { 111 | err := b.reader.Close() 112 | if b.compressedReader != nil { 113 | return b.compressedReader.Close() 114 | } 115 | return err 116 | } 117 | 118 | func (b *Body) FromJsonTo(o interface{}) error { 119 | return json.NewDecoder(b).Decode(o) 120 | } 121 | 122 | func (b *Body) ToString() (string, error) { 123 | body, err := ioutil.ReadAll(b) 124 | if err != nil { 125 | return "", err 126 | } 127 | return string(body), nil 128 | } 129 | 130 | func Gzip() *compression { 131 | reader := func(buffer io.Reader) (io.ReadCloser, error) { 132 | return gzip.NewReader(buffer) 133 | } 134 | writer := func(buffer io.Writer) (io.WriteCloser, error) { 135 | return gzip.NewWriter(buffer), nil 136 | } 137 | return &compression{writer: writer, reader: reader, ContentEncoding: "gzip"} 138 | } 139 | 140 | func Deflate() *compression { 141 | reader := func(buffer io.Reader) (io.ReadCloser, error) { 142 | return zlib.NewReader(buffer) 143 | } 144 | writer := func(buffer io.Writer) (io.WriteCloser, error) { 145 | return zlib.NewWriter(buffer), nil 146 | } 147 | return &compression{writer: writer, reader: reader, ContentEncoding: "deflate"} 148 | } 149 | 150 | func Zlib() *compression { 151 | return Deflate() 152 | } 153 | 154 | func paramParse(query interface{}) (string, error) { 155 | switch query.(type) { 156 | case url.Values: 157 | return query.(url.Values).Encode(), nil 158 | case *url.Values: 159 | return query.(*url.Values).Encode(), nil 160 | default: 161 | var v = &url.Values{} 162 | err := paramParseStruct(v, query) 163 | return v.Encode(), err 164 | } 165 | } 166 | 167 | func paramParseStruct(v *url.Values, query interface{}) error { 168 | var ( 169 | s = reflect.ValueOf(query) 170 | t = reflect.TypeOf(query) 171 | ) 172 | for t.Kind() == reflect.Ptr || t.Kind() == reflect.Interface { 173 | s = s.Elem() 174 | t = s.Type() 175 | } 176 | 177 | if t.Kind() != reflect.Struct { 178 | return errors.New("Can not parse QueryString.") 179 | } 180 | 181 | for i := 0; i < t.NumField(); i++ { 182 | var name string 183 | 184 | field := s.Field(i) 185 | typeField := t.Field(i) 186 | 187 | if !field.CanInterface() { 188 | continue 189 | } 190 | 191 | urlTag := typeField.Tag.Get("url") 192 | if urlTag == "-" { 193 | continue 194 | } 195 | 196 | name, opts := parseTag(urlTag) 197 | 198 | var omitEmpty, squash bool 199 | omitEmpty = opts.Contains("omitempty") 200 | squash = opts.Contains("squash") 201 | 202 | if squash { 203 | err := paramParseStruct(v, field.Interface()) 204 | if err != nil { 205 | return err 206 | } 207 | continue 208 | } 209 | 210 | if urlTag == "" { 211 | name = strings.ToLower(typeField.Name) 212 | } 213 | 214 | if val := fmt.Sprintf("%v", field.Interface()); !(omitEmpty && len(val) == 0) { 215 | v.Add(name, val) 216 | } 217 | } 218 | return nil 219 | } 220 | 221 | func prepareRequestBody(b interface{}) (io.Reader, error) { 222 | switch b.(type) { 223 | case string: 224 | // treat is as text 225 | return strings.NewReader(b.(string)), nil 226 | case io.Reader: 227 | // treat is as text 228 | return b.(io.Reader), nil 229 | case []byte: 230 | //treat as byte array 231 | return bytes.NewReader(b.([]byte)), nil 232 | case nil: 233 | return nil, nil 234 | default: 235 | // try to jsonify it 236 | j, err := json.Marshal(b) 237 | if err == nil { 238 | return bytes.NewReader(j), nil 239 | } 240 | return nil, err 241 | } 242 | } 243 | 244 | var DefaultDialer = &net.Dialer{Timeout: 1000 * time.Millisecond} 245 | var DefaultTransport http.RoundTripper = &http.Transport{Dial: DefaultDialer.Dial, Proxy: http.ProxyFromEnvironment} 246 | var DefaultClient = &http.Client{Transport: DefaultTransport} 247 | 248 | var proxyTransport http.RoundTripper 249 | var proxyClient *http.Client 250 | 251 | func SetConnectTimeout(duration time.Duration) { 252 | DefaultDialer.Timeout = duration 253 | } 254 | 255 | func (r *Request) AddHeader(name string, value string) { 256 | if r.headers == nil { 257 | r.headers = []headerTuple{} 258 | } 259 | r.headers = append(r.headers, headerTuple{name: name, value: value}) 260 | } 261 | 262 | func (r Request) WithHeader(name string, value string) Request { 263 | r.AddHeader(name, value) 264 | return r 265 | } 266 | 267 | func (r *Request) AddCookie(c *http.Cookie) { 268 | r.cookies = append(r.cookies, c) 269 | } 270 | 271 | func (r Request) WithCookie(c *http.Cookie) Request { 272 | r.AddCookie(c) 273 | return r 274 | } 275 | 276 | func (r *Request) AddProxyConnectHeader(name string, value string) { 277 | if r.proxyConnectHeaders == nil { 278 | r.proxyConnectHeaders = []headerTuple{} 279 | } 280 | r.proxyConnectHeaders = append(r.proxyConnectHeaders, headerTuple{name: name, value: value}) 281 | } 282 | 283 | func (r Request) WithProxyConnectHeader(name string, value string) Request { 284 | r.AddProxyConnectHeader(name, value) 285 | return r 286 | } 287 | 288 | func (r Request) Do() (*Response, error) { 289 | var client = DefaultClient 290 | var transport = DefaultTransport 291 | var resUri string 292 | var redirectFailed bool 293 | 294 | r.Method = valueOrDefault(r.Method, "GET") 295 | 296 | // use a client with a cookie jar if necessary. We create a new client not 297 | // to modify the default one. 298 | if r.CookieJar != nil { 299 | client = &http.Client{ 300 | Transport: transport, 301 | Jar: r.CookieJar, 302 | } 303 | } 304 | 305 | if r.Proxy != "" { 306 | proxyUrl, err := url.Parse(r.Proxy) 307 | if err != nil { 308 | // proxy address is in a wrong format 309 | return nil, &Error{Err: err} 310 | } 311 | 312 | proxyHeader := make(http.Header) 313 | if r.proxyConnectHeaders != nil { 314 | for _, header := range r.proxyConnectHeaders { 315 | proxyHeader.Add(header.name, header.value) 316 | } 317 | } 318 | 319 | //If jar is specified new client needs to be built 320 | if proxyTransport == nil || client.Jar != nil { 321 | proxyTransport = &http.Transport{ 322 | Dial: DefaultDialer.Dial, 323 | Proxy: http.ProxyURL(proxyUrl), 324 | ProxyConnectHeader: proxyHeader, 325 | } 326 | proxyClient = &http.Client{Transport: proxyTransport, Jar: client.Jar} 327 | } else if proxyTransport, ok := proxyTransport.(*http.Transport); ok { 328 | proxyTransport.Proxy = http.ProxyURL(proxyUrl) 329 | proxyTransport.ProxyConnectHeader = proxyHeader 330 | } 331 | transport = proxyTransport 332 | client = proxyClient 333 | } 334 | 335 | client.CheckRedirect = func(req *http.Request, via []*http.Request) error { 336 | 337 | if len(via) > r.MaxRedirects { 338 | redirectFailed = true 339 | return errors.New("Error redirecting. MaxRedirects reached") 340 | } 341 | 342 | resUri = req.URL.String() 343 | 344 | //By default Golang will not redirect request headers 345 | // https://code.google.com/p/go/issues/detail?id=4800&q=request%20header 346 | if r.RedirectHeaders { 347 | for key, val := range via[0].Header { 348 | req.Header[key] = val 349 | } 350 | } 351 | return nil 352 | } 353 | 354 | if transport, ok := transport.(*http.Transport); ok { 355 | if r.Insecure { 356 | if transport.TLSClientConfig != nil { 357 | transport.TLSClientConfig.InsecureSkipVerify = true 358 | } else { 359 | transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} 360 | } 361 | } else if transport.TLSClientConfig != nil { 362 | // the default TLS client (when transport.TLSClientConfig==nil) is 363 | // already set to verify, so do nothing in that case 364 | transport.TLSClientConfig.InsecureSkipVerify = false 365 | } 366 | } 367 | 368 | req, err := r.NewRequest() 369 | 370 | if err != nil { 371 | // we couldn't parse the URL. 372 | return nil, &Error{Err: err} 373 | } 374 | 375 | timeout := false 376 | if r.Timeout > 0 { 377 | client.Timeout = r.Timeout 378 | } 379 | 380 | if r.ShowDebug { 381 | dump, err := httputil.DumpRequest(req, true) 382 | if err != nil { 383 | log.Println(err) 384 | } 385 | log.Println(string(dump)) 386 | } 387 | 388 | if r.OnBeforeRequest != nil { 389 | r.OnBeforeRequest(&r, req) 390 | } 391 | res, err := client.Do(req) 392 | 393 | if err != nil { 394 | if !timeout { 395 | if t, ok := err.(itimeout); ok { 396 | timeout = t.Timeout() 397 | } 398 | if ue, ok := err.(*url.Error); ok { 399 | if t, ok := ue.Err.(itimeout); ok { 400 | timeout = t.Timeout() 401 | } 402 | } 403 | } 404 | 405 | var response *Response 406 | //If redirect fails we still want to return response data 407 | if redirectFailed { 408 | if res != nil { 409 | response = &Response{res, resUri, &Body{reader: res.Body}, req} 410 | } else { 411 | response = &Response{res, resUri, nil, req} 412 | } 413 | } 414 | 415 | //If redirect fails and we haven't set a redirect count we shouldn't return an error 416 | if redirectFailed && r.MaxRedirects == 0 { 417 | return response, nil 418 | } 419 | 420 | return response, &Error{timeout: timeout, Err: err} 421 | } 422 | 423 | if r.Compression != nil && strings.Contains(res.Header.Get("Content-Encoding"), r.Compression.ContentEncoding) { 424 | compressedReader, err := r.Compression.reader(res.Body) 425 | if err != nil { 426 | return nil, &Error{Err: err} 427 | } 428 | return &Response{res, resUri, &Body{reader: res.Body, compressedReader: compressedReader}, req}, nil 429 | } 430 | 431 | return &Response{res, resUri, &Body{reader: res.Body}, req}, nil 432 | } 433 | 434 | func (r Request) addHeaders(headersMap http.Header) { 435 | if len(r.UserAgent) > 0 { 436 | headersMap.Add("User-Agent", r.UserAgent) 437 | } 438 | if r.Accept != "" { 439 | headersMap.Add("Accept", r.Accept) 440 | } 441 | if r.ContentType != "" { 442 | headersMap.Add("Content-Type", r.ContentType) 443 | } 444 | } 445 | 446 | func (r Request) NewRequest() (*http.Request, error) { 447 | 448 | b, e := prepareRequestBody(r.Body) 449 | if e != nil { 450 | // there was a problem marshaling the body 451 | return nil, &Error{Err: e} 452 | } 453 | 454 | if r.QueryString != nil { 455 | param, e := paramParse(r.QueryString) 456 | if e != nil { 457 | return nil, &Error{Err: e} 458 | } 459 | r.Uri = r.Uri + "?" + param 460 | } 461 | 462 | var bodyReader io.Reader 463 | if b != nil && r.Compression != nil { 464 | buffer := bytes.NewBuffer([]byte{}) 465 | readBuffer := bufio.NewReader(b) 466 | writer, err := r.Compression.writer(buffer) 467 | if err != nil { 468 | return nil, &Error{Err: err} 469 | } 470 | _, e = readBuffer.WriteTo(writer) 471 | writer.Close() 472 | if e != nil { 473 | return nil, &Error{Err: e} 474 | } 475 | bodyReader = buffer 476 | } else { 477 | bodyReader = b 478 | } 479 | 480 | req, err := http.NewRequest(r.Method, r.Uri, bodyReader) 481 | if err != nil { 482 | return nil, err 483 | } 484 | // add headers to the request 485 | req.Host = r.Host 486 | 487 | r.addHeaders(req.Header) 488 | if r.Compression != nil { 489 | req.Header.Add("Content-Encoding", r.Compression.ContentEncoding) 490 | req.Header.Add("Accept-Encoding", r.Compression.ContentEncoding) 491 | } 492 | if r.headers != nil { 493 | for _, header := range r.headers { 494 | req.Header.Add(header.name, header.value) 495 | } 496 | } 497 | 498 | //use basic auth if required 499 | if r.BasicAuthUsername != "" { 500 | req.SetBasicAuth(r.BasicAuthUsername, r.BasicAuthPassword) 501 | } 502 | 503 | for _, c := range r.cookies { 504 | req.AddCookie(c) 505 | } 506 | return req, nil 507 | } 508 | 509 | // Return value if nonempty, def otherwise. 510 | func valueOrDefault(value, def string) string { 511 | if value != "" { 512 | return value 513 | } 514 | return def 515 | } 516 | -------------------------------------------------------------------------------- /goreq_test.go: -------------------------------------------------------------------------------- 1 | package goreq 2 | 3 | import ( 4 | "compress/gzip" 5 | "compress/zlib" 6 | "encoding/base64" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "math" 11 | "net/http" 12 | "net/http/cookiejar" 13 | "net/http/httptest" 14 | "net/url" 15 | "strings" 16 | "testing" 17 | "time" 18 | 19 | . "github.com/franela/goblin" 20 | . "github.com/onsi/gomega" 21 | ) 22 | 23 | type Query struct { 24 | Limit int 25 | Skip int 26 | } 27 | 28 | func TestRequest(t *testing.T) { 29 | 30 | query := Query{ 31 | Limit: 3, 32 | Skip: 5, 33 | } 34 | 35 | valuesQuery := url.Values{} 36 | valuesQuery.Set("name", "marcos") 37 | valuesQuery.Add("friend", "jonas") 38 | valuesQuery.Add("friend", "peter") 39 | 40 | g := Goblin(t) 41 | 42 | RegisterFailHandler(func(m string, _ ...int) { g.Fail(m) }) 43 | 44 | g.Describe("Request", func() { 45 | 46 | g.Describe("General request methods", func() { 47 | var ts *httptest.Server 48 | var requestHeaders http.Header 49 | 50 | g.Before(func() { 51 | ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 52 | requestHeaders = r.Header 53 | if (r.Method == "GET" || r.Method == "OPTIONS" || r.Method == "TRACE" || r.Method == "PATCH" || r.Method == "FOOBAR") && r.URL.Path == "/foo" { 54 | w.WriteHeader(200) 55 | fmt.Fprint(w, "bar") 56 | } 57 | if r.Method == "GET" && r.URL.Path == "/getquery" { 58 | w.WriteHeader(200) 59 | fmt.Fprint(w, fmt.Sprintf("%v", r.URL)) 60 | } 61 | if r.Method == "GET" && r.URL.Path == "/getbody" { 62 | w.WriteHeader(200) 63 | io.Copy(w, r.Body) 64 | } 65 | if r.Method == "POST" && r.URL.Path == "/" { 66 | w.Header().Add("Location", ts.URL+"/123") 67 | w.WriteHeader(201) 68 | io.Copy(w, r.Body) 69 | } 70 | if r.Method == "POST" && r.URL.Path == "/getquery" { 71 | w.WriteHeader(200) 72 | fmt.Fprint(w, fmt.Sprintf("%v", r.URL)) 73 | } 74 | if r.Method == "PUT" && r.URL.Path == "/foo/123" { 75 | w.WriteHeader(200) 76 | io.Copy(w, r.Body) 77 | } 78 | if r.Method == "DELETE" && r.URL.Path == "/foo/123" { 79 | w.WriteHeader(204) 80 | } 81 | if r.Method == "GET" && r.URL.Path == "/redirect_test/301" { 82 | http.Redirect(w, r, "/redirect_test/302", 301) 83 | } 84 | if r.Method == "GET" && r.URL.Path == "/redirect_test/302" { 85 | http.Redirect(w, r, "/redirect_test/303", 302) 86 | } 87 | if r.Method == "GET" && r.URL.Path == "/redirect_test/303" { 88 | http.Redirect(w, r, "/redirect_test/307", 303) 89 | } 90 | if r.Method == "GET" && r.URL.Path == "/redirect_test/307" { 91 | http.Redirect(w, r, "/getquery", 307) 92 | } 93 | if r.Method == "GET" && r.URL.Path == "/redirect_test/destination" { 94 | http.Redirect(w, r, ts.URL+"/destination", 301) 95 | } 96 | if r.Method == "GET" && r.URL.Path == "/getcookies" { 97 | defer r.Body.Close() 98 | w.WriteHeader(200) 99 | fmt.Fprint(w, requestHeaders.Get("Cookie")) 100 | } 101 | if r.Method == "GET" && r.URL.Path == "/setcookies" { 102 | defer r.Body.Close() 103 | w.Header().Add("Set-Cookie", "foobar=42 ; Path=/") 104 | w.WriteHeader(200) 105 | } 106 | if r.Method == "GET" && r.URL.Path == "/compressed" { 107 | defer r.Body.Close() 108 | b := "{\"foo\":\"bar\",\"fuu\":\"baz\"}" 109 | gw := gzip.NewWriter(w) 110 | defer gw.Close() 111 | if strings.Contains(r.Header.Get("Content-Encoding"), "gzip") { 112 | w.Header().Add("Content-Encoding", "gzip") 113 | } 114 | w.WriteHeader(200) 115 | gw.Write([]byte(b)) 116 | } 117 | if r.Method == "GET" && r.URL.Path == "/compressed_deflate" { 118 | defer r.Body.Close() 119 | b := "{\"foo\":\"bar\",\"fuu\":\"baz\"}" 120 | gw := zlib.NewWriter(w) 121 | defer gw.Close() 122 | if strings.Contains(r.Header.Get("Content-Encoding"), "deflate") { 123 | w.Header().Add("Content-Encoding", "deflate") 124 | } 125 | w.WriteHeader(200) 126 | gw.Write([]byte(b)) 127 | } 128 | if r.Method == "GET" && r.URL.Path == "/compressed_and_return_compressed_without_header" { 129 | defer r.Body.Close() 130 | b := "{\"foo\":\"bar\",\"fuu\":\"baz\"}" 131 | gw := gzip.NewWriter(w) 132 | defer gw.Close() 133 | w.WriteHeader(200) 134 | gw.Write([]byte(b)) 135 | } 136 | if r.Method == "GET" && r.URL.Path == "/compressed_deflate_and_return_compressed_without_header" { 137 | defer r.Body.Close() 138 | b := "{\"foo\":\"bar\",\"fuu\":\"baz\"}" 139 | gw := zlib.NewWriter(w) 140 | defer gw.Close() 141 | w.WriteHeader(200) 142 | gw.Write([]byte(b)) 143 | } 144 | if r.Method == "POST" && r.URL.Path == "/compressed" && r.Header.Get("Content-Encoding") == "gzip" { 145 | defer r.Body.Close() 146 | gr, _ := gzip.NewReader(r.Body) 147 | defer gr.Close() 148 | b, _ := ioutil.ReadAll(gr) 149 | w.WriteHeader(201) 150 | w.Write(b) 151 | } 152 | if r.Method == "POST" && r.URL.Path == "/compressed_deflate" && r.Header.Get("Content-Encoding") == "deflate" { 153 | defer r.Body.Close() 154 | gr, _ := zlib.NewReader(r.Body) 155 | defer gr.Close() 156 | b, _ := ioutil.ReadAll(gr) 157 | w.WriteHeader(201) 158 | w.Write(b) 159 | } 160 | if r.Method == "POST" && r.URL.Path == "/compressed_and_return_compressed" { 161 | defer r.Body.Close() 162 | w.Header().Add("Content-Encoding", "gzip") 163 | w.WriteHeader(201) 164 | io.Copy(w, r.Body) 165 | } 166 | if r.Method == "POST" && r.URL.Path == "/compressed_deflate_and_return_compressed" { 167 | defer r.Body.Close() 168 | w.Header().Add("Content-Encoding", "deflate") 169 | w.WriteHeader(201) 170 | io.Copy(w, r.Body) 171 | } 172 | if r.Method == "POST" && r.URL.Path == "/compressed_deflate_and_return_compressed_without_header" { 173 | defer r.Body.Close() 174 | w.WriteHeader(201) 175 | io.Copy(w, r.Body) 176 | } 177 | if r.Method == "POST" && r.URL.Path == "/compressed_and_return_compressed_without_header" { 178 | defer r.Body.Close() 179 | w.WriteHeader(201) 180 | io.Copy(w, r.Body) 181 | } 182 | })) 183 | }) 184 | 185 | g.After(func() { 186 | ts.Close() 187 | }) 188 | 189 | g.Describe("GET", func() { 190 | 191 | g.It("Should do a GET", func() { 192 | res, err := Request{Uri: ts.URL + "/foo"}.Do() 193 | 194 | Expect(err).Should(BeNil()) 195 | str, _ := res.Body.ToString() 196 | Expect(str).Should(Equal("bar")) 197 | Expect(res.StatusCode).Should(Equal(200)) 198 | }) 199 | 200 | g.It("Should return ContentLength", func() { 201 | res, err := Request{Uri: ts.URL + "/foo"}.Do() 202 | 203 | Expect(err).Should(BeNil()) 204 | str, _ := res.Body.ToString() 205 | Expect(str).Should(Equal("bar")) 206 | Expect(res.StatusCode).Should(Equal(200)) 207 | Expect(res.ContentLength).Should(Equal(int64(3))) 208 | }) 209 | 210 | g.It("Should do a GET with querystring", func() { 211 | res, err := Request{ 212 | Uri: ts.URL + "/getquery", 213 | QueryString: query, 214 | }.Do() 215 | 216 | Expect(err).Should(BeNil()) 217 | str, _ := res.Body.ToString() 218 | Expect(str).Should(Equal("/getquery?limit=3&skip=5")) 219 | Expect(res.StatusCode).Should(Equal(200)) 220 | }) 221 | 222 | g.It("Should support url.Values in querystring", func() { 223 | res, err := Request{ 224 | Uri: ts.URL + "/getquery", 225 | QueryString: valuesQuery, 226 | }.Do() 227 | 228 | Expect(err).Should(BeNil()) 229 | str, _ := res.Body.ToString() 230 | Expect(str).Should(Equal("/getquery?friend=jonas&friend=peter&name=marcos")) 231 | Expect(res.StatusCode).Should(Equal(200)) 232 | }) 233 | 234 | g.It("Should support sending string body", func() { 235 | res, err := Request{Uri: ts.URL + "/getbody", Body: "foo"}.Do() 236 | 237 | Expect(err).Should(BeNil()) 238 | str, _ := res.Body.ToString() 239 | Expect(str).Should(Equal("foo")) 240 | Expect(res.StatusCode).Should(Equal(200)) 241 | }) 242 | 243 | g.It("Shoulds support sending a Reader body", func() { 244 | res, err := Request{Uri: ts.URL + "/getbody", Body: strings.NewReader("foo")}.Do() 245 | 246 | Expect(err).Should(BeNil()) 247 | str, _ := res.Body.ToString() 248 | Expect(str).Should(Equal("foo")) 249 | Expect(res.StatusCode).Should(Equal(200)) 250 | }) 251 | 252 | g.It("Support sending any object that is json encodable", func() { 253 | obj := map[string]string{"foo": "bar"} 254 | res, err := Request{Uri: ts.URL + "/getbody", Body: obj}.Do() 255 | 256 | Expect(err).Should(BeNil()) 257 | str, _ := res.Body.ToString() 258 | Expect(str).Should(Equal(`{"foo":"bar"}`)) 259 | Expect(res.StatusCode).Should(Equal(200)) 260 | }) 261 | 262 | g.It("Support sending an array of bytes body", func() { 263 | bdy := []byte{'f', 'o', 'o'} 264 | res, err := Request{Uri: ts.URL + "/getbody", Body: bdy}.Do() 265 | 266 | Expect(err).Should(BeNil()) 267 | str, _ := res.Body.ToString() 268 | Expect(str).Should(Equal("foo")) 269 | Expect(res.StatusCode).Should(Equal(200)) 270 | }) 271 | 272 | g.It("Should return an error when body is not JSON encodable", func() { 273 | res, err := Request{Uri: ts.URL + "/getbody", Body: math.NaN()}.Do() 274 | 275 | Expect(res).Should(BeNil()) 276 | Expect(err).ShouldNot(BeNil()) 277 | }) 278 | 279 | g.It("Should return a gzip reader if Content-Encoding is 'gzip'", func() { 280 | res, err := Request{Uri: ts.URL + "/compressed", Compression: Gzip()}.Do() 281 | b, _ := ioutil.ReadAll(res.Body) 282 | Expect(err).Should(BeNil()) 283 | Expect(res.Body.compressedReader).ShouldNot(BeNil()) 284 | Expect(res.Body.reader).ShouldNot(BeNil()) 285 | Expect(string(b)).Should(Equal("{\"foo\":\"bar\",\"fuu\":\"baz\"}")) 286 | Expect(res.Body.compressedReader).ShouldNot(BeNil()) 287 | Expect(res.Body.reader).ShouldNot(BeNil()) 288 | }) 289 | 290 | g.It("Should close reader and compresserReader on Body close", func() { 291 | res, err := Request{Uri: ts.URL + "/compressed", Compression: Gzip()}.Do() 292 | Expect(err).Should(BeNil()) 293 | 294 | _, e := ioutil.ReadAll(res.Body.reader) 295 | Expect(e).Should(BeNil()) 296 | _, e = ioutil.ReadAll(res.Body.compressedReader) 297 | Expect(e).Should(BeNil()) 298 | 299 | _, e = ioutil.ReadAll(res.Body.reader) 300 | //when reading body again it doesnt error 301 | Expect(e).Should(BeNil()) 302 | 303 | res.Body.Close() 304 | _, e = ioutil.ReadAll(res.Body.reader) 305 | //error because body is already closed 306 | Expect(e).ShouldNot(BeNil()) 307 | 308 | _, e = ioutil.ReadAll(res.Body.compressedReader) 309 | //compressedReaders dont error on reading when closed 310 | Expect(e).Should(BeNil()) 311 | }) 312 | 313 | g.It("Should not return a gzip reader if Content-Encoding is not 'gzip'", func() { 314 | res, err := Request{Uri: ts.URL + "/compressed_and_return_compressed_without_header", Compression: Gzip()}.Do() 315 | b, _ := ioutil.ReadAll(res.Body) 316 | Expect(err).Should(BeNil()) 317 | Expect(string(b)).ShouldNot(Equal("{\"foo\":\"bar\",\"fuu\":\"baz\"}")) 318 | }) 319 | 320 | g.It("Should return a deflate reader if Content-Encoding is 'deflate'", func() { 321 | res, err := Request{Uri: ts.URL + "/compressed_deflate", Compression: Deflate()}.Do() 322 | b, _ := ioutil.ReadAll(res.Body) 323 | Expect(err).Should(BeNil()) 324 | Expect(string(b)).Should(Equal("{\"foo\":\"bar\",\"fuu\":\"baz\"}")) 325 | }) 326 | 327 | g.It("Should not return a delfate reader if Content-Encoding is not 'deflate'", func() { 328 | res, err := Request{Uri: ts.URL + "/compressed_deflate_and_return_compressed_without_header", Compression: Deflate()}.Do() 329 | b, _ := ioutil.ReadAll(res.Body) 330 | Expect(err).Should(BeNil()) 331 | Expect(string(b)).ShouldNot(Equal("{\"foo\":\"bar\",\"fuu\":\"baz\"}")) 332 | }) 333 | 334 | g.It("Should return a deflate reader when using zlib if Content-Encoding is 'deflate'", func() { 335 | res, err := Request{Uri: ts.URL + "/compressed_deflate", Compression: Zlib()}.Do() 336 | b, _ := ioutil.ReadAll(res.Body) 337 | Expect(err).Should(BeNil()) 338 | Expect(string(b)).Should(Equal("{\"foo\":\"bar\",\"fuu\":\"baz\"}")) 339 | }) 340 | 341 | g.It("Should not return a delfate reader when using zlib if Content-Encoding is not 'deflate'", func() { 342 | res, err := Request{Uri: ts.URL + "/compressed_deflate_and_return_compressed_without_header", Compression: Zlib()}.Do() 343 | b, _ := ioutil.ReadAll(res.Body) 344 | Expect(err).Should(BeNil()) 345 | Expect(string(b)).ShouldNot(Equal("{\"foo\":\"bar\",\"fuu\":\"baz\"}")) 346 | }) 347 | 348 | g.It("Should send cookies from the cookiejar", func() { 349 | uri, err := url.Parse(ts.URL + "/getcookies") 350 | Expect(err).Should(BeNil()) 351 | 352 | jar, err := cookiejar.New(nil) 353 | Expect(err).Should(BeNil()) 354 | 355 | jar.SetCookies(uri, []*http.Cookie{ 356 | { 357 | Name: "bar", 358 | Value: "foo", 359 | Path: "/", 360 | }, 361 | }) 362 | 363 | res, err := Request{ 364 | Uri: ts.URL + "/getcookies", 365 | CookieJar: jar, 366 | }.Do() 367 | 368 | Expect(err).Should(BeNil()) 369 | str, _ := res.Body.ToString() 370 | Expect(str).Should(Equal("bar=foo")) 371 | Expect(res.StatusCode).Should(Equal(200)) 372 | Expect(res.ContentLength).Should(Equal(int64(7))) 373 | }) 374 | 375 | g.It("Should send cookies added with .AddCookie", func() { 376 | c1 := &http.Cookie{Name: "c1", Value: "v1"} 377 | c2 := &http.Cookie{Name: "c2", Value: "v2"} 378 | 379 | req := Request{Uri: ts.URL + "/getcookies"} 380 | req.AddCookie(c1) 381 | req.AddCookie(c2) 382 | 383 | res, err := req.Do() 384 | Expect(err).Should(BeNil()) 385 | str, _ := res.Body.ToString() 386 | Expect(str).Should(Equal("c1=v1; c2=v2")) 387 | Expect(res.StatusCode).Should(Equal(200)) 388 | Expect(res.ContentLength).Should(Equal(int64(12))) 389 | }) 390 | 391 | g.It("Should send cookies added with .WithCookie", func() { 392 | c1 := &http.Cookie{Name: "c1", Value: "v2"} 393 | c2 := &http.Cookie{Name: "c2", Value: "v3"} 394 | 395 | res, err := Request{Uri: ts.URL + "/getcookies"}. 396 | WithCookie(c1). 397 | WithCookie(c2). 398 | Do() 399 | Expect(err).Should(BeNil()) 400 | str, _ := res.Body.ToString() 401 | Expect(str).Should(Equal("c1=v2; c2=v3")) 402 | Expect(res.StatusCode).Should(Equal(200)) 403 | Expect(res.ContentLength).Should(Equal(int64(12))) 404 | }) 405 | 406 | g.It("Should populate the cookiejar", func() { 407 | uri, err := url.Parse(ts.URL + "/setcookies") 408 | Expect(err).Should(BeNil()) 409 | 410 | jar, _ := cookiejar.New(nil) 411 | Expect(err).Should(BeNil()) 412 | 413 | res, err := Request{ 414 | Uri: ts.URL + "/setcookies", 415 | CookieJar: jar, 416 | }.Do() 417 | 418 | Expect(err).Should(BeNil()) 419 | 420 | Expect(res.Header.Get("Set-Cookie")).Should(Equal("foobar=42 ; Path=/")) 421 | 422 | cookies := jar.Cookies(uri) 423 | Expect(len(cookies)).Should(Equal(1)) 424 | 425 | cookie := cookies[0] 426 | Expect(*cookie).Should(Equal(http.Cookie{ 427 | Name: "foobar", 428 | Value: "42", 429 | })) 430 | }) 431 | }) 432 | 433 | g.Describe("POST", func() { 434 | g.It("Should send a string", func() { 435 | res, err := Request{Method: "POST", Uri: ts.URL, Body: "foo"}.Do() 436 | 437 | Expect(err).Should(BeNil()) 438 | str, _ := res.Body.ToString() 439 | Expect(str).Should(Equal("foo")) 440 | Expect(res.StatusCode).Should(Equal(201)) 441 | Expect(res.Header.Get("Location")).Should(Equal(ts.URL + "/123")) 442 | }) 443 | 444 | g.It("Should send a Reader", func() { 445 | res, err := Request{Method: "POST", Uri: ts.URL, Body: strings.NewReader("foo")}.Do() 446 | 447 | Expect(err).Should(BeNil()) 448 | str, _ := res.Body.ToString() 449 | Expect(str).Should(Equal("foo")) 450 | Expect(res.StatusCode).Should(Equal(201)) 451 | Expect(res.Header.Get("Location")).Should(Equal(ts.URL + "/123")) 452 | }) 453 | 454 | g.It("Send any object that is json encodable", func() { 455 | obj := map[string]string{"foo": "bar"} 456 | res, err := Request{Method: "POST", Uri: ts.URL, Body: obj}.Do() 457 | 458 | Expect(err).Should(BeNil()) 459 | str, _ := res.Body.ToString() 460 | Expect(str).Should(Equal(`{"foo":"bar"}`)) 461 | Expect(res.StatusCode).Should(Equal(201)) 462 | Expect(res.Header.Get("Location")).Should(Equal(ts.URL + "/123")) 463 | }) 464 | 465 | g.It("Send an array of bytes", func() { 466 | bdy := []byte{'f', 'o', 'o'} 467 | res, err := Request{Method: "POST", Uri: ts.URL, Body: bdy}.Do() 468 | 469 | Expect(err).Should(BeNil()) 470 | str, _ := res.Body.ToString() 471 | Expect(str).Should(Equal("foo")) 472 | Expect(res.StatusCode).Should(Equal(201)) 473 | Expect(res.Header.Get("Location")).Should(Equal(ts.URL + "/123")) 474 | }) 475 | 476 | g.It("Should return an error when body is not JSON encodable", func() { 477 | res, err := Request{Method: "POST", Uri: ts.URL, Body: math.NaN()}.Do() 478 | 479 | Expect(res).Should(BeNil()) 480 | Expect(err).ShouldNot(BeNil()) 481 | }) 482 | 483 | g.It("Should do a POST with querystring", func() { 484 | bdy := []byte{'f', 'o', 'o'} 485 | res, err := Request{ 486 | Method: "POST", 487 | Uri: ts.URL + "/getquery", 488 | Body: bdy, 489 | QueryString: query, 490 | }.Do() 491 | 492 | Expect(err).Should(BeNil()) 493 | str, _ := res.Body.ToString() 494 | Expect(str).Should(Equal("/getquery?limit=3&skip=5")) 495 | Expect(res.StatusCode).Should(Equal(200)) 496 | }) 497 | 498 | g.It("Should send body as gzip if compressed", func() { 499 | obj := map[string]string{"foo": "bar"} 500 | res, err := Request{Method: "POST", Uri: ts.URL + "/compressed", Body: obj, Compression: Gzip()}.Do() 501 | 502 | Expect(err).Should(BeNil()) 503 | str, _ := res.Body.ToString() 504 | Expect(str).Should(Equal(`{"foo":"bar"}`)) 505 | Expect(res.StatusCode).Should(Equal(201)) 506 | }) 507 | 508 | g.It("Should send body as deflate if compressed", func() { 509 | obj := map[string]string{"foo": "bar"} 510 | res, err := Request{Method: "POST", Uri: ts.URL + "/compressed_deflate", Body: obj, Compression: Deflate()}.Do() 511 | 512 | Expect(err).Should(BeNil()) 513 | str, _ := res.Body.ToString() 514 | Expect(str).Should(Equal(`{"foo":"bar"}`)) 515 | Expect(res.StatusCode).Should(Equal(201)) 516 | }) 517 | 518 | g.It("Should send body as deflate using zlib if compressed", func() { 519 | obj := map[string]string{"foo": "bar"} 520 | res, err := Request{Method: "POST", Uri: ts.URL + "/compressed_deflate", Body: obj, Compression: Zlib()}.Do() 521 | 522 | Expect(err).Should(BeNil()) 523 | str, _ := res.Body.ToString() 524 | Expect(str).Should(Equal(`{"foo":"bar"}`)) 525 | Expect(res.StatusCode).Should(Equal(201)) 526 | }) 527 | 528 | g.It("Should send body as gzip if compressed and parse return body", func() { 529 | obj := map[string]string{"foo": "bar"} 530 | res, err := Request{Method: "POST", Uri: ts.URL + "/compressed_and_return_compressed", Body: obj, Compression: Gzip()}.Do() 531 | 532 | Expect(err).Should(BeNil()) 533 | b, _ := ioutil.ReadAll(res.Body) 534 | Expect(string(b)).Should(Equal(`{"foo":"bar"}`)) 535 | Expect(res.StatusCode).Should(Equal(201)) 536 | }) 537 | 538 | g.It("Should send body as deflate if compressed and parse return body", func() { 539 | obj := map[string]string{"foo": "bar"} 540 | res, err := Request{Method: "POST", Uri: ts.URL + "/compressed_deflate_and_return_compressed", Body: obj, Compression: Deflate()}.Do() 541 | 542 | Expect(err).Should(BeNil()) 543 | b, _ := ioutil.ReadAll(res.Body) 544 | Expect(string(b)).Should(Equal(`{"foo":"bar"}`)) 545 | Expect(res.StatusCode).Should(Equal(201)) 546 | }) 547 | 548 | g.It("Should send body as deflate using zlib if compressed and parse return body", func() { 549 | obj := map[string]string{"foo": "bar"} 550 | res, err := Request{Method: "POST", Uri: ts.URL + "/compressed_deflate_and_return_compressed", Body: obj, Compression: Zlib()}.Do() 551 | 552 | Expect(err).Should(BeNil()) 553 | b, _ := ioutil.ReadAll(res.Body) 554 | Expect(string(b)).Should(Equal(`{"foo":"bar"}`)) 555 | Expect(res.StatusCode).Should(Equal(201)) 556 | }) 557 | 558 | g.It("Should send body as gzip if compressed and not parse return body if header not set ", func() { 559 | obj := map[string]string{"foo": "bar"} 560 | res, err := Request{Method: "POST", Uri: ts.URL + "/compressed_and_return_compressed_without_header", Body: obj, Compression: Gzip()}.Do() 561 | 562 | Expect(err).Should(BeNil()) 563 | b, _ := ioutil.ReadAll(res.Body) 564 | Expect(string(b)).ShouldNot(Equal(`{"foo":"bar"}`)) 565 | Expect(res.StatusCode).Should(Equal(201)) 566 | }) 567 | 568 | g.It("Should send body as deflate if compressed and not parse return body if header not set ", func() { 569 | obj := map[string]string{"foo": "bar"} 570 | res, err := Request{Method: "POST", Uri: ts.URL + "/compressed_deflate_and_return_compressed_without_header", Body: obj, Compression: Deflate()}.Do() 571 | 572 | Expect(err).Should(BeNil()) 573 | b, _ := ioutil.ReadAll(res.Body) 574 | Expect(string(b)).ShouldNot(Equal(`{"foo":"bar"}`)) 575 | Expect(res.StatusCode).Should(Equal(201)) 576 | }) 577 | 578 | g.It("Should send body as deflate using zlib if compressed and not parse return body if header not set ", func() { 579 | obj := map[string]string{"foo": "bar"} 580 | res, err := Request{Method: "POST", Uri: ts.URL + "/compressed_deflate_and_return_compressed_without_header", Body: obj, Compression: Zlib()}.Do() 581 | 582 | Expect(err).Should(BeNil()) 583 | b, _ := ioutil.ReadAll(res.Body) 584 | Expect(string(b)).ShouldNot(Equal(`{"foo":"bar"}`)) 585 | Expect(res.StatusCode).Should(Equal(201)) 586 | }) 587 | }) 588 | 589 | g.It("Should do a PUT", func() { 590 | res, err := Request{Method: "PUT", Uri: ts.URL + "/foo/123", Body: "foo"}.Do() 591 | 592 | Expect(err).Should(BeNil()) 593 | str, _ := res.Body.ToString() 594 | Expect(str).Should(Equal("foo")) 595 | Expect(res.StatusCode).Should(Equal(200)) 596 | }) 597 | 598 | g.It("Should do a DELETE", func() { 599 | res, err := Request{Method: "DELETE", Uri: ts.URL + "/foo/123"}.Do() 600 | 601 | Expect(err).Should(BeNil()) 602 | Expect(res.StatusCode).Should(Equal(204)) 603 | }) 604 | 605 | g.It("Should do a OPTIONS", func() { 606 | res, err := Request{Method: "OPTIONS", Uri: ts.URL + "/foo"}.Do() 607 | 608 | Expect(err).Should(BeNil()) 609 | str, _ := res.Body.ToString() 610 | Expect(str).Should(Equal("bar")) 611 | Expect(res.StatusCode).Should(Equal(200)) 612 | }) 613 | 614 | g.It("Should do a PATCH", func() { 615 | res, err := Request{Method: "PATCH", Uri: ts.URL + "/foo"}.Do() 616 | 617 | Expect(err).Should(BeNil()) 618 | str, _ := res.Body.ToString() 619 | Expect(str).Should(Equal("bar")) 620 | Expect(res.StatusCode).Should(Equal(200)) 621 | }) 622 | 623 | g.It("Should do a TRACE", func() { 624 | res, err := Request{Method: "TRACE", Uri: ts.URL + "/foo"}.Do() 625 | 626 | Expect(err).Should(BeNil()) 627 | str, _ := res.Body.ToString() 628 | Expect(str).Should(Equal("bar")) 629 | Expect(res.StatusCode).Should(Equal(200)) 630 | }) 631 | 632 | g.It("Should do a custom method", func() { 633 | res, err := Request{Method: "FOOBAR", Uri: ts.URL + "/foo"}.Do() 634 | 635 | Expect(err).Should(BeNil()) 636 | str, _ := res.Body.ToString() 637 | Expect(str).Should(Equal("bar")) 638 | Expect(res.StatusCode).Should(Equal(200)) 639 | }) 640 | 641 | g.Describe("Responses", func() { 642 | g.It("Should handle strings", func() { 643 | res, _ := Request{Method: "POST", Uri: ts.URL, Body: "foo bar"}.Do() 644 | 645 | str, _ := res.Body.ToString() 646 | Expect(str).Should(Equal("foo bar")) 647 | }) 648 | 649 | g.It("Should handle io.ReaderCloser", func() { 650 | res, _ := Request{Method: "POST", Uri: ts.URL, Body: "foo bar"}.Do() 651 | 652 | body, _ := ioutil.ReadAll(res.Body) 653 | Expect(string(body)).Should(Equal("foo bar")) 654 | }) 655 | 656 | g.It("Should handle parsing JSON", func() { 657 | res, _ := Request{Method: "POST", Uri: ts.URL, Body: `{"foo": "bar"}`}.Do() 658 | 659 | var foobar map[string]string 660 | 661 | res.Body.FromJsonTo(&foobar) 662 | 663 | Expect(foobar).Should(Equal(map[string]string{"foo": "bar"})) 664 | }) 665 | 666 | g.It("Should return the original request response", func() { 667 | res, _ := Request{Method: "POST", Uri: ts.URL, Body: `{"foo": "bar"}`}.Do() 668 | 669 | Expect(res.Response).ShouldNot(BeNil()) 670 | }) 671 | }) 672 | g.Describe("Redirects", func() { 673 | g.It("Should not follow by default", func() { 674 | res, _ := Request{ 675 | Uri: ts.URL + "/redirect_test/301", 676 | }.Do() 677 | Expect(res.StatusCode).Should(Equal(301)) 678 | }) 679 | 680 | g.It("Should not follow if method is explicitly specified", func() { 681 | res, err := Request{ 682 | Method: "GET", 683 | Uri: ts.URL + "/redirect_test/301", 684 | }.Do() 685 | Expect(res.StatusCode).Should(Equal(301)) 686 | Expect(err).ShouldNot(HaveOccurred()) 687 | }) 688 | 689 | g.It("Should throw an error if MaxRedirect limit is exceeded", func() { 690 | res, err := Request{ 691 | Method: "GET", 692 | MaxRedirects: 1, 693 | Uri: ts.URL + "/redirect_test/301", 694 | }.Do() 695 | Expect(res.StatusCode).Should(Equal(302)) 696 | Expect(err).Should(HaveOccurred()) 697 | }) 698 | 699 | g.It("Should copy request headers headers when redirecting if specified", func() { 700 | req := Request{ 701 | Method: "GET", 702 | Uri: ts.URL + "/redirect_test/301", 703 | MaxRedirects: 4, 704 | RedirectHeaders: true, 705 | } 706 | req.AddHeader("Testheader", "TestValue") 707 | res, _ := req.Do() 708 | Expect(res.StatusCode).Should(Equal(200)) 709 | Expect(requestHeaders.Get("Testheader")).Should(Equal("TestValue")) 710 | }) 711 | 712 | g.It("Should follow only specified number of MaxRedirects", func() { 713 | res, _ := Request{ 714 | Uri: ts.URL + "/redirect_test/301", 715 | MaxRedirects: 1, 716 | }.Do() 717 | Expect(res.StatusCode).Should(Equal(302)) 718 | res, _ = Request{ 719 | Uri: ts.URL + "/redirect_test/301", 720 | MaxRedirects: 2, 721 | }.Do() 722 | Expect(res.StatusCode).Should(Equal(303)) 723 | res, _ = Request{ 724 | Uri: ts.URL + "/redirect_test/301", 725 | MaxRedirects: 3, 726 | }.Do() 727 | Expect(res.StatusCode).Should(Equal(307)) 728 | res, _ = Request{ 729 | Uri: ts.URL + "/redirect_test/301", 730 | MaxRedirects: 4, 731 | }.Do() 732 | Expect(res.StatusCode).Should(Equal(200)) 733 | }) 734 | 735 | g.It("Should return final URL of the response when redirecting", func() { 736 | res, _ := Request{ 737 | Uri: ts.URL + "/redirect_test/destination", 738 | MaxRedirects: 2, 739 | }.Do() 740 | Expect(res.Uri).Should(Equal(ts.URL + "/destination")) 741 | }) 742 | }) 743 | }) 744 | 745 | g.Describe("Timeouts", func() { 746 | 747 | g.Describe("Connection timeouts", func() { 748 | g.It("Should connect timeout after a default of 1000 ms", func() { 749 | start := time.Now() 750 | res, err := Request{Uri: "http://10.255.255.1"}.Do() 751 | elapsed := time.Since(start) 752 | 753 | Expect(elapsed).Should(BeNumerically("<", 1100*time.Millisecond)) 754 | Expect(elapsed).Should(BeNumerically(">=", 1000*time.Millisecond)) 755 | Expect(res).Should(BeNil()) 756 | Expect(err.(*Error).Timeout()).Should(BeTrue()) 757 | }) 758 | g.It("Should connect timeout after a custom amount of time", func() { 759 | SetConnectTimeout(100 * time.Millisecond) 760 | start := time.Now() 761 | res, err := Request{Uri: "http://10.255.255.1"}.Do() 762 | elapsed := time.Since(start) 763 | 764 | Expect(elapsed).Should(BeNumerically("<", 150*time.Millisecond)) 765 | Expect(elapsed).Should(BeNumerically(">=", 100*time.Millisecond)) 766 | Expect(res).Should(BeNil()) 767 | Expect(err.(*Error).Timeout()).Should(BeTrue()) 768 | }) 769 | g.It("Should connect timeout after a custom amount of time even with method set", func() { 770 | SetConnectTimeout(100 * time.Millisecond) 771 | start := time.Now() 772 | request := Request{ 773 | Uri: "http://10.255.255.1", 774 | Method: "GET", 775 | } 776 | res, err := request.Do() 777 | elapsed := time.Since(start) 778 | 779 | Expect(elapsed).Should(BeNumerically("<", 150*time.Millisecond)) 780 | Expect(elapsed).Should(BeNumerically(">=", 100*time.Millisecond)) 781 | Expect(res).Should(BeNil()) 782 | Expect(err.(*Error).Timeout()).Should(BeTrue()) 783 | }) 784 | }) 785 | 786 | g.Describe("Request timeout", func() { 787 | var ts *httptest.Server 788 | stop := make(chan bool) 789 | 790 | g.Before(func() { 791 | ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 792 | <-stop 793 | // just wait for someone to tell you when to end the request. this is used to simulate a slow server 794 | })) 795 | }) 796 | g.After(func() { 797 | stop <- true 798 | ts.Close() 799 | }) 800 | g.It("Should request timeout after a custom amount of time", func() { 801 | SetConnectTimeout(1000 * time.Millisecond) 802 | 803 | start := time.Now() 804 | res, err := Request{Uri: ts.URL, Timeout: 500 * time.Millisecond}.Do() 805 | elapsed := time.Since(start) 806 | 807 | Expect(elapsed).Should(BeNumerically("<", 550*time.Millisecond)) 808 | Expect(elapsed).Should(BeNumerically(">=", 500*time.Millisecond)) 809 | Expect(res).Should(BeNil()) 810 | Expect(err.(*Error).Timeout()).Should(BeTrue()) 811 | }) 812 | g.It("Should request timeout after a custom amount of time even with proxy", func() { 813 | proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 814 | time.Sleep(2000 * time.Millisecond) 815 | w.WriteHeader(200) 816 | })) 817 | SetConnectTimeout(1000 * time.Millisecond) 818 | start := time.Now() 819 | request := Request{ 820 | Uri: ts.URL, 821 | Proxy: proxy.URL, 822 | Timeout: 500 * time.Millisecond, 823 | } 824 | res, err := request.Do() 825 | elapsed := time.Since(start) 826 | 827 | Expect(elapsed).Should(BeNumerically("<", 550*time.Millisecond)) 828 | Expect(elapsed).Should(BeNumerically(">=", 500*time.Millisecond)) 829 | Expect(res).Should(BeNil()) 830 | Expect(err.(*Error).Timeout()).Should(BeTrue()) 831 | }) 832 | }) 833 | }) 834 | 835 | g.Describe("Misc", func() { 836 | g.It("Should set default golang user agent when not explicitly passed", func() { 837 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 838 | Expect(r.Header.Get("User-Agent")).ShouldNot(BeZero()) 839 | Expect(r.Host).Should(Equal("foobar.com")) 840 | 841 | w.WriteHeader(200) 842 | })) 843 | defer ts.Close() 844 | 845 | req := Request{Uri: ts.URL, Host: "foobar.com"} 846 | res, err := req.Do() 847 | Expect(err).ShouldNot(HaveOccurred()) 848 | 849 | Expect(res.StatusCode).Should(Equal(200)) 850 | }) 851 | 852 | g.It("Should offer to set request headers", func() { 853 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 854 | Expect(r.Header.Get("User-Agent")).Should(Equal("foobaragent")) 855 | Expect(r.Host).Should(Equal("foobar.com")) 856 | Expect(r.Header.Get("Accept")).Should(Equal("application/json")) 857 | Expect(r.Header.Get("Content-Type")).Should(Equal("application/json")) 858 | Expect(r.Header.Get("X-Custom")).Should(Equal("foobar")) 859 | Expect(r.Header.Get("X-Custom2")).Should(Equal("barfoo")) 860 | 861 | w.WriteHeader(200) 862 | })) 863 | defer ts.Close() 864 | 865 | req := Request{Uri: ts.URL, Accept: "application/json", ContentType: "application/json", UserAgent: "foobaragent", Host: "foobar.com"} 866 | req.AddHeader("X-Custom", "foobar") 867 | res, _ := req.WithHeader("X-Custom2", "barfoo").Do() 868 | 869 | Expect(res.StatusCode).Should(Equal(200)) 870 | }) 871 | 872 | g.It("Should call hook before request", func() { 873 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 874 | Expect(r.Header.Get("X-Custom")).Should(Equal("foobar")) 875 | 876 | w.WriteHeader(200) 877 | })) 878 | defer ts.Close() 879 | 880 | hook := func(goreq *Request, httpreq *http.Request) { 881 | httpreq.Header.Add("X-Custom", "foobar") 882 | } 883 | req := Request{Uri: ts.URL, OnBeforeRequest: hook} 884 | res, _ := req.Do() 885 | 886 | Expect(res.StatusCode).Should(Equal(200)) 887 | }) 888 | 889 | g.It("Should not create a body by defualt", func() { 890 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 891 | b, _ := ioutil.ReadAll(r.Body) 892 | Expect(b).Should(HaveLen(0)) 893 | w.WriteHeader(200) 894 | })) 895 | defer ts.Close() 896 | 897 | req := Request{Uri: ts.URL, Host: "foobar.com"} 898 | req.Do() 899 | }) 900 | g.It("Should change transport TLS config if Request.Insecure is set", func() { 901 | ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 902 | w.WriteHeader(200) 903 | })) 904 | defer ts.Close() 905 | 906 | req := Request{ 907 | Insecure: true, 908 | Uri: ts.URL, 909 | Host: "foobar.com", 910 | } 911 | res, _ := req.Do() 912 | 913 | Expect(DefaultClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify).Should(Equal(true)) 914 | Expect(res.StatusCode).Should(Equal(200)) 915 | }) 916 | g.It("Should work if a different transport is specified", func() { 917 | ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 918 | w.WriteHeader(200) 919 | })) 920 | defer ts.Close() 921 | var currentTransport = DefaultTransport 922 | DefaultTransport = &http.Transport{Dial: DefaultDialer.Dial} 923 | 924 | req := Request{ 925 | Insecure: true, 926 | Uri: ts.URL, 927 | Host: "foobar.com", 928 | } 929 | res, _ := req.Do() 930 | 931 | Expect(DefaultClient.Transport.(*http.Transport).TLSClientConfig.InsecureSkipVerify).Should(Equal(true)) 932 | Expect(res.StatusCode).Should(Equal(200)) 933 | 934 | DefaultTransport = currentTransport 935 | 936 | }) 937 | g.It("GetRequest should return the underlying httpRequest ", func() { 938 | req := Request{ 939 | Host: "foobar.com", 940 | } 941 | 942 | request, _ := req.NewRequest() 943 | Expect(request).ShouldNot(BeNil()) 944 | Expect(request.Host).Should(Equal(req.Host)) 945 | }) 946 | 947 | g.It("Response should allow to cancel in-flight request", func() { 948 | unblockc := make(chan bool) 949 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 950 | fmt.Fprintf(w, "Hello") 951 | w.(http.Flusher).Flush() 952 | <-unblockc 953 | })) 954 | defer ts.Close() 955 | defer close(unblockc) 956 | 957 | req := Request{ 958 | Insecure: true, 959 | Uri: ts.URL, 960 | Host: "foobar.com", 961 | } 962 | res, _ := req.Do() 963 | res.CancelRequest() 964 | _, err := ioutil.ReadAll(res.Body) 965 | g.Assert(err != nil).IsTrue() 966 | }) 967 | }) 968 | 969 | g.Describe("Errors", func() { 970 | var ts *httptest.Server 971 | 972 | g.Before(func() { 973 | ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 974 | if r.Method == "POST" && r.URL.Path == "/" { 975 | w.Header().Add("Location", ts.URL+"/123") 976 | w.WriteHeader(201) 977 | io.Copy(w, r.Body) 978 | } 979 | })) 980 | }) 981 | 982 | g.After(func() { 983 | ts.Close() 984 | }) 985 | g.It("Should throw an error when FromJsonTo fails", func() { 986 | res, _ := Request{Method: "POST", Uri: ts.URL, Body: `{"foo" "bar"}`}.Do() 987 | var foobar map[string]string 988 | 989 | err := res.Body.FromJsonTo(&foobar) 990 | Expect(err).Should(HaveOccurred()) 991 | }) 992 | g.It("Should handle Url parsing errors", func() { 993 | _, err := Request{Uri: ":"}.Do() 994 | 995 | Expect(err).ShouldNot(BeNil()) 996 | }) 997 | g.It("Should handle DNS errors", func() { 998 | _, err := Request{Uri: "http://.localhost"}.Do() 999 | Expect(err).ShouldNot(BeNil()) 1000 | }) 1001 | }) 1002 | 1003 | g.Describe("Proxy", func() { 1004 | var ts *httptest.Server 1005 | var lastReq *http.Request 1006 | g.Before(func() { 1007 | ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1008 | if r.Method == "GET" && r.URL.Path == "/" { 1009 | lastReq = r 1010 | w.Header().Add("x-forwarded-for", "test") 1011 | w.Header().Add("Set-Cookie", "foo=bar") 1012 | w.WriteHeader(200) 1013 | w.Write([]byte("")) 1014 | } else if r.Method == "GET" && r.URL.Path == "/redirect_test/301" { 1015 | http.Redirect(w, r, "/", 301) 1016 | } else if r.Method == "CONNECT" { 1017 | lastReq = r 1018 | } 1019 | })) 1020 | 1021 | }) 1022 | 1023 | g.BeforeEach(func() { 1024 | lastReq = nil 1025 | }) 1026 | 1027 | g.After(func() { 1028 | ts.Close() 1029 | }) 1030 | 1031 | g.It("Should use Proxy", func() { 1032 | proxiedHost := "www.google.com" 1033 | res, err := Request{Uri: "http://" + proxiedHost, Proxy: ts.URL}.Do() 1034 | Expect(err).Should(BeNil()) 1035 | Expect(res.Header.Get("x-forwarded-for")).Should(Equal("test")) 1036 | Expect(lastReq).ShouldNot(BeNil()) 1037 | Expect(lastReq.Host).Should(Equal(proxiedHost)) 1038 | }) 1039 | 1040 | g.It("Should not redirect if MaxRedirects is not set", func() { 1041 | res, err := Request{Uri: ts.URL + "/redirect_test/301", Proxy: ts.URL}.Do() 1042 | Expect(err).ShouldNot(HaveOccurred()) 1043 | Expect(res.StatusCode).Should(Equal(301)) 1044 | }) 1045 | 1046 | g.It("Should use Proxy authentication", func() { 1047 | proxiedHost := "www.google.com" 1048 | uri := strings.Replace(ts.URL, "http://", "http://user:pass@", -1) 1049 | res, err := Request{Uri: "http://" + proxiedHost, Proxy: uri}.Do() 1050 | Expect(err).Should(BeNil()) 1051 | Expect(res.Header.Get("x-forwarded-for")).Should(Equal("test")) 1052 | Expect(lastReq).ShouldNot(BeNil()) 1053 | Expect(lastReq.Header.Get("Proxy-Authorization")).Should(Equal("Basic dXNlcjpwYXNz")) 1054 | }) 1055 | 1056 | g.It("Should propagate cookies", func() { 1057 | proxiedHost, _ := url.Parse("http://www.google.com") 1058 | jar, _ := cookiejar.New(nil) 1059 | res, err := Request{Uri: proxiedHost.String(), Proxy: ts.URL, CookieJar: jar}.Do() 1060 | Expect(err).Should(BeNil()) 1061 | Expect(res.Header.Get("x-forwarded-for")).Should(Equal("test")) 1062 | 1063 | Expect(jar.Cookies(proxiedHost)).Should(HaveLen(1)) 1064 | Expect(jar.Cookies(proxiedHost)[0].Name).Should(Equal("foo")) 1065 | Expect(jar.Cookies(proxiedHost)[0].Value).Should(Equal("bar")) 1066 | }) 1067 | 1068 | g.It("Should use ProxyConnectHeader authentication", func() { 1069 | _, err := Request{Uri: "https://10.255.255.1", 1070 | Proxy: ts.URL, 1071 | Insecure: true, 1072 | }.WithProxyConnectHeader("X-TEST-HEADER", "TEST").Do() 1073 | 1074 | Expect(err).ShouldNot(BeNil()) 1075 | Expect(lastReq.Header.Get("X-TEST-HEADER")).Should(Equal("TEST")) 1076 | }) 1077 | 1078 | }) 1079 | 1080 | g.Describe("BasicAuth", func() { 1081 | var ts *httptest.Server 1082 | 1083 | g.Before(func() { 1084 | ts = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1085 | if r.URL.Path == "/basic_auth" { 1086 | auth_array := r.Header["Authorization"] 1087 | if len(auth_array) > 0 { 1088 | auth := strings.TrimSpace(auth_array[0]) 1089 | w.WriteHeader(200) 1090 | fmt.Fprint(w, auth) 1091 | } else { 1092 | w.WriteHeader(401) 1093 | fmt.Fprint(w, "private") 1094 | } 1095 | } 1096 | })) 1097 | 1098 | }) 1099 | 1100 | g.After(func() { 1101 | ts.Close() 1102 | }) 1103 | 1104 | g.It("Should support basic http authorization", func() { 1105 | res, err := Request{ 1106 | Uri: ts.URL + "/basic_auth", 1107 | BasicAuthUsername: "username", 1108 | BasicAuthPassword: "password", 1109 | }.Do() 1110 | Expect(err).Should(BeNil()) 1111 | str, _ := res.Body.ToString() 1112 | Expect(res.StatusCode).Should(Equal(200)) 1113 | expectedStr := "Basic " + base64.StdEncoding.EncodeToString([]byte("username:password")) 1114 | Expect(str).Should(Equal(expectedStr)) 1115 | }) 1116 | 1117 | g.It("Should fail when basic http authorization is required and not provided", func() { 1118 | res, err := Request{ 1119 | Uri: ts.URL + "/basic_auth", 1120 | }.Do() 1121 | Expect(err).Should(BeNil()) 1122 | str, _ := res.Body.ToString() 1123 | Expect(res.StatusCode).Should(Equal(401)) 1124 | Expect(str).Should(Equal("private")) 1125 | }) 1126 | }) 1127 | }) 1128 | } 1129 | 1130 | func Test_paramParse(t *testing.T) { 1131 | type Form struct { 1132 | A string 1133 | B string 1134 | c string 1135 | } 1136 | 1137 | type AnnotedForm struct { 1138 | Foo string `url:"foo_bar"` 1139 | Baz string `url:"bad,omitempty"` 1140 | Norf string `url:"norf,omitempty"` 1141 | Qux string `url:"-"` 1142 | } 1143 | 1144 | type EmbedForm struct { 1145 | AnnotedForm `url:",squash"` 1146 | Form `url:",squash"` 1147 | Corge string `url:"corge"` 1148 | } 1149 | 1150 | g := Goblin(t) 1151 | RegisterFailHandler(func(m string, _ ...int) { g.Fail(m) }) 1152 | var form = Form{} 1153 | var aform = AnnotedForm{} 1154 | var eform = EmbedForm{} 1155 | var values = url.Values{} 1156 | const result = "a=1&b=2" 1157 | g.Describe("QueryString ParamParse", func() { 1158 | g.Before(func() { 1159 | form.A = "1" 1160 | form.B = "2" 1161 | form.c = "3" 1162 | aform.Foo = "xyz" 1163 | aform.Norf = "abc" 1164 | aform.Qux = "def" 1165 | eform.Form = form 1166 | eform.AnnotedForm = aform 1167 | eform.Corge = "xxx" 1168 | values.Add("a", "1") 1169 | values.Add("b", "2") 1170 | }) 1171 | g.It("Should accept struct and ignores unexported field", func() { 1172 | str, err := paramParse(form) 1173 | Expect(err).Should(BeNil()) 1174 | Expect(str).Should(Equal(result)) 1175 | }) 1176 | g.It("Should accept struct and use the field annotations", func() { 1177 | str, err := paramParse(aform) 1178 | Expect(err).Should(BeNil()) 1179 | Expect(str).Should(Equal("foo_bar=xyz&norf=abc")) 1180 | }) 1181 | g.It("Should accept pointer of struct", func() { 1182 | str, err := paramParse(&form) 1183 | Expect(err).Should(BeNil()) 1184 | Expect(str).Should(Equal(result)) 1185 | }) 1186 | g.It("Should accept recursive pointer of struct", func() { 1187 | f := &form 1188 | ff := &f 1189 | str, err := paramParse(ff) 1190 | Expect(err).Should(BeNil()) 1191 | Expect(str).Should(Equal(result)) 1192 | }) 1193 | g.It("Should accept embedded struct", func() { 1194 | str, err := paramParse(eform) 1195 | Expect(err).Should(BeNil()) 1196 | Expect(str).Should(Equal("a=1&b=2&corge=xxx&foo_bar=xyz&norf=abc")) 1197 | }) 1198 | g.It("Should accept interface{} which forcely converted by struct", func() { 1199 | str, err := paramParse(interface{}(&form)) 1200 | Expect(err).Should(BeNil()) 1201 | Expect(str).Should(Equal(result)) 1202 | }) 1203 | 1204 | g.It("Should accept url.Values", func() { 1205 | str, err := paramParse(values) 1206 | Expect(err).Should(BeNil()) 1207 | Expect(str).Should(Equal(result)) 1208 | }) 1209 | g.It("Should accept &url.Values", func() { 1210 | str, err := paramParse(&values) 1211 | Expect(err).Should(BeNil()) 1212 | Expect(str).Should(Equal(result)) 1213 | }) 1214 | }) 1215 | 1216 | } 1217 | -------------------------------------------------------------------------------- /tags.go: -------------------------------------------------------------------------------- 1 | // Copyright 2011 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found here: https://github.com/golang/go/blob/master/LICENSE 4 | 5 | package goreq 6 | 7 | import ( 8 | "strings" 9 | "unicode" 10 | ) 11 | 12 | // tagOptions is the string following a comma in a struct field's "json" 13 | // tag, or the empty string. It does not include the leading comma. 14 | type tagOptions string 15 | 16 | // parseTag splits a struct field's json tag into its name and 17 | // comma-separated options. 18 | func parseTag(tag string) (string, tagOptions) { 19 | if idx := strings.Index(tag, ","); idx != -1 { 20 | return tag[:idx], tagOptions(tag[idx+1:]) 21 | } 22 | return tag, tagOptions("") 23 | } 24 | 25 | // Contains reports whether a comma-separated list of options 26 | // contains a particular substr flag. substr must be surrounded by a 27 | // string boundary or commas. 28 | func (o tagOptions) Contains(optionName string) bool { 29 | if len(o) == 0 { 30 | return false 31 | } 32 | s := string(o) 33 | for s != "" { 34 | var next string 35 | i := strings.Index(s, ",") 36 | if i >= 0 { 37 | s, next = s[:i], s[i+1:] 38 | } 39 | if s == optionName { 40 | return true 41 | } 42 | s = next 43 | } 44 | return false 45 | } 46 | 47 | func isValidTag(s string) bool { 48 | if s == "" { 49 | return false 50 | } 51 | for _, c := range s { 52 | switch { 53 | case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c): 54 | // Backslash and quote chars are reserved, but 55 | // otherwise any punctuation chars are allowed 56 | // in a tag name. 57 | default: 58 | if !unicode.IsLetter(c) && !unicode.IsDigit(c) { 59 | return false 60 | } 61 | } 62 | } 63 | return true 64 | } 65 | --------------------------------------------------------------------------------