Skip to content

Commit 797a00f

Browse files
committed
ezhttp: CURL equivalent command helper, make request preparation more modular from callsite
1 parent 9550e4f commit 797a00f

File tree

3 files changed

+98
-18
lines changed

3 files changed

+98
-18
lines changed

net/http/ezhttp/ezhttp.go

+35-18
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
// This package aims to wrap Go HTTP Client's request-response with sane defaults:
22
//
3-
// - You are forced to consider timeouts by having to specify Context
4-
// - Instead of not considering non-2xx status codes as a failure, check that by default
5-
// (unless explicitly asked to)
6-
// - Sending and receiving JSON requires much less boilerplate, and on receiving JSON you
7-
// are forced to think whether to "allowUnknownFields"
3+
// - You are forced to consider timeouts by having to specify Context
4+
// - Instead of not considering non-2xx status codes as a failure, check that by default
5+
// (unless explicitly asked to)
6+
// - Sending and receiving JSON requires much less boilerplate, and on receiving JSON you
7+
// are forced to think whether to "allowUnknownFields"
88
package ezhttp
99

1010
import (
@@ -58,33 +58,46 @@ func (e ResponseStatusError) StatusCode() int {
5858
return e.statusCode
5959
}
6060

61+
// returns *ResponseStatusError as error if non-2xx response (unless TolerateNon2xxResponse()).
62+
// error is not *ResponseStatusError for transport-level errors, content (JSON) marshaling errors etc
6163
func Get(ctx context.Context, url string, confPieces ...ConfigPiece) (*http.Response, error) {
62-
return do(ctx, http.MethodGet, url, confPieces...)
64+
return newRequest(ctx, http.MethodGet, url, confPieces...).Send()
6365
}
6466

67+
// returns *ResponseStatusError as error if non-2xx response (unless TolerateNon2xxResponse()).
68+
// error is not *ResponseStatusError for transport-level errors, content (JSON) marshaling errors etc
6569
func Post(ctx context.Context, url string, confPieces ...ConfigPiece) (*http.Response, error) {
66-
return do(ctx, http.MethodPost, url, confPieces...)
70+
return newRequest(ctx, http.MethodPost, url, confPieces...).Send()
6771
}
6872

73+
// returns *ResponseStatusError as error if non-2xx response (unless TolerateNon2xxResponse()).
74+
// error is not *ResponseStatusError for transport-level errors, content (JSON) marshaling errors etc
6975
func Put(ctx context.Context, url string, confPieces ...ConfigPiece) (*http.Response, error) {
70-
return do(ctx, http.MethodPut, url, confPieces...)
76+
return newRequest(ctx, http.MethodPut, url, confPieces...).Send()
7177
}
7278

79+
// returns *ResponseStatusError as error if non-2xx response (unless TolerateNon2xxResponse()).
80+
// error is not *ResponseStatusError for transport-level errors, content (JSON) marshaling errors etc
7381
func Head(ctx context.Context, url string, confPieces ...ConfigPiece) (*http.Response, error) {
74-
return do(ctx, http.MethodHead, url, confPieces...)
82+
return newRequest(ctx, http.MethodHead, url, confPieces...).Send()
7583
}
7684

85+
// returns *ResponseStatusError as error if non-2xx response (unless TolerateNon2xxResponse()).
86+
// error is not *ResponseStatusError for transport-level errors, content (JSON) marshaling errors etc
7787
func Del(ctx context.Context, url string, confPieces ...ConfigPiece) (*http.Response, error) {
78-
return do(ctx, http.MethodDelete, url, confPieces...)
88+
return newRequest(ctx, http.MethodDelete, url, confPieces...).Send()
7989
}
8090

81-
// returns *ResponseStatusError as error if non-2xx response (unless TolerateNon2xxResponse()).
82-
// error is not *ResponseStatusError for transport-level errors, content (JSON) marshaling errors etc
83-
func do(ctx context.Context, method string, url string, confPieces ...ConfigPiece) (*http.Response, error) {
91+
func newRequest(ctx context.Context, method string, url string, confPieces ...ConfigPiece) *Config {
8492
conf := &Config{
8593
Client: http.DefaultClient,
8694
}
8795

96+
withErr := func(err error) *Config {
97+
conf.Abort = err // will be early-error-returned in `Send()`
98+
return conf
99+
}
100+
88101
for _, configure := range confPieces {
89102
if configure.BeforeInit == nil {
90103
continue
@@ -93,7 +106,7 @@ func do(ctx context.Context, method string, url string, confPieces ...ConfigPiec
93106
}
94107

95108
if conf.Abort != nil {
96-
return nil, conf.Abort
109+
return withErr(conf.Abort)
97110
}
98111

99112
// "Request has body = No" for:
@@ -102,15 +115,15 @@ func do(ctx context.Context, method string, url string, confPieces ...ConfigPiec
102115
if conf.RequestBody != nil && (method == http.MethodGet || method == http.MethodHead) {
103116
// Technically, these can have body, but it's usually a mistake so if we need it we'll
104117
// make it an opt-in flag.
105-
return nil, fmt.Errorf("ezhttp: %s with non-nil body is usually a mistake", method)
118+
return withErr(fmt.Errorf("ezhttp: %s with non-nil body is usually a mistake", method))
106119
}
107120

108121
req, err := http.NewRequest(
109122
method,
110123
url,
111124
conf.RequestBody)
112125
if err != nil {
113-
return nil, err
126+
return withErr(err)
114127
}
115128

116129
req = req.WithContext(ctx)
@@ -124,17 +137,21 @@ func do(ctx context.Context, method string, url string, confPieces ...ConfigPiec
124137
configure.AfterInit(conf)
125138
}
126139

140+
return conf
141+
}
142+
143+
func (conf *Config) Send() (*http.Response, error) {
127144
if conf.Abort != nil {
128145
return nil, conf.Abort
129146
}
130147

131-
resp, err := conf.Client.Do(req)
148+
resp, err := conf.Client.Do(conf.Request)
132149
if err != nil {
133150
return resp, err // this is a transport-level error
134151
}
135152

136153
// 304 is an error unless caller is expecting such response by sending caching headers
137-
if resp.StatusCode == http.StatusNotModified && req.Header.Get("If-None-Match") != "" {
154+
if resp.StatusCode == http.StatusNotModified && conf.Request.Header.Get("If-None-Match") != "" {
138155
return resp, nil
139156
}
140157

net/http/ezhttp/helpers.go

+47
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package ezhttp
22

33
import (
4+
"context"
45
"crypto/tls"
6+
"fmt"
57
"net/http"
68
)
79

@@ -26,3 +28,48 @@ func ErrorIs(err error, statusCode int) bool {
2628
return false
2729
}
2830
}
31+
32+
// same as the corresponding without "New" prefix, but just prepared the request configuration without sending it yet
33+
func NewGet(ctx context.Context, url string, confPieces ...ConfigPiece) *Config {
34+
return newRequest(ctx, http.MethodGet, url, confPieces...)
35+
}
36+
37+
// same as the corresponding without "New" prefix, but just prepared the request configuration without sending it yet
38+
func NewPost(ctx context.Context, url string, confPieces ...ConfigPiece) *Config {
39+
return newRequest(ctx, http.MethodPost, url, confPieces...)
40+
}
41+
42+
// same as the corresponding without "New" prefix, but just prepared the request configuration without sending it yet
43+
func NewPut(ctx context.Context, url string, confPieces ...ConfigPiece) *Config {
44+
return newRequest(ctx, http.MethodPut, url, confPieces...)
45+
}
46+
47+
// same as the corresponding without "New" prefix, but just prepared the request configuration without sending it yet
48+
func NewHead(ctx context.Context, url string, confPieces ...ConfigPiece) *Config {
49+
return newRequest(ctx, http.MethodHead, url, confPieces...)
50+
}
51+
52+
// same as the corresponding without "New" prefix, but just prepared the request configuration without sending it yet
53+
func NewDel(ctx context.Context, url string, confPieces ...ConfigPiece) *Config {
54+
return newRequest(ctx, http.MethodDelete, url, confPieces...)
55+
}
56+
57+
// for `method` please use `net/http` "enum" (quotes because it's not declared as such)
58+
func (c *Config) CURLEquivalent() ([]string, error) {
59+
if err := c.Abort; err != nil {
60+
return nil, err
61+
}
62+
63+
req := c.Request // shorthand
64+
65+
cmd := []string{"curl", "--request=" + req.Method}
66+
67+
for key, values := range req.Header {
68+
// FIXME: doesn't take into account multiple values
69+
cmd = append(cmd, fmt.Sprintf("--header=%s=%s", key, values[0]))
70+
}
71+
72+
cmd = append(cmd, req.URL.String())
73+
74+
return cmd, nil
75+
}

net/http/ezhttp/helpers_test.go

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package ezhttp
2+
3+
import (
4+
"context"
5+
"strings"
6+
"testing"
7+
8+
. "github.com/function61/gokit/builtin"
9+
"github.com/function61/gokit/testing/assert"
10+
)
11+
12+
func TestCURLEquivalent(t *testing.T) {
13+
curlCmd := Must(NewPost(context.Background(), "https://example.net/hello", Header("x-correlation-id", "123")).CURLEquivalent())
14+
15+
assert.Equal(t, strings.Join(curlCmd, " "), "curl --request=POST --header=X-Correlation-Id=123 https://example.net/hello")
16+
}

0 commit comments

Comments
 (0)