Skip to content

Commit ed53f99

Browse files
Add BodyMultipart (#119)
* Add BodyMultipart * Test: Make CR visible
1 parent 34081e9 commit ed53f99

File tree

2 files changed

+92
-0
lines changed

2 files changed

+92
-0
lines changed

config.go

+32
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ package requests
22

33
import (
44
"compress/gzip"
5+
"fmt"
56
"io"
7+
"mime/multipart"
68
"net/http/httptest"
79
)
810

@@ -43,3 +45,33 @@ func TestServerConfig(s *httptest.Server) Config {
4345
Client(s.Client())
4446
}
4547
}
48+
49+
// BodyMultipart returns a Config
50+
// that uses a multipart.Writer for the request body.
51+
// If boundary is "", a multipart boundary is chosen at random.
52+
// The content type of the request is set to multipart/form-data
53+
// with the correct boundary.
54+
// The multipart.Writer is automatically closed if the callback succeeds.
55+
func BodyMultipart(boundary string, h func(multi *multipart.Writer) error) Config {
56+
return func(rb *Builder) {
57+
if boundary == "" {
58+
multi := multipart.NewWriter(nil)
59+
boundary = multi.Boundary()
60+
}
61+
rb.
62+
ContentType("multipart/form-data; boundary=" + boundary).
63+
BodyWriter(func(w io.Writer) error {
64+
multi := multipart.NewWriter(w)
65+
if err := multi.SetBoundary(boundary); err != nil {
66+
return fmt.Errorf("setting boundary: %w", err)
67+
}
68+
if err := h(multi); err != nil {
69+
return err
70+
}
71+
if err := multi.Close(); err != nil {
72+
return fmt.Errorf("closing multipart writer: %w", err)
73+
}
74+
return nil
75+
})
76+
}
77+
}

config_example_test.go

+60
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ import (
44
"compress/gzip"
55
"context"
66
"fmt"
7+
"io"
8+
"mime/multipart"
79
"net/http"
810
"net/http/httptest"
11+
"net/http/httputil"
12+
"net/textproto"
913
"strings"
1014

1115
"github.com/carlmjohnson/requests"
@@ -100,3 +104,59 @@ func ExampleTestServerConfig() {
100104
// Hello, world!
101105
// Howdy, planet!
102106
}
107+
108+
func ExampleBodyMultipart() {
109+
req, err := requests.
110+
URL("http://example.com").
111+
Config(requests.BodyMultipart("abc", func(multi *multipart.Writer) error {
112+
// CreateFormFile hardcodes the Content-Type as application/octet-stream
113+
w, err := multi.CreateFormFile("file", "en.txt")
114+
if err != nil {
115+
return err
116+
}
117+
_, err = io.WriteString(w, "Hello, World!")
118+
if err != nil {
119+
return err
120+
}
121+
// CreatePart is more flexible and lets you add headers
122+
h := make(textproto.MIMEHeader)
123+
h.Set("Content-Disposition", `form-data; name="file"; filename="jp.txt"`)
124+
h.Set("Content-Type", "text/plain; charset=utf-8")
125+
w, err = multi.CreatePart(h)
126+
if err != nil {
127+
panic(err)
128+
}
129+
_, err = io.WriteString(w, "こんにちは世界!")
130+
if err != nil {
131+
return err
132+
}
133+
return nil
134+
})).
135+
Request(context.Background())
136+
if err != nil {
137+
panic(err)
138+
}
139+
b, err := httputil.DumpRequest(req, true)
140+
if err != nil {
141+
panic(err)
142+
}
143+
144+
// Make carriage return visible
145+
fmt.Println(strings.ReplaceAll(string(b), "\r", "↵"))
146+
// Output:
147+
// POST / HTTP/1.1↵
148+
// Host: example.com↵
149+
// Content-Type: multipart/form-data; boundary=abc↵
150+
// ↵
151+
// --abc↵
152+
// Content-Disposition: form-data; name="file"; filename="en.txt"↵
153+
// Content-Type: application/octet-stream↵
154+
// ↵
155+
// Hello, World!↵
156+
// --abc↵
157+
// Content-Disposition: form-data; name="file"; filename="jp.txt"↵
158+
// Content-Type: text/plain; charset=utf-8↵
159+
// ↵
160+
// こんにちは世界!↵
161+
// --abc--↵
162+
}

0 commit comments

Comments
 (0)