Skip to content

Commit 9485a29

Browse files
committed
transfer and adapt code from internal repo
1 parent fab7f6e commit 9485a29

File tree

7 files changed

+634
-2
lines changed

7 files changed

+634
-2
lines changed

.gitignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Binaries for programs and plugins
2+
*.exe
3+
*.exe~
4+
*.dll
5+
*.so
6+
*.dylib
7+
8+
# Test binary, build with `go test -c`
9+
*.test
10+
11+
# Output of the go coverage tool, specifically when used with LiteIDE
12+
*.out
13+
14+
# External dependencies
15+
vendor
16+
17+
# Debugger files
18+
debug

.travis.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
language: go
2+
3+
go:
4+
- 1.11.x
5+
6+
before_install:
7+
- go get -u github.com/golang/dep/cmd/dep
8+
- dep ensure
9+
- go get -u gopkg.in/alecthomas/gometalinter.v2
10+
- gometalinter.v2 --install
11+
12+
script:
13+
- go test -race -cover ./...
14+
- gometalinter.v2 --vendor --deadline=300s ./...
15+
16+
notifications:
17+
email: false

Gopkg.lock

Lines changed: 54 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Gopkg.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
2+
# for detailed Gopkg.toml documentation.
3+
4+
[[constraint]]
5+
name = "github.com/fastbill/go-httperrors"
6+
version = "^1.0.0"
7+
source = "[email protected]:fastbill/go-httperrors.git"
8+
9+
[[constraint]]
10+
name = "github.com/pkg/errors"
11+
version = "^0.8.0"
12+
13+
[[constraint]]
14+
name = "github.com/stretchr/testify"
15+
version = "^1.3.0"

README.md

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,50 @@
1-
# go-request
2-
Simple but opinionated HTTP request client for Go
1+
# Request [![Build Status](https://travis-ci.com/fastbill/go-request.svg?branch=master)](https://travis-ci.com/fastbill/go-request) [![Go Report Card](https://goreportcard.com/badge/github.com/fastbill/go-request)](https://goreportcard.com/report/github.com/fastbill/go-request) [![GoDoc](https://godoc.org/github.com/fastbill/go-request?status.svg)](https://godoc.org/github.com/fastbill/go-request)
2+
3+
> An opinionated but extremely easy to use HTTP request client for Go to make JSON request and retrieve the results
4+
5+
## Description
6+
With this request package you just need to define the structs that correspond to the JSON request and response body. Together with the parameters like URL, method and headers you can directly execute a request with `Do`. If the request body is not of type `io.Reader` already it will be encoded as JSON. Also the response will be decoded back into the struct you provided for the result. Request and reponse body are optional which means they can be `nil`.
7+
If the request could be made but the response status code was not `2xx` an error of the type `HTTPError` from the package [httperrors](https://github.com/fastbill/go-httperrors) will be returned.
8+
9+
## Example
10+
```go
11+
import "github.com/fastbill/go-request"
12+
13+
type Input struct {
14+
RequestValue string `json:"requestValue"`
15+
}
16+
17+
type Output struct {
18+
ResponseValue string `json:"responseValue"`
19+
}
20+
21+
params := Params{
22+
URL: "https://example.com",
23+
Method: "POST",
24+
Headers: map[string]string{"my-header":"value", "another-header":"value2"}
25+
Body: Input{RequestValue: "someValueIn"},
26+
Query: map[string]string{"key": "value"},
27+
}
28+
29+
result := &Output{}
30+
err := request.Do(params, result)
31+
```
32+
33+
## Convenience wrappers
34+
```go
35+
err := request.Get("http://example.com", result)
36+
37+
err := request.Post("http://example.com", Input{RequestValue: "someValueIn"}, result)
38+
```
39+
40+
## Defaults
41+
* All `2xx` response codes are treated as success, all other codes lead to an error being returned
42+
* If an HTTPError is returned it contains the response body as message if there was one
43+
* The request package takes care of closing the response body after sending the request
44+
* The http client does not follow redirects
45+
* The http client timeout is set to 30 seconds
46+
* `Accept` and `Content-Type` request header are set to `application/json` and can be overwritten via the Headers parameter
47+
48+
## Streaming
49+
The package allows the request body (`Body` property of `Params`) to be of type `io.Reader`. That way you can pass on request bodies to other services without parsing them.
50+

request.go

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
package request
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"io"
7+
"io/ioutil"
8+
"net/http"
9+
"time"
10+
11+
"github.com/fastbill/go-httperrors"
12+
"github.com/pkg/errors"
13+
)
14+
15+
// client is the global client instance.
16+
var client *http.Client
17+
18+
// defaultTimeout is the timeout applied if there is none provided.
19+
var defaultTimeout = 30 * time.Second
20+
21+
// getCachedClient returns the client instance or creates it if it did not exist.
22+
// The client does not follow redirects and has a timeout of defaultTimeout.
23+
func getCachedClient() *http.Client {
24+
if client == nil {
25+
client = GetClient()
26+
}
27+
28+
return client
29+
}
30+
31+
// GetClient returns an http client that does not follow redirects and has a timeout of defaultTimeout.
32+
func GetClient() *http.Client {
33+
return &http.Client{
34+
CheckRedirect: func(req *http.Request, via []*http.Request) error {
35+
return http.ErrUseLastResponse
36+
},
37+
Timeout: defaultTimeout,
38+
}
39+
}
40+
41+
// Params holds all information necessary to set up the request instance.
42+
type Params struct {
43+
Method string
44+
URL string
45+
Headers map[string]string
46+
Body interface{}
47+
Query map[string]string
48+
Timeout time.Duration
49+
}
50+
51+
// Do executes the request as specified in the request params
52+
// The reponse body will be parsed into the provided struct
53+
func Do(params Params, responseBody interface{}) (returnErr error) {
54+
req, err := createRequest(params)
55+
if err != nil {
56+
return err
57+
}
58+
59+
var client *http.Client
60+
if params.Timeout != 0 {
61+
client = GetClient()
62+
client.Timeout = params.Timeout
63+
} else {
64+
client = getCachedClient()
65+
}
66+
67+
res, err := client.Do(req)
68+
if err != nil {
69+
return errors.Wrap(err, "failed to send request")
70+
}
71+
72+
defer func() {
73+
if cErr := res.Body.Close(); cErr != nil && returnErr == nil {
74+
returnErr = cErr
75+
}
76+
}()
77+
78+
if !isSuccessCode(res.StatusCode) {
79+
bodyBytes, err := ioutil.ReadAll(res.Body)
80+
if err != nil || len(bodyBytes) == 0 {
81+
return httperrors.New(res.StatusCode, nil)
82+
}
83+
84+
return httperrors.New(res.StatusCode, string(bodyBytes))
85+
}
86+
87+
if responseBody == nil {
88+
return nil
89+
}
90+
91+
return json.NewDecoder(res.Body).Decode(responseBody)
92+
}
93+
94+
func createRequest(params Params) (*http.Request, error) {
95+
reader, err := convertToReader(params.Body)
96+
if err != nil {
97+
return nil, err
98+
}
99+
100+
req, err := http.NewRequest(params.Method, params.URL, reader)
101+
if err != nil {
102+
return nil, errors.Wrap(err, "failed to create request")
103+
}
104+
req.Header.Set("Accept", "application/json")
105+
req.Header.Set("Content-Type", "application/json")
106+
for key, value := range params.Headers {
107+
req.Header.Set(key, value)
108+
}
109+
110+
if len(params.Query) > 0 {
111+
q := req.URL.Query()
112+
for key, value := range params.Query {
113+
q.Add(key, value)
114+
}
115+
req.URL.RawQuery = q.Encode()
116+
}
117+
118+
return req, nil
119+
}
120+
121+
// Get is a convience wrapper for "Do" to execute GET requests
122+
func Get(url string, responseBody interface{}) error {
123+
return Do(Params{Method: "GET", URL: url}, responseBody)
124+
}
125+
126+
// Post is a convience wrapper for "Do" to execute POST requests
127+
func Post(url string, requestBody interface{}, responseBody interface{}) error {
128+
return Do(Params{Method: "POST", URL: url, Body: requestBody}, responseBody)
129+
}
130+
131+
func isSuccessCode(statusCode int) bool {
132+
return 200 <= statusCode && statusCode <= 299
133+
}
134+
135+
func convertToReader(body interface{}) (io.Reader, error) {
136+
if body == nil {
137+
return nil, nil
138+
}
139+
140+
reader, ok := body.(io.Reader)
141+
if ok {
142+
return reader, nil
143+
}
144+
145+
buffer := &bytes.Buffer{}
146+
err := json.NewEncoder(buffer).Encode(body)
147+
if err != nil {
148+
return nil, errors.Wrap(err, "failed to parse request body to json")
149+
}
150+
151+
return buffer, nil
152+
}

0 commit comments

Comments
 (0)