parent
22cad45ef3
commit
9901266769
@ -1,24 +0,0 @@
|
|||||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
|
||||||
*.o
|
|
||||||
*.a
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Folders
|
|
||||||
_obj
|
|
||||||
_test
|
|
||||||
|
|
||||||
# Architecture specific extensions/prefixes
|
|
||||||
*.[568vq]
|
|
||||||
[568vq].out
|
|
||||||
|
|
||||||
*.cgo1.go
|
|
||||||
*.cgo2.c
|
|
||||||
_cgo_defun.c
|
|
||||||
_cgo_gotypes.go
|
|
||||||
_cgo_export.*
|
|
||||||
|
|
||||||
_testmain.go
|
|
||||||
|
|
||||||
*.exe
|
|
||||||
|
|
||||||
src
|
|
@ -1,8 +0,0 @@
|
|||||||
language: go
|
|
||||||
go:
|
|
||||||
- 1.2
|
|
||||||
- tip
|
|
||||||
notifications:
|
|
||||||
email:
|
|
||||||
- ionathan@gmail.com
|
|
||||||
- marcosnils@gmail.com
|
|
@ -1,20 +0,0 @@
|
|||||||
The MIT License (MIT)
|
|
||||||
|
|
||||||
Copyright (c) 2013 Jonathan Leibiusky and Marcos Lilljedahl
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
this software and associated documentation files (the "Software"), to deal in
|
|
||||||
the Software without restriction, including without limitation the rights to
|
|
||||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -1,3 +0,0 @@
|
|||||||
test:
|
|
||||||
go get -v -d -t ./...
|
|
||||||
go test -v
|
|
@ -1,444 +0,0 @@
|
|||||||
[![Build Status](https://img.shields.io/travis/franela/goreq/master.svg)](https://travis-ci.org/franela/goreq)
|
|
||||||
[![GoDoc](https://godoc.org/github.com/franela/goreq?status.svg)](https://godoc.org/github.com/franela/goreq)
|
|
||||||
|
|
||||||
GoReq
|
|
||||||
=======
|
|
||||||
|
|
||||||
Simple and sane HTTP request library for Go language.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
**Table of Contents**
|
|
||||||
|
|
||||||
- [Why GoReq?](#user-content-why-goreq)
|
|
||||||
- [How do I install it?](#user-content-how-do-i-install-it)
|
|
||||||
- [What can I do with it?](#user-content-what-can-i-do-with-it)
|
|
||||||
- [Making requests with different methods](#user-content-making-requests-with-different-methods)
|
|
||||||
- [GET](#user-content-get)
|
|
||||||
- [Tags](#user-content-tags)
|
|
||||||
- [POST](#user-content-post)
|
|
||||||
- [Sending payloads in the Body](#user-content-sending-payloads-in-the-body)
|
|
||||||
- [Specifiying request headers](#user-content-specifiying-request-headers)
|
|
||||||
- [Sending Cookies](#cookie-support)
|
|
||||||
- [Setting timeouts](#user-content-setting-timeouts)
|
|
||||||
- [Using the Response and Error](#user-content-using-the-response-and-error)
|
|
||||||
- [Receiving JSON](#user-content-receiving-json)
|
|
||||||
- [Sending/Receiving Compressed Payloads](#user-content-sendingreceiving-compressed-payloads)
|
|
||||||
- [Using gzip compression:](#user-content-using-gzip-compression)
|
|
||||||
- [Using deflate compression:](#user-content-using-deflate-compression)
|
|
||||||
- [Using compressed responses:](#user-content-using-compressed-responses)
|
|
||||||
- [Proxy](#proxy)
|
|
||||||
- [Debugging requests](#debug)
|
|
||||||
- [Getting raw Request & Response](#getting-raw-request--response)
|
|
||||||
- [TODO:](#user-content-todo)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Why GoReq?
|
|
||||||
==========
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
How do I install it?
|
|
||||||
====================
|
|
||||||
|
|
||||||
```bash
|
|
||||||
go get github.com/franela/goreq
|
|
||||||
```
|
|
||||||
|
|
||||||
What can I do with it?
|
|
||||||
======================
|
|
||||||
|
|
||||||
## Making requests with different methods
|
|
||||||
|
|
||||||
#### GET
|
|
||||||
```go
|
|
||||||
res, err := goreq.Request{ Uri: "http://www.google.com" }.Do()
|
|
||||||
```
|
|
||||||
|
|
||||||
GoReq default method is GET.
|
|
||||||
|
|
||||||
You can also set value to GET method easily
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Item struct {
|
|
||||||
Limit int
|
|
||||||
Skip int
|
|
||||||
Fields string
|
|
||||||
}
|
|
||||||
|
|
||||||
item := Item {
|
|
||||||
Limit: 3,
|
|
||||||
Skip: 5,
|
|
||||||
Fields: "Value",
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := goreq.Request{
|
|
||||||
Uri: "http://localhost:3000/",
|
|
||||||
QueryString: item,
|
|
||||||
}.Do()
|
|
||||||
```
|
|
||||||
The sample above will send `http://localhost:3000/?limit=3&skip=5&fields=Value`
|
|
||||||
|
|
||||||
Alternatively the `url` tag can be used in struct fields to customize encoding properties
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Item struct {
|
|
||||||
TheLimit int `url:"the_limit"`
|
|
||||||
TheSkip string `url:"the_skip,omitempty"`
|
|
||||||
TheFields string `url:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
item := Item {
|
|
||||||
TheLimit: 3,
|
|
||||||
TheSkip: "",
|
|
||||||
TheFields: "Value",
|
|
||||||
}
|
|
||||||
|
|
||||||
res, err := goreq.Request{
|
|
||||||
Uri: "http://localhost:3000/",
|
|
||||||
QueryString: item,
|
|
||||||
}.Do()
|
|
||||||
```
|
|
||||||
The sample above will send `http://localhost:3000/?the_limit=3`
|
|
||||||
|
|
||||||
|
|
||||||
QueryString also support url.Values
|
|
||||||
|
|
||||||
```go
|
|
||||||
item := url.Values{}
|
|
||||||
item.Set("Limit", 3)
|
|
||||||
item.Add("Field", "somefield")
|
|
||||||
item.Add("Field", "someotherfield")
|
|
||||||
|
|
||||||
res, err := goreq.Request{
|
|
||||||
Uri: "http://localhost:3000/",
|
|
||||||
QueryString: item,
|
|
||||||
}.Do()
|
|
||||||
```
|
|
||||||
|
|
||||||
The sample above will send `http://localhost:3000/?limit=3&field=somefield&field=someotherfield`
|
|
||||||
|
|
||||||
### Tags
|
|
||||||
|
|
||||||
Struct field `url` tag is mainly used as the request parameter name.
|
|
||||||
Tags can be comma separated multiple values, 1st value is for naming and rest has special meanings.
|
|
||||||
|
|
||||||
- special tag for 1st value
|
|
||||||
- `-`: value is ignored if set this
|
|
||||||
|
|
||||||
- special tag for rest 2nd value
|
|
||||||
- `omitempty`: zero-value is ignored if set this
|
|
||||||
- `squash`: the fields of embedded struct is used for parameter
|
|
||||||
|
|
||||||
#### Tag Examples
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Place struct {
|
|
||||||
Country string `url:"country"`
|
|
||||||
City string `url:"city"`
|
|
||||||
ZipCode string `url:"zipcode,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Person struct {
|
|
||||||
Place `url:",squash"`
|
|
||||||
|
|
||||||
FirstName string `url:"first_name"`
|
|
||||||
LastName string `url:"last_name"`
|
|
||||||
Age string `url:"age,omitempty"`
|
|
||||||
Password string `url:"-"`
|
|
||||||
}
|
|
||||||
|
|
||||||
johnbull := Person{
|
|
||||||
Place: Place{ // squash the embedded struct value
|
|
||||||
Country: "UK",
|
|
||||||
City: "London",
|
|
||||||
ZipCode: "SW1",
|
|
||||||
},
|
|
||||||
FirstName: "John",
|
|
||||||
LastName: "Doe",
|
|
||||||
Age: "35",
|
|
||||||
Password: "my-secret", // ignored for parameter
|
|
||||||
}
|
|
||||||
|
|
||||||
goreq.Request{
|
|
||||||
Uri: "http://localhost/",
|
|
||||||
QueryString: johnbull,
|
|
||||||
}.Do()
|
|
||||||
// => `http://localhost/?first_name=John&last_name=Doe&age=35&country=UK&city=London&zip_code=SW1`
|
|
||||||
|
|
||||||
|
|
||||||
// age and zipcode will be ignored because of `omitempty`
|
|
||||||
// but firstname isn't.
|
|
||||||
samurai := Person{
|
|
||||||
Place: Place{ // squash the embedded struct value
|
|
||||||
Country: "Japan",
|
|
||||||
City: "Tokyo",
|
|
||||||
},
|
|
||||||
LastName: "Yagyu",
|
|
||||||
}
|
|
||||||
|
|
||||||
goreq.Request{
|
|
||||||
Uri: "http://localhost/",
|
|
||||||
QueryString: samurai,
|
|
||||||
}.Do()
|
|
||||||
// => `http://localhost/?first_name=&last_name=yagyu&country=Japan&city=Tokyo`
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
#### POST
|
|
||||||
|
|
||||||
```go
|
|
||||||
res, err := goreq.Request{ Method: "POST", Uri: "http://www.google.com" }.Do()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sending payloads in the Body
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Item struct {
|
|
||||||
Id int
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
item := Item{ Id: 1111, Name: "foobar" }
|
|
||||||
|
|
||||||
res, err := goreq.Request{
|
|
||||||
Method: "POST",
|
|
||||||
Uri: "http://www.google.com",
|
|
||||||
Body: item,
|
|
||||||
}.Do()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Specifiying request headers
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
```go
|
|
||||||
res, err := goreq.Request{
|
|
||||||
Uri: "http://www.google.com",
|
|
||||||
Host: "foobar.com",
|
|
||||||
Accept: "application/json",
|
|
||||||
ContentType: "application/json",
|
|
||||||
UserAgent: "goreq",
|
|
||||||
}.Do()
|
|
||||||
```
|
|
||||||
|
|
||||||
But sometimes you need to set other headers. You can still do it.
|
|
||||||
|
|
||||||
```go
|
|
||||||
req := goreq.Request{ Uri: "http://www.google.com" }
|
|
||||||
|
|
||||||
req.AddHeader("X-Custom", "somevalue")
|
|
||||||
|
|
||||||
req.Do()
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively you can use the `WithHeader` function to keep the syntax short
|
|
||||||
|
|
||||||
```go
|
|
||||||
res, err = goreq.Request{ Uri: "http://www.google.com" }.WithHeader("X-Custom", "somevalue").Do()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Cookie support
|
|
||||||
|
|
||||||
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
|
|
||||||
or you can use goreq's one-liner WithCookie method as shown below
|
|
||||||
|
|
||||||
```go
|
|
||||||
res, err := goreq.Request{
|
|
||||||
Uri: "http://www.google.com",
|
|
||||||
}.
|
|
||||||
WithCookie(&http.Cookie{Name: "c1", Value: "v1"}).
|
|
||||||
Do()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Setting timeouts
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
You can change the connection timeout doing:
|
|
||||||
|
|
||||||
```go
|
|
||||||
goreq.SetConnectTimeout(100 * time.Millisecond)
|
|
||||||
```
|
|
||||||
|
|
||||||
And specify the request timeout doing:
|
|
||||||
|
|
||||||
```go
|
|
||||||
res, err := goreq.Request{
|
|
||||||
Uri: "http://www.google.com",
|
|
||||||
Timeout: 500 * time.Millisecond,
|
|
||||||
}.Do()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Using the Response and Error
|
|
||||||
|
|
||||||
GoReq will always return 2 values: a ```Response``` and an ```Error```.
|
|
||||||
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.
|
|
||||||
You can check what happened by getting the error message:
|
|
||||||
|
|
||||||
```go
|
|
||||||
fmt.Println(err.Error())
|
|
||||||
```
|
|
||||||
And to make it easy to know if it was a timeout error, you can ask the error or return it:
|
|
||||||
|
|
||||||
```go
|
|
||||||
if serr, ok := err.(*goreq.Error); ok {
|
|
||||||
if serr.Timeout() {
|
|
||||||
...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
```
|
|
||||||
|
|
||||||
If you don't get an error, you can safely use the ```Response```.
|
|
||||||
|
|
||||||
```go
|
|
||||||
res.Uri // return final URL location of the response (fulfilled after redirect was made)
|
|
||||||
res.StatusCode // return the status code of the response
|
|
||||||
res.Body // gives you access to the body
|
|
||||||
res.Body.ToString() // will return the body as a string
|
|
||||||
res.Header.Get("Content-Type") // gives you access to all the response headers
|
|
||||||
```
|
|
||||||
Remember that you should **always** close `res.Body` if it's not `nil`
|
|
||||||
|
|
||||||
## Receiving JSON
|
|
||||||
|
|
||||||
GoReq will help you to receive and unmarshal JSON.
|
|
||||||
|
|
||||||
```go
|
|
||||||
type Item struct {
|
|
||||||
Id int
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
|
|
||||||
var item Item
|
|
||||||
|
|
||||||
res.Body.FromJsonTo(&item)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Sending/Receiving Compressed Payloads
|
|
||||||
GoReq supports gzip, deflate and zlib compression of requests' body and transparent decompression of responses provided they have a correct `Content-Encoding` header.
|
|
||||||
|
|
||||||
#####Using gzip compression:
|
|
||||||
```go
|
|
||||||
res, err := goreq.Request{
|
|
||||||
Method: "POST",
|
|
||||||
Uri: "http://www.google.com",
|
|
||||||
Body: item,
|
|
||||||
Compression: goreq.Gzip(),
|
|
||||||
}.Do()
|
|
||||||
```
|
|
||||||
#####Using deflate/zlib compression:
|
|
||||||
```go
|
|
||||||
res, err := goreq.Request{
|
|
||||||
Method: "POST",
|
|
||||||
Uri: "http://www.google.com",
|
|
||||||
Body: item,
|
|
||||||
Compression: goreq.Deflate(),
|
|
||||||
}.Do()
|
|
||||||
```
|
|
||||||
#####Using compressed responses:
|
|
||||||
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:
|
|
||||||
```go
|
|
||||||
type Item struct {
|
|
||||||
Id int
|
|
||||||
Name string
|
|
||||||
}
|
|
||||||
res, err := goreq.Request{
|
|
||||||
Method: "POST",
|
|
||||||
Uri: "http://www.google.com",
|
|
||||||
Body: item,
|
|
||||||
Compression: goreq.Gzip(),
|
|
||||||
}.Do()
|
|
||||||
var item Item
|
|
||||||
res.Body.FromJsonTo(&item)
|
|
||||||
```
|
|
||||||
If no `Content-Encoding` header is replied by the server GoReq will return the crude response.
|
|
||||||
|
|
||||||
## Proxy
|
|
||||||
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
|
|
||||||
|
|
||||||
```go
|
|
||||||
res, err := goreq.Request{
|
|
||||||
Method: "GET",
|
|
||||||
Proxy: "http://myproxy:myproxyport",
|
|
||||||
Uri: "http://www.google.com",
|
|
||||||
}.Do()
|
|
||||||
```
|
|
||||||
|
|
||||||
### Proxy basic auth is also supported
|
|
||||||
|
|
||||||
```go
|
|
||||||
res, err := goreq.Request{
|
|
||||||
Method: "GET",
|
|
||||||
Proxy: "http://user:pass@myproxy:myproxyport",
|
|
||||||
Uri: "http://www.google.com",
|
|
||||||
}.Do()
|
|
||||||
```
|
|
||||||
|
|
||||||
## Debug
|
|
||||||
If you need to debug your http requests, it can print the http request detail.
|
|
||||||
|
|
||||||
```go
|
|
||||||
res, err := goreq.Request{
|
|
||||||
Method: "GET",
|
|
||||||
Uri: "http://www.google.com",
|
|
||||||
Compression: goreq.Gzip(),
|
|
||||||
ShowDebug: true,
|
|
||||||
}.Do()
|
|
||||||
fmt.Println(res, err)
|
|
||||||
```
|
|
||||||
|
|
||||||
and it will print the log:
|
|
||||||
```
|
|
||||||
GET / HTTP/1.1
|
|
||||||
Host: www.google.com
|
|
||||||
Accept:
|
|
||||||
Accept-Encoding: gzip
|
|
||||||
Content-Encoding: gzip
|
|
||||||
Content-Type:
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Getting raw Request & Response
|
|
||||||
|
|
||||||
To get the Request:
|
|
||||||
|
|
||||||
```go
|
|
||||||
req := goreq.Request{
|
|
||||||
Host: "foobar.com",
|
|
||||||
}
|
|
||||||
|
|
||||||
//req.Request will return a new instance of an http.Request so you can safely use it for something else
|
|
||||||
request, _ := req.NewRequest()
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
To get the Response:
|
|
||||||
|
|
||||||
```go
|
|
||||||
res, err := goreq.Request{
|
|
||||||
Method: "GET",
|
|
||||||
Uri: "http://www.google.com",
|
|
||||||
Compression: goreq.Gzip(),
|
|
||||||
ShowDebug: true,
|
|
||||||
}.Do()
|
|
||||||
|
|
||||||
// res.Response will contain the original http.Response structure
|
|
||||||
fmt.Println(res.Response, err)
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
TODO:
|
|
||||||
-----
|
|
||||||
|
|
||||||
We do have a couple of [issues](https://github.com/franela/goreq/issues) pending we'll be addressing soon. But feel free to
|
|
||||||
contribute and send us PRs (with tests please :smile:).
|
|
@ -1,494 +0,0 @@
|
|||||||
package goreq
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"bytes"
|
|
||||||
"compress/gzip"
|
|
||||||
"compress/zlib"
|
|
||||||
"crypto/tls"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httputil"
|
|
||||||
"net/url"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Request struct {
|
|
||||||
headers []headerTuple
|
|
||||||
cookies []*http.Cookie
|
|
||||||
Method string
|
|
||||||
Uri string
|
|
||||||
Body interface{}
|
|
||||||
QueryString interface{}
|
|
||||||
Timeout time.Duration
|
|
||||||
ContentType string
|
|
||||||
Accept string
|
|
||||||
Host string
|
|
||||||
UserAgent string
|
|
||||||
Insecure bool
|
|
||||||
MaxRedirects int
|
|
||||||
RedirectHeaders bool
|
|
||||||
Proxy string
|
|
||||||
Compression *compression
|
|
||||||
BasicAuthUsername string
|
|
||||||
BasicAuthPassword string
|
|
||||||
CookieJar http.CookieJar
|
|
||||||
ShowDebug bool
|
|
||||||
OnBeforeRequest func(goreq *Request, httpreq *http.Request)
|
|
||||||
}
|
|
||||||
|
|
||||||
type compression struct {
|
|
||||||
writer func(buffer io.Writer) (io.WriteCloser, error)
|
|
||||||
reader func(buffer io.Reader) (io.ReadCloser, error)
|
|
||||||
ContentEncoding string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Response struct {
|
|
||||||
*http.Response
|
|
||||||
Uri string
|
|
||||||
Body *Body
|
|
||||||
req *http.Request
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r Response) CancelRequest() {
|
|
||||||
cancelRequest(r.req)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func cancelRequest(r *http.Request) {
|
|
||||||
if transport, ok := DefaultTransport.(transportRequestCanceler); ok {
|
|
||||||
transport.CancelRequest(r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type headerTuple struct {
|
|
||||||
name string
|
|
||||||
value string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Body struct {
|
|
||||||
reader io.ReadCloser
|
|
||||||
compressedReader io.ReadCloser
|
|
||||||
}
|
|
||||||
|
|
||||||
type Error struct {
|
|
||||||
timeout bool
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
type transportRequestCanceler interface {
|
|
||||||
CancelRequest(*http.Request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Error) Timeout() bool {
|
|
||||||
return e.timeout
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Error) Error() string {
|
|
||||||
return e.Err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Body) Read(p []byte) (int, error) {
|
|
||||||
if b.compressedReader != nil {
|
|
||||||
return b.compressedReader.Read(p)
|
|
||||||
}
|
|
||||||
return b.reader.Read(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Body) Close() error {
|
|
||||||
err := b.reader.Close()
|
|
||||||
if b.compressedReader != nil {
|
|
||||||
return b.compressedReader.Close()
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Body) FromJsonTo(o interface{}) error {
|
|
||||||
return json.NewDecoder(b).Decode(o)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *Body) ToString() (string, error) {
|
|
||||||
body, err := ioutil.ReadAll(b)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(body), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func Gzip() *compression {
|
|
||||||
reader := func(buffer io.Reader) (io.ReadCloser, error) {
|
|
||||||
return gzip.NewReader(buffer)
|
|
||||||
}
|
|
||||||
writer := func(buffer io.Writer) (io.WriteCloser, error) {
|
|
||||||
return gzip.NewWriter(buffer), nil
|
|
||||||
}
|
|
||||||
return &compression{writer: writer, reader: reader, ContentEncoding: "gzip"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Deflate() *compression {
|
|
||||||
reader := func(buffer io.Reader) (io.ReadCloser, error) {
|
|
||||||
return zlib.NewReader(buffer)
|
|
||||||
}
|
|
||||||
writer := func(buffer io.Writer) (io.WriteCloser, error) {
|
|
||||||
return zlib.NewWriter(buffer), nil
|
|
||||||
}
|
|
||||||
return &compression{writer: writer, reader: reader, ContentEncoding: "deflate"}
|
|
||||||
}
|
|
||||||
|
|
||||||
func Zlib() *compression {
|
|
||||||
return Deflate()
|
|
||||||
}
|
|
||||||
|
|
||||||
func paramParse(query interface{}) (string, error) {
|
|
||||||
switch query.(type) {
|
|
||||||
case url.Values:
|
|
||||||
return query.(url.Values).Encode(), nil
|
|
||||||
case *url.Values:
|
|
||||||
return query.(*url.Values).Encode(), nil
|
|
||||||
default:
|
|
||||||
var v = &url.Values{}
|
|
||||||
err := paramParseStruct(v, query)
|
|
||||||
return v.Encode(), err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func paramParseStruct(v *url.Values, query interface{}) error {
|
|
||||||
var (
|
|
||||||
s = reflect.ValueOf(query)
|
|
||||||
t = reflect.TypeOf(query)
|
|
||||||
)
|
|
||||||
for t.Kind() == reflect.Ptr || t.Kind() == reflect.Interface {
|
|
||||||
s = s.Elem()
|
|
||||||
t = s.Type()
|
|
||||||
}
|
|
||||||
if t.Kind() != reflect.Struct {
|
|
||||||
return errors.New("Can not parse QueryString.")
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i < t.NumField(); i++ {
|
|
||||||
var name string
|
|
||||||
|
|
||||||
field := s.Field(i)
|
|
||||||
typeField := t.Field(i)
|
|
||||||
|
|
||||||
if !field.CanInterface() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
urlTag := typeField.Tag.Get("url")
|
|
||||||
if urlTag == "-" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
name, opts := parseTag(urlTag)
|
|
||||||
|
|
||||||
var omitEmpty, squash bool
|
|
||||||
omitEmpty = opts.Contains("omitempty")
|
|
||||||
squash = opts.Contains("squash")
|
|
||||||
|
|
||||||
if squash {
|
|
||||||
err := paramParseStruct(v, field.Interface())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if urlTag == "" {
|
|
||||||
name = strings.ToLower(typeField.Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
if val := fmt.Sprintf("%v", field.Interface()); !(omitEmpty && len(val) == 0) {
|
|
||||||
v.Add(name, val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func prepareRequestBody(b interface{}) (io.Reader, error) {
|
|
||||||
switch b.(type) {
|
|
||||||
case string:
|
|
||||||
// treat is as text
|
|
||||||
return strings.NewReader(b.(string)), nil
|
|
||||||
case io.Reader:
|
|
||||||
// treat is as text
|
|
||||||
return b.(io.Reader), nil
|
|
||||||
case []byte:
|
|
||||||
//treat as byte array
|
|
||||||
return bytes.NewReader(b.([]byte)), nil
|
|
||||||
case nil:
|
|
||||||
return nil, nil
|
|
||||||
default:
|
|
||||||
// try to jsonify it
|
|
||||||
j, err := json.Marshal(b)
|
|
||||||
if err == nil {
|
|
||||||
return bytes.NewReader(j), nil
|
|
||||||
}
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var DefaultDialer = &net.Dialer{Timeout: 1000 * time.Millisecond}
|
|
||||||
var DefaultTransport http.RoundTripper = &http.Transport{Dial: DefaultDialer.Dial, Proxy: http.ProxyFromEnvironment}
|
|
||||||
var DefaultClient = &http.Client{Transport: DefaultTransport}
|
|
||||||
|
|
||||||
var proxyTransport http.RoundTripper
|
|
||||||
var proxyClient *http.Client
|
|
||||||
|
|
||||||
func SetConnectTimeout(duration time.Duration) {
|
|
||||||
DefaultDialer.Timeout = duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Request) AddHeader(name string, value string) {
|
|
||||||
if r.headers == nil {
|
|
||||||
r.headers = []headerTuple{}
|
|
||||||
}
|
|
||||||
r.headers = append(r.headers, headerTuple{name: name, value: value})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r Request) WithHeader(name string, value string) Request {
|
|
||||||
r.AddHeader(name, value)
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Request) AddCookie(c *http.Cookie) {
|
|
||||||
r.cookies = append(r.cookies, c)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r Request) WithCookie(c *http.Cookie) Request {
|
|
||||||
r.AddCookie(c)
|
|
||||||
return r
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r Request) Do() (*Response, error) {
|
|
||||||
var client = DefaultClient
|
|
||||||
var transport = DefaultTransport
|
|
||||||
var resUri string
|
|
||||||
var redirectFailed bool
|
|
||||||
|
|
||||||
r.Method = valueOrDefault(r.Method, "GET")
|
|
||||||
|
|
||||||
// use a client with a cookie jar if necessary. We create a new client not
|
|
||||||
// to modify the default one.
|
|
||||||
if r.CookieJar != nil {
|
|
||||||
client = &http.Client{
|
|
||||||
Transport: transport,
|
|
||||||
Jar: r.CookieJar,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Proxy != "" {
|
|
||||||
proxyUrl, err := url.Parse(r.Proxy)
|
|
||||||
if err != nil {
|
|
||||||
// proxy address is in a wrong format
|
|
||||||
return nil, &Error{Err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
//If jar is specified new client needs to be built
|
|
||||||
if proxyTransport == nil || client.Jar != nil {
|
|
||||||
proxyTransport = &http.Transport{Dial: DefaultDialer.Dial, Proxy: http.ProxyURL(proxyUrl)}
|
|
||||||
proxyClient = &http.Client{Transport: proxyTransport, Jar: client.Jar}
|
|
||||||
} else if proxyTransport, ok := proxyTransport.(*http.Transport); ok {
|
|
||||||
proxyTransport.Proxy = http.ProxyURL(proxyUrl)
|
|
||||||
}
|
|
||||||
transport = proxyTransport
|
|
||||||
client = proxyClient
|
|
||||||
}
|
|
||||||
|
|
||||||
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
|
||||||
|
|
||||||
if len(via) > r.MaxRedirects {
|
|
||||||
redirectFailed = true
|
|
||||||
return errors.New("Error redirecting. MaxRedirects reached")
|
|
||||||
}
|
|
||||||
|
|
||||||
resUri = req.URL.String()
|
|
||||||
|
|
||||||
//By default Golang will not redirect request headers
|
|
||||||
// https://code.google.com/p/go/issues/detail?id=4800&q=request%20header
|
|
||||||
if r.RedirectHeaders {
|
|
||||||
for key, val := range via[0].Header {
|
|
||||||
req.Header[key] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if transport, ok := transport.(*http.Transport); ok {
|
|
||||||
if r.Insecure {
|
|
||||||
if transport.TLSClientConfig != nil {
|
|
||||||
transport.TLSClientConfig.InsecureSkipVerify = true
|
|
||||||
} else {
|
|
||||||
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
|
|
||||||
}
|
|
||||||
} else if transport.TLSClientConfig != nil {
|
|
||||||
// the default TLS client (when transport.TLSClientConfig==nil) is
|
|
||||||
// already set to verify, so do nothing in that case
|
|
||||||
transport.TLSClientConfig.InsecureSkipVerify = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := r.NewRequest()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
// we couldn't parse the URL.
|
|
||||||
return nil, &Error{Err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
timeout := false
|
|
||||||
var timer *time.Timer
|
|
||||||
if r.Timeout > 0 {
|
|
||||||
timer = time.AfterFunc(r.Timeout, func() {
|
|
||||||
cancelRequest(req)
|
|
||||||
timeout = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.ShowDebug {
|
|
||||||
dump, err := httputil.DumpRequest(req, true)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
log.Println(string(dump))
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.OnBeforeRequest != nil {
|
|
||||||
r.OnBeforeRequest(&r, req)
|
|
||||||
}
|
|
||||||
res, err := client.Do(req)
|
|
||||||
if timer != nil {
|
|
||||||
timer.Stop()
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if !timeout {
|
|
||||||
switch err := err.(type) {
|
|
||||||
case *net.OpError:
|
|
||||||
timeout = err.Timeout()
|
|
||||||
case *url.Error:
|
|
||||||
if op, ok := err.Err.(*net.OpError); ok {
|
|
||||||
timeout = op.Timeout()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var response *Response
|
|
||||||
//If redirect fails we still want to return response data
|
|
||||||
if redirectFailed {
|
|
||||||
if res != nil {
|
|
||||||
response = &Response{res, resUri, &Body{reader: res.Body}, req}
|
|
||||||
} else {
|
|
||||||
response = &Response{res, resUri, nil, req}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//If redirect fails and we haven't set a redirect count we shouldn't return an error
|
|
||||||
if redirectFailed && r.MaxRedirects == 0 {
|
|
||||||
return response, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return response, &Error{timeout: timeout, Err: err}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.Compression != nil && strings.Contains(res.Header.Get("Content-Encoding"), r.Compression.ContentEncoding) {
|
|
||||||
compressedReader, err := r.Compression.reader(res.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &Error{Err: err}
|
|
||||||
}
|
|
||||||
return &Response{res, resUri, &Body{reader: res.Body, compressedReader: compressedReader}, req}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Response{res, resUri, &Body{reader: res.Body}, req}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r Request) addHeaders(headersMap http.Header) {
|
|
||||||
if len(r.UserAgent) > 0 {
|
|
||||||
headersMap.Add("User-Agent", r.UserAgent)
|
|
||||||
}
|
|
||||||
if r.Accept != "" {
|
|
||||||
headersMap.Add("Accept", r.Accept)
|
|
||||||
}
|
|
||||||
if r.ContentType != "" {
|
|
||||||
headersMap.Add("Content-Type", r.ContentType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r Request) NewRequest() (*http.Request, error) {
|
|
||||||
|
|
||||||
b, e := prepareRequestBody(r.Body)
|
|
||||||
if e != nil {
|
|
||||||
// there was a problem marshaling the body
|
|
||||||
return nil, &Error{Err: e}
|
|
||||||
}
|
|
||||||
|
|
||||||
if r.QueryString != nil {
|
|
||||||
param, e := paramParse(r.QueryString)
|
|
||||||
if e != nil {
|
|
||||||
return nil, &Error{Err: e}
|
|
||||||
}
|
|
||||||
r.Uri = r.Uri + "?" + param
|
|
||||||
}
|
|
||||||
|
|
||||||
var bodyReader io.Reader
|
|
||||||
if b != nil && r.Compression != nil {
|
|
||||||
buffer := bytes.NewBuffer([]byte{})
|
|
||||||
readBuffer := bufio.NewReader(b)
|
|
||||||
writer, err := r.Compression.writer(buffer)
|
|
||||||
if err != nil {
|
|
||||||
return nil, &Error{Err: err}
|
|
||||||
}
|
|
||||||
_, e = readBuffer.WriteTo(writer)
|
|
||||||
writer.Close()
|
|
||||||
if e != nil {
|
|
||||||
return nil, &Error{Err: e}
|
|
||||||
}
|
|
||||||
bodyReader = buffer
|
|
||||||
} else {
|
|
||||||
bodyReader = b
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequest(r.Method, r.Uri, bodyReader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// add headers to the request
|
|
||||||
req.Host = r.Host
|
|
||||||
|
|
||||||
r.addHeaders(req.Header)
|
|
||||||
if r.Compression != nil {
|
|
||||||
req.Header.Add("Content-Encoding", r.Compression.ContentEncoding)
|
|
||||||
req.Header.Add("Accept-Encoding", r.Compression.ContentEncoding)
|
|
||||||
}
|
|
||||||
if r.headers != nil {
|
|
||||||
for _, header := range r.headers {
|
|
||||||
req.Header.Add(header.name, header.value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//use basic auth if required
|
|
||||||
if r.BasicAuthUsername != "" {
|
|
||||||
req.SetBasicAuth(r.BasicAuthUsername, r.BasicAuthPassword)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, c := range r.cookies {
|
|
||||||
req.AddCookie(c)
|
|
||||||
}
|
|
||||||
return req, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return value if nonempty, def otherwise.
|
|
||||||
func valueOrDefault(value, def string) string {
|
|
||||||
if value != "" {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
return def
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,64 +0,0 @@
|
|||||||
// Copyright 2011 The Go Authors. All rights reserved.
|
|
||||||
// Use of this source code is governed by a BSD-style
|
|
||||||
// license that can be found in the LICENSE file.
|
|
||||||
|
|
||||||
package goreq
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
"unicode"
|
|
||||||
)
|
|
||||||
|
|
||||||
// tagOptions is the string following a comma in a struct field's "json"
|
|
||||||
// tag, or the empty string. It does not include the leading comma.
|
|
||||||
type tagOptions string
|
|
||||||
|
|
||||||
// parseTag splits a struct field's json tag into its name and
|
|
||||||
// comma-separated options.
|
|
||||||
func parseTag(tag string) (string, tagOptions) {
|
|
||||||
if idx := strings.Index(tag, ","); idx != -1 {
|
|
||||||
return tag[:idx], tagOptions(tag[idx+1:])
|
|
||||||
}
|
|
||||||
return tag, tagOptions("")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Contains reports whether a comma-separated list of options
|
|
||||||
// contains a particular substr flag. substr must be surrounded by a
|
|
||||||
// string boundary or commas.
|
|
||||||
func (o tagOptions) Contains(optionName string) bool {
|
|
||||||
if len(o) == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
s := string(o)
|
|
||||||
for s != "" {
|
|
||||||
var next string
|
|
||||||
i := strings.Index(s, ",")
|
|
||||||
if i >= 0 {
|
|
||||||
s, next = s[:i], s[i+1:]
|
|
||||||
}
|
|
||||||
if s == optionName {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
s = next
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isValidTag(s string) bool {
|
|
||||||
if s == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
for _, c := range s {
|
|
||||||
switch {
|
|
||||||
case strings.ContainsRune("!#$%&()*+-./:<=>?@[]^_{|}~ ", c):
|
|
||||||
// Backslash and quote chars are reserved, but
|
|
||||||
// otherwise any punctuation chars are allowed
|
|
||||||
// in a tag name.
|
|
||||||
default:
|
|
||||||
if !unicode.IsLetter(c) && !unicode.IsDigit(c) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
Loading…
Reference in new issue