From 797a00fcb45f086be5907a3b061f354099123ced Mon Sep 17 00:00:00 2001 From: Joonas Loppi Date: Mon, 27 Nov 2023 13:36:08 +0200 Subject: [PATCH] ezhttp: CURL equivalent command helper, make request preparation more modular from callsite --- net/http/ezhttp/ezhttp.go | 53 ++++++++++++++++++++++----------- net/http/ezhttp/helpers.go | 47 +++++++++++++++++++++++++++++ net/http/ezhttp/helpers_test.go | 16 ++++++++++ 3 files changed, 98 insertions(+), 18 deletions(-) create mode 100644 net/http/ezhttp/helpers_test.go diff --git a/net/http/ezhttp/ezhttp.go b/net/http/ezhttp/ezhttp.go index 70699c3..1950d4d 100644 --- a/net/http/ezhttp/ezhttp.go +++ b/net/http/ezhttp/ezhttp.go @@ -1,10 +1,10 @@ // This package aims to wrap Go HTTP Client's request-response with sane defaults: // -// - You are forced to consider timeouts by having to specify Context -// - Instead of not considering non-2xx status codes as a failure, check that by default -// (unless explicitly asked to) -// - Sending and receiving JSON requires much less boilerplate, and on receiving JSON you -// are forced to think whether to "allowUnknownFields" +// - You are forced to consider timeouts by having to specify Context +// - Instead of not considering non-2xx status codes as a failure, check that by default +// (unless explicitly asked to) +// - Sending and receiving JSON requires much less boilerplate, and on receiving JSON you +// are forced to think whether to "allowUnknownFields" package ezhttp import ( @@ -58,33 +58,46 @@ func (e ResponseStatusError) StatusCode() int { return e.statusCode } +// returns *ResponseStatusError as error if non-2xx response (unless TolerateNon2xxResponse()). +// error is not *ResponseStatusError for transport-level errors, content (JSON) marshaling errors etc func Get(ctx context.Context, url string, confPieces ...ConfigPiece) (*http.Response, error) { - return do(ctx, http.MethodGet, url, confPieces...) + return newRequest(ctx, http.MethodGet, url, confPieces...).Send() } +// returns *ResponseStatusError as error if non-2xx response (unless TolerateNon2xxResponse()). +// error is not *ResponseStatusError for transport-level errors, content (JSON) marshaling errors etc func Post(ctx context.Context, url string, confPieces ...ConfigPiece) (*http.Response, error) { - return do(ctx, http.MethodPost, url, confPieces...) + return newRequest(ctx, http.MethodPost, url, confPieces...).Send() } +// returns *ResponseStatusError as error if non-2xx response (unless TolerateNon2xxResponse()). +// error is not *ResponseStatusError for transport-level errors, content (JSON) marshaling errors etc func Put(ctx context.Context, url string, confPieces ...ConfigPiece) (*http.Response, error) { - return do(ctx, http.MethodPut, url, confPieces...) + return newRequest(ctx, http.MethodPut, url, confPieces...).Send() } +// returns *ResponseStatusError as error if non-2xx response (unless TolerateNon2xxResponse()). +// error is not *ResponseStatusError for transport-level errors, content (JSON) marshaling errors etc func Head(ctx context.Context, url string, confPieces ...ConfigPiece) (*http.Response, error) { - return do(ctx, http.MethodHead, url, confPieces...) + return newRequest(ctx, http.MethodHead, url, confPieces...).Send() } +// returns *ResponseStatusError as error if non-2xx response (unless TolerateNon2xxResponse()). +// error is not *ResponseStatusError for transport-level errors, content (JSON) marshaling errors etc func Del(ctx context.Context, url string, confPieces ...ConfigPiece) (*http.Response, error) { - return do(ctx, http.MethodDelete, url, confPieces...) + return newRequest(ctx, http.MethodDelete, url, confPieces...).Send() } -// returns *ResponseStatusError as error if non-2xx response (unless TolerateNon2xxResponse()). -// error is not *ResponseStatusError for transport-level errors, content (JSON) marshaling errors etc -func do(ctx context.Context, method string, url string, confPieces ...ConfigPiece) (*http.Response, error) { +func newRequest(ctx context.Context, method string, url string, confPieces ...ConfigPiece) *Config { conf := &Config{ Client: http.DefaultClient, } + withErr := func(err error) *Config { + conf.Abort = err // will be early-error-returned in `Send()` + return conf + } + for _, configure := range confPieces { if configure.BeforeInit == nil { continue @@ -93,7 +106,7 @@ func do(ctx context.Context, method string, url string, confPieces ...ConfigPiec } if conf.Abort != nil { - return nil, conf.Abort + return withErr(conf.Abort) } // "Request has body = No" for: @@ -102,7 +115,7 @@ func do(ctx context.Context, method string, url string, confPieces ...ConfigPiec if conf.RequestBody != nil && (method == http.MethodGet || method == http.MethodHead) { // Technically, these can have body, but it's usually a mistake so if we need it we'll // make it an opt-in flag. - return nil, fmt.Errorf("ezhttp: %s with non-nil body is usually a mistake", method) + return withErr(fmt.Errorf("ezhttp: %s with non-nil body is usually a mistake", method)) } req, err := http.NewRequest( @@ -110,7 +123,7 @@ func do(ctx context.Context, method string, url string, confPieces ...ConfigPiec url, conf.RequestBody) if err != nil { - return nil, err + return withErr(err) } req = req.WithContext(ctx) @@ -124,17 +137,21 @@ func do(ctx context.Context, method string, url string, confPieces ...ConfigPiec configure.AfterInit(conf) } + return conf +} + +func (conf *Config) Send() (*http.Response, error) { if conf.Abort != nil { return nil, conf.Abort } - resp, err := conf.Client.Do(req) + resp, err := conf.Client.Do(conf.Request) if err != nil { return resp, err // this is a transport-level error } // 304 is an error unless caller is expecting such response by sending caching headers - if resp.StatusCode == http.StatusNotModified && req.Header.Get("If-None-Match") != "" { + if resp.StatusCode == http.StatusNotModified && conf.Request.Header.Get("If-None-Match") != "" { return resp, nil } diff --git a/net/http/ezhttp/helpers.go b/net/http/ezhttp/helpers.go index f5e37e5..6d59fc8 100644 --- a/net/http/ezhttp/helpers.go +++ b/net/http/ezhttp/helpers.go @@ -1,7 +1,9 @@ package ezhttp import ( + "context" "crypto/tls" + "fmt" "net/http" ) @@ -26,3 +28,48 @@ func ErrorIs(err error, statusCode int) bool { return false } } + +// same as the corresponding without "New" prefix, but just prepared the request configuration without sending it yet +func NewGet(ctx context.Context, url string, confPieces ...ConfigPiece) *Config { + return newRequest(ctx, http.MethodGet, url, confPieces...) +} + +// same as the corresponding without "New" prefix, but just prepared the request configuration without sending it yet +func NewPost(ctx context.Context, url string, confPieces ...ConfigPiece) *Config { + return newRequest(ctx, http.MethodPost, url, confPieces...) +} + +// same as the corresponding without "New" prefix, but just prepared the request configuration without sending it yet +func NewPut(ctx context.Context, url string, confPieces ...ConfigPiece) *Config { + return newRequest(ctx, http.MethodPut, url, confPieces...) +} + +// same as the corresponding without "New" prefix, but just prepared the request configuration without sending it yet +func NewHead(ctx context.Context, url string, confPieces ...ConfigPiece) *Config { + return newRequest(ctx, http.MethodHead, url, confPieces...) +} + +// same as the corresponding without "New" prefix, but just prepared the request configuration without sending it yet +func NewDel(ctx context.Context, url string, confPieces ...ConfigPiece) *Config { + return newRequest(ctx, http.MethodDelete, url, confPieces...) +} + +// for `method` please use `net/http` "enum" (quotes because it's not declared as such) +func (c *Config) CURLEquivalent() ([]string, error) { + if err := c.Abort; err != nil { + return nil, err + } + + req := c.Request // shorthand + + cmd := []string{"curl", "--request=" + req.Method} + + for key, values := range req.Header { + // FIXME: doesn't take into account multiple values + cmd = append(cmd, fmt.Sprintf("--header=%s=%s", key, values[0])) + } + + cmd = append(cmd, req.URL.String()) + + return cmd, nil +} diff --git a/net/http/ezhttp/helpers_test.go b/net/http/ezhttp/helpers_test.go new file mode 100644 index 0000000..8bd4704 --- /dev/null +++ b/net/http/ezhttp/helpers_test.go @@ -0,0 +1,16 @@ +package ezhttp + +import ( + "context" + "strings" + "testing" + + . "github.com/function61/gokit/builtin" + "github.com/function61/gokit/testing/assert" +) + +func TestCURLEquivalent(t *testing.T) { + curlCmd := Must(NewPost(context.Background(), "https://example.net/hello", Header("x-correlation-id", "123")).CURLEquivalent()) + + assert.Equal(t, strings.Join(curlCmd, " "), "curl --request=POST --header=X-Correlation-Id=123 https://example.net/hello") +}