diff --git a/request.go b/request.go index c7babbe..07af9bb 100644 --- a/request.go +++ b/request.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "net/url" ) // NewRequest creates an http.Request with context. It is a convenience @@ -32,3 +33,20 @@ func NewJSONRequest(ctx context.Context, method, url string, body any) (*http.Re } return req, nil } + +// NewFormRequest creates an http.Request with a form-encoded body and +// sets Content-Type to application/x-www-form-urlencoded. +// The GetBody function is set so that the request can be retried. +func NewFormRequest(ctx context.Context, method, rawURL string, values url.Values) (*http.Request, error) { + encoded := values.Encode() + b := []byte(encoded) + req, err := http.NewRequestWithContext(ctx, method, rawURL, bytes.NewReader(b)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.GetBody = func() (io.ReadCloser, error) { + return io.NopCloser(bytes.NewReader(b)), nil + } + return req, nil +} diff --git a/request_form_test.go b/request_form_test.go new file mode 100644 index 0000000..1b24989 --- /dev/null +++ b/request_form_test.go @@ -0,0 +1,80 @@ +package httpx_test + +import ( + "context" + "io" + "net/http" + "net/url" + "testing" + + "git.codelab.vc/pkg/httpx" +) + +func TestNewFormRequest(t *testing.T) { + t.Run("body is form-encoded", func(t *testing.T) { + values := url.Values{"username": {"alice"}, "scope": {"read"}} + req, err := httpx.NewFormRequest(context.Background(), http.MethodPost, "http://example.com/token", values) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + body, err := io.ReadAll(req.Body) + if err != nil { + t.Fatalf("reading body: %v", err) + } + + parsed, err := url.ParseQuery(string(body)) + if err != nil { + t.Fatalf("parsing form: %v", err) + } + if parsed.Get("username") != "alice" { + t.Errorf("username = %q, want %q", parsed.Get("username"), "alice") + } + if parsed.Get("scope") != "read" { + t.Errorf("scope = %q, want %q", parsed.Get("scope"), "read") + } + }) + + t.Run("content type is set", func(t *testing.T) { + values := url.Values{"key": {"value"}} + req, err := httpx.NewFormRequest(context.Background(), http.MethodPost, "http://example.com", values) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + ct := req.Header.Get("Content-Type") + if ct != "application/x-www-form-urlencoded" { + t.Errorf("Content-Type = %q, want %q", ct, "application/x-www-form-urlencoded") + } + }) + + t.Run("GetBody works for retry", func(t *testing.T) { + values := url.Values{"key": {"value"}} + req, err := httpx.NewFormRequest(context.Background(), http.MethodPost, "http://example.com", values) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if req.GetBody == nil { + t.Fatal("GetBody is nil") + } + + b1, err := io.ReadAll(req.Body) + if err != nil { + t.Fatalf("reading body: %v", err) + } + + body2, err := req.GetBody() + if err != nil { + t.Fatalf("GetBody(): %v", err) + } + b2, err := io.ReadAll(body2) + if err != nil { + t.Fatalf("reading body2: %v", err) + } + + if string(b1) != string(b2) { + t.Errorf("GetBody returned different data: %q vs %q", b1, b2) + } + }) +}