diff --git a/config.go b/config.go index 8cf7c2c..ba4ef61 100644 --- a/config.go +++ b/config.go @@ -2,7 +2,9 @@ package requests import ( "compress/gzip" + "fmt" "io" + "mime/multipart" "net/http/httptest" ) @@ -43,3 +45,33 @@ func TestServerConfig(s *httptest.Server) Config { Client(s.Client()) } } + +// BodyMultipart returns a Config +// that uses a multipart.Writer for the request body. +// If boundary is "", a multipart boundary is chosen at random. +// The content type of the request is set to multipart/form-data +// with the correct boundary. +// The multipart.Writer is automatically closed if the callback succeeds. +func BodyMultipart(boundary string, h func(multi *multipart.Writer) error) Config { + return func(rb *Builder) { + if boundary == "" { + multi := multipart.NewWriter(nil) + boundary = multi.Boundary() + } + rb. + ContentType("multipart/form-data; boundary=" + boundary). + BodyWriter(func(w io.Writer) error { + multi := multipart.NewWriter(w) + if err := multi.SetBoundary(boundary); err != nil { + return fmt.Errorf("setting boundary: %w", err) + } + if err := h(multi); err != nil { + return err + } + if err := multi.Close(); err != nil { + return fmt.Errorf("closing multipart writer: %w", err) + } + return nil + }) + } +} diff --git a/config_example_test.go b/config_example_test.go index 8ee7eeb..f35c6dc 100644 --- a/config_example_test.go +++ b/config_example_test.go @@ -4,8 +4,12 @@ import ( "compress/gzip" "context" "fmt" + "io" + "mime/multipart" "net/http" "net/http/httptest" + "net/http/httputil" + "net/textproto" "strings" "github.com/carlmjohnson/requests" @@ -100,3 +104,59 @@ func ExampleTestServerConfig() { // Hello, world! // Howdy, planet! } + +func ExampleBodyMultipart() { + req, err := requests. + URL("http://example.com"). + Config(requests.BodyMultipart("abc", func(multi *multipart.Writer) error { + // CreateFormFile hardcodes the Content-Type as application/octet-stream + w, err := multi.CreateFormFile("file", "en.txt") + if err != nil { + return err + } + _, err = io.WriteString(w, "Hello, World!") + if err != nil { + return err + } + // CreatePart is more flexible and lets you add headers + h := make(textproto.MIMEHeader) + h.Set("Content-Disposition", `form-data; name="file"; filename="jp.txt"`) + h.Set("Content-Type", "text/plain; charset=utf-8") + w, err = multi.CreatePart(h) + if err != nil { + panic(err) + } + _, err = io.WriteString(w, "こんにちは世界!") + if err != nil { + return err + } + return nil + })). + Request(context.Background()) + if err != nil { + panic(err) + } + b, err := httputil.DumpRequest(req, true) + if err != nil { + panic(err) + } + + // Make carriage return visible + fmt.Println(strings.ReplaceAll(string(b), "\r", "↵")) + // Output: + // POST / HTTP/1.1↵ + // Host: example.com↵ + // Content-Type: multipart/form-data; boundary=abc↵ + // ↵ + // --abc↵ + // Content-Disposition: form-data; name="file"; filename="en.txt"↵ + // Content-Type: application/octet-stream↵ + // ↵ + // Hello, World!↵ + // --abc↵ + // Content-Disposition: form-data; name="file"; filename="jp.txt"↵ + // Content-Type: text/plain; charset=utf-8↵ + // ↵ + // こんにちは世界!↵ + // --abc--↵ +}