Skip to content

Commit 3495ab8

Browse files
authored
Merge pull request #4 from fastbill/string-response-and-expected-status
add DoWithStringResponse function and ExpectedResponseCode parameter
2 parents c7a31a1 + 037c263 commit 3495ab8

File tree

5 files changed

+192
-57
lines changed

5 files changed

+192
-57
lines changed

.golangci.yml

+13
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,28 @@ linters:
1111

1212
run:
1313
deadline: 10m
14+
modules-download-mode: vendor
1415

1516
issues:
17+
exclude-use-default: false
1618
exclude-rules:
1719
- path: _test\.go
1820
linters:
1921
- dupl
2022
- goconst
2123
- gosec
24+
- linters:
25+
- govet
26+
text: 'shadow: declaration of "err" shadows declaration'
27+
- linters:
28+
- golint
29+
text: 'in another file for this package'
2230

2331
linters-settings:
2432
gocyclo:
2533
min-complexity: 10
34+
golint:
35+
min-confidence: 0
36+
govet:
37+
check-shadowing: true
38+

README.md

+18-6
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@ If the request could be made but the response status code was not `2xx` an error
88

99
## Example
1010
```go
11-
import "github.com/fastbill/go-request/v2"
11+
import (
12+
"net/http"
13+
"github.com/fastbill/go-request/v2"
14+
)
1215

1316
type Input struct {
1417
RequestValue string `json:"requestValue"`
@@ -20,15 +23,23 @@ type Output struct {
2023

2124
params := request.Params{
2225
URL: "https://example.com",
23-
Method: "POST",
26+
Method: http.MethodPost,
2427
Headers: map[string]string{"my-header":"value", "another-header":"value2"},
2528
Body: Input{RequestValue: "someValueIn"},
2629
Query: map[string]string{"key": "value"},
30+
Timeout: 10 * time.Second,
31+
ExpectedResponseCode: 201,
2732
}
2833

2934
result := &Output{}
3035
err := request.Do(params, result)
3136
```
37+
All parameters besides the `URL` and the `Method` are optional and can be omitted.
38+
39+
If you want to retrieve the response body as a string, e.g. for debugging or testing purposes, you can use `DoWithStringResponse` instead.
40+
```go
41+
result, err := request.DoWithStringResponse(params)
42+
```
3243

3344
## Convenience wrappers
3445
```go
@@ -38,11 +49,11 @@ err := request.Post("http://example.com", Input{RequestValue: "someValueIn"}, re
3849
```
3950

4051
## Defaults
41-
* All `2xx` response codes are treated as success, all other codes lead to an error being returned
52+
* All `2xx` response codes are treated as success, all other codes lead to an error being returned, if you want to check for a specific response code set `ExpectedResponseCode` in the parameters
4253
* If an HTTPError is returned it contains the response body as message if there was one
4354
* The request package takes care of closing the response body after sending the request
4455
* The http client does not follow redirects
45-
* The http client timeout is set to 30 seconds
56+
* The http client timeout is set to 30 seconds, use the `Timeout` parameter in case you want to define a different timeout for one of the requests
4657
* `Accept` and `Content-Type` request header are set to `application/json` and can be overwritten via the Headers parameter
4758

4859
## Streaming
@@ -72,7 +83,7 @@ if err != nil {
7283
return err
7384
}
7485

75-
req, err := http.NewRequest("POST", url, buf)
86+
req, err := http.NewRequest(http.MethodPost, url, buf)
7687
if err != nil {
7788
return err
7889
}
@@ -98,7 +109,8 @@ if err != nil {
98109
return err
99110
}
100111
defer func() {
101-
res.Body.Close()
112+
err = res.Body.Close()
113+
// handle err somehow
102114
}()
103115

104116
result := &Output{}

go.sum

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
77
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
88
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
99
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
10+
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
1011
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
1112
github.com/stretchr/testify v1.1.4/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
1213
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=

request.go

+83-32
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package request
33
import (
44
"bytes"
55
"encoding/json"
6+
"fmt"
67
"io"
78
"io/ioutil"
89
"net/http"
@@ -12,20 +13,20 @@ import (
1213
"github.com/pkg/errors"
1314
)
1415

15-
// client is the global client instance.
16-
var client *http.Client
16+
// cachedClient is the global client instance.
17+
var cachedClient *http.Client
1718

1819
// defaultTimeout is the timeout applied if there is none provided.
1920
var defaultTimeout = 30 * time.Second
2021

2122
// getCachedClient returns the client instance or creates it if it did not exist.
2223
// The client does not follow redirects and has a timeout of defaultTimeout.
2324
func getCachedClient() *http.Client {
24-
if client == nil {
25-
client = GetClient()
25+
if cachedClient == nil {
26+
cachedClient = GetClient()
2627
}
2728

28-
return client
29+
return cachedClient
2930
}
3031

3132
// GetClient returns an http client that does not follow redirects and has a timeout of defaultTimeout.
@@ -40,12 +41,13 @@ func GetClient() *http.Client {
4041

4142
// Params holds all information necessary to set up the request instance.
4243
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
44+
URL string
45+
Method string
46+
Headers map[string]string
47+
Body interface{}
48+
Query map[string]string
49+
Timeout time.Duration
50+
ExpectedResponseCode int
4951
}
5052

5153
// Do executes the request as specified in the request params
@@ -56,14 +58,7 @@ func Do(params Params, responseBody interface{}) (returnErr error) {
5658
return err
5759
}
5860

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-
61+
client := selectClient(params.Timeout)
6762
res, err := client.Do(req)
6863
if err != nil {
6964
return errors.Wrap(err, "failed to send request")
@@ -75,13 +70,9 @@ func Do(params Params, responseBody interface{}) (returnErr error) {
7570
}
7671
}()
7772

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))
73+
err = checkResponseCode(res, params.ExpectedResponseCode)
74+
if err != nil {
75+
return err
8576
}
8677

8778
if responseBody == nil {
@@ -91,6 +82,39 @@ func Do(params Params, responseBody interface{}) (returnErr error) {
9182
return json.NewDecoder(res.Body).Decode(responseBody)
9283
}
9384

85+
// DoWithStringResponse is the same as Do but the response body is returned as string
86+
// instead of being parsed into the provided struct.
87+
func DoWithStringResponse(params Params) (result string, returnErr error) {
88+
req, err := createRequest(params)
89+
if err != nil {
90+
return "", err
91+
}
92+
93+
client := selectClient(params.Timeout)
94+
res, err := client.Do(req)
95+
if err != nil {
96+
return "", errors.Wrap(err, "failed to send request")
97+
}
98+
99+
defer func() {
100+
if cErr := res.Body.Close(); cErr != nil && returnErr == nil {
101+
returnErr = cErr
102+
}
103+
}()
104+
105+
err = checkResponseCode(res, params.ExpectedResponseCode)
106+
if err != nil {
107+
return "", err
108+
}
109+
110+
bodyBytes, err := ioutil.ReadAll(res.Body)
111+
if err != nil {
112+
return "", fmt.Errorf("failed to read response body: %w", err)
113+
}
114+
115+
return string(bodyBytes), nil
116+
}
117+
94118
func createRequest(params Params) (*http.Request, error) {
95119
reader, err := convertToReader(params.Body)
96120
if err != nil {
@@ -120,16 +144,12 @@ func createRequest(params Params) (*http.Request, error) {
120144

121145
// Get is a convience wrapper for "Do" to execute GET requests
122146
func Get(url string, responseBody interface{}) error {
123-
return Do(Params{Method: "GET", URL: url}, responseBody)
147+
return Do(Params{Method: http.MethodGet, URL: url}, responseBody)
124148
}
125149

126150
// Post is a convience wrapper for "Do" to execute POST requests
127151
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
152+
return Do(Params{Method: http.MethodPost, URL: url, Body: requestBody}, responseBody)
133153
}
134154

135155
func convertToReader(body interface{}) (io.Reader, error) {
@@ -150,3 +170,34 @@ func convertToReader(body interface{}) (io.Reader, error) {
150170

151171
return buffer, nil
152172
}
173+
174+
func selectClient(timeout time.Duration) *http.Client {
175+
if timeout != 0 {
176+
client := GetClient()
177+
client.Timeout = timeout
178+
return client
179+
}
180+
181+
return getCachedClient()
182+
}
183+
184+
func checkResponseCode(res *http.Response, expectedResponseCode int) error {
185+
if expectedResponseCode != 0 && res.StatusCode != expectedResponseCode {
186+
return fmt.Errorf("expected response code %d but got %d", expectedResponseCode, res.StatusCode)
187+
}
188+
189+
if !isSuccessCode(res.StatusCode) {
190+
bodyBytes, err := ioutil.ReadAll(res.Body)
191+
if err != nil || len(bodyBytes) == 0 {
192+
return httperrors.New(res.StatusCode, nil)
193+
}
194+
195+
return httperrors.New(res.StatusCode, string(bodyBytes))
196+
}
197+
198+
return nil
199+
}
200+
201+
func isSuccessCode(statusCode int) bool {
202+
return 200 <= statusCode && statusCode <= 299
203+
}

0 commit comments

Comments
 (0)