Skip to content

Commit 69e0f18

Browse files
authored
fix: populate files slice on multipart/form submission (#127)
1 parent 6f5e534 commit 69e0f18

File tree

3 files changed

+90
-0
lines changed

3 files changed

+90
-0
lines changed

httpbin/handlers_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -542,6 +542,7 @@ func testRequestWithBody(t *testing.T, verb, path string) {
542542
testRequestWithBodyBodyTooBig,
543543
testRequestWithBodyEmptyBody,
544544
testRequestWithBodyFormEncodedBody,
545+
testRequestWithBodyMultiPartBodyFiles,
545546
testRequestWithBodyFormEncodedBodyNoContentType,
546547
testRequestWithBodyInvalidFormEncodedBody,
547548
testRequestWithBodyInvalidJSON,
@@ -816,6 +817,47 @@ func testRequestWithBodyMultiPartBody(t *testing.T, verb, path string) {
816817
}
817818
}
818819

820+
func testRequestWithBodyMultiPartBodyFiles(t *testing.T, verb, path string) {
821+
var body bytes.Buffer
822+
mw := multipart.NewWriter(&body)
823+
824+
// Add a file to the multipart request
825+
part, _ := mw.CreateFormFile("fieldname", "filename")
826+
part.Write([]byte("hello world"))
827+
mw.Close()
828+
829+
r, _ := http.NewRequest(verb, path, bytes.NewReader(body.Bytes()))
830+
r.Header.Set("Content-Type", mw.FormDataContentType())
831+
w := httptest.NewRecorder()
832+
app.ServeHTTP(w, r)
833+
834+
assertStatusCode(t, w, http.StatusOK)
835+
assertContentType(t, w, jsonContentType)
836+
837+
var resp *bodyResponse
838+
err := json.Unmarshal(w.Body.Bytes(), &resp)
839+
if err != nil {
840+
t.Fatalf("failed to unmarshal body %#v from JSON: %s", w.Body.String(), err)
841+
}
842+
843+
if len(resp.Args) > 0 {
844+
t.Fatalf("expected no query params, got %#v", resp.Args)
845+
}
846+
847+
// verify that the file we added is present in the `files` attribute of the
848+
// response, with the field as key and content as value
849+
wantFiles := map[string][]string{
850+
"fieldname": {"hello world"},
851+
}
852+
if !reflect.DeepEqual(resp.Files, wantFiles) {
853+
t.Fatalf("want resp.Files = %#v, got %#v", wantFiles, resp.Files)
854+
}
855+
856+
if resp.Method != verb {
857+
t.Fatalf("expected method to be %s, got %s", verb, resp.Method)
858+
}
859+
}
860+
819861
func testRequestWithBodyInvalidFormEncodedBody(t *testing.T, verb, path string) {
820862
r, _ := http.NewRequest(verb, path, strings.NewReader("%ZZ"))
821863
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")

httpbin/helpers.go

+28
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"fmt"
1111
"io"
1212
"math/rand"
13+
"mime/multipart"
1314
"net/http"
1415
"net/url"
1516
"strconv"
@@ -109,6 +110,28 @@ func writeHTML(w http.ResponseWriter, body []byte, status int) {
109110
writeResponse(w, status, htmlContentType, body)
110111
}
111112

113+
// parseFiles handles reading the contents of files in a multipart FileHeader
114+
// and returning a map that can be used as the Files attribute of a response
115+
func parseFiles(fileHeaders map[string][]*multipart.FileHeader) (map[string][]string, error) {
116+
files := map[string][]string{}
117+
for k, fs := range fileHeaders {
118+
files[k] = []string{}
119+
120+
for _, f := range fs {
121+
fh, err := f.Open()
122+
if err != nil {
123+
return nil, err
124+
}
125+
contents, err := io.ReadAll(fh)
126+
if err != nil {
127+
return nil, err
128+
}
129+
files[k] = append(files[k], string(contents))
130+
}
131+
}
132+
return files, nil
133+
}
134+
112135
// parseBody handles parsing a request body into our standard API response,
113136
// taking care to only consume the request body once based on the Content-Type
114137
// of the request. The given bodyResponse will be modified.
@@ -175,6 +198,11 @@ func parseBody(w http.ResponseWriter, r *http.Request, resp *bodyResponse) error
175198
return err
176199
}
177200
resp.Form = r.PostForm
201+
files, err := parseFiles(r.MultipartForm.File)
202+
if err != nil {
203+
return err
204+
}
205+
resp.Files = files
178206
case ct == "application/json":
179207
err := json.NewDecoder(r.Body).Decode(&resp.JSON)
180208
if err != nil && err != io.EOF {

httpbin/helpers_test.go

+20
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"crypto/tls"
55
"fmt"
66
"io"
7+
"io/fs"
8+
"mime/multipart"
79
"net/http"
810
"net/url"
911
"reflect"
@@ -293,3 +295,21 @@ func Test_getClientIP(t *testing.T) {
293295
})
294296
}
295297
}
298+
299+
func TestParseFileDoesntExist(t *testing.T) {
300+
// set up a headers map where the filename doesn't exist, to test `f.Open`
301+
// throwing an error
302+
headers := map[string][]*multipart.FileHeader{
303+
"fieldname": {
304+
{
305+
Filename: "bananas",
306+
},
307+
},
308+
}
309+
310+
// expect a patherror
311+
_, err := parseFiles(headers)
312+
if _, ok := err.(*fs.PathError); !ok {
313+
t.Fatalf("Open(nonexist): error is %T, want *PathError", err)
314+
}
315+
}

0 commit comments

Comments
 (0)