Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ezhttp: CURL equivalent command helper #26

Merged
merged 1 commit into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 35 additions & 18 deletions net/http/ezhttp/ezhttp.go
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -102,15 +115,15 @@ 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(
method,
url,
conf.RequestBody)
if err != nil {
return nil, err
return withErr(err)
}

req = req.WithContext(ctx)
Expand All @@ -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
}

Expand Down
47 changes: 47 additions & 0 deletions net/http/ezhttp/helpers.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package ezhttp

import (
"context"
"crypto/tls"
"fmt"
"net/http"
)

Expand All @@ -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
}
16 changes: 16 additions & 0 deletions net/http/ezhttp/helpers_test.go
Original file line number Diff line number Diff line change
@@ -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")
}
Loading