Skip to content

Commit

Permalink
fix: populate files slice on multipart/form submission (#127)
Browse files Browse the repository at this point in the history
  • Loading branch information
llimllib authored Jun 21, 2023
1 parent 6f5e534 commit 69e0f18
Show file tree
Hide file tree
Showing 3 changed files with 90 additions and 0 deletions.
42 changes: 42 additions & 0 deletions httpbin/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,6 +542,7 @@ func testRequestWithBody(t *testing.T, verb, path string) {
testRequestWithBodyBodyTooBig,
testRequestWithBodyEmptyBody,
testRequestWithBodyFormEncodedBody,
testRequestWithBodyMultiPartBodyFiles,
testRequestWithBodyFormEncodedBodyNoContentType,
testRequestWithBodyInvalidFormEncodedBody,
testRequestWithBodyInvalidJSON,
Expand Down Expand Up @@ -816,6 +817,47 @@ func testRequestWithBodyMultiPartBody(t *testing.T, verb, path string) {
}
}

func testRequestWithBodyMultiPartBodyFiles(t *testing.T, verb, path string) {
var body bytes.Buffer
mw := multipart.NewWriter(&body)

// Add a file to the multipart request
part, _ := mw.CreateFormFile("fieldname", "filename")
part.Write([]byte("hello world"))
mw.Close()

r, _ := http.NewRequest(verb, path, bytes.NewReader(body.Bytes()))
r.Header.Set("Content-Type", mw.FormDataContentType())
w := httptest.NewRecorder()
app.ServeHTTP(w, r)

assertStatusCode(t, w, http.StatusOK)
assertContentType(t, w, jsonContentType)

var resp *bodyResponse
err := json.Unmarshal(w.Body.Bytes(), &resp)
if err != nil {
t.Fatalf("failed to unmarshal body %#v from JSON: %s", w.Body.String(), err)
}

if len(resp.Args) > 0 {
t.Fatalf("expected no query params, got %#v", resp.Args)
}

// verify that the file we added is present in the `files` attribute of the
// response, with the field as key and content as value
wantFiles := map[string][]string{
"fieldname": {"hello world"},
}
if !reflect.DeepEqual(resp.Files, wantFiles) {
t.Fatalf("want resp.Files = %#v, got %#v", wantFiles, resp.Files)
}

if resp.Method != verb {
t.Fatalf("expected method to be %s, got %s", verb, resp.Method)
}
}

func testRequestWithBodyInvalidFormEncodedBody(t *testing.T, verb, path string) {
r, _ := http.NewRequest(verb, path, strings.NewReader("%ZZ"))
r.Header.Set("Content-Type", "application/x-www-form-urlencoded")
Expand Down
28 changes: 28 additions & 0 deletions httpbin/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"fmt"
"io"
"math/rand"
"mime/multipart"
"net/http"
"net/url"
"strconv"
Expand Down Expand Up @@ -109,6 +110,28 @@ func writeHTML(w http.ResponseWriter, body []byte, status int) {
writeResponse(w, status, htmlContentType, body)
}

// parseFiles handles reading the contents of files in a multipart FileHeader
// and returning a map that can be used as the Files attribute of a response
func parseFiles(fileHeaders map[string][]*multipart.FileHeader) (map[string][]string, error) {
files := map[string][]string{}
for k, fs := range fileHeaders {
files[k] = []string{}

for _, f := range fs {
fh, err := f.Open()
if err != nil {
return nil, err
}
contents, err := io.ReadAll(fh)
if err != nil {
return nil, err
}
files[k] = append(files[k], string(contents))
}
}
return files, nil
}

// parseBody handles parsing a request body into our standard API response,
// taking care to only consume the request body once based on the Content-Type
// of the request. The given bodyResponse will be modified.
Expand Down Expand Up @@ -175,6 +198,11 @@ func parseBody(w http.ResponseWriter, r *http.Request, resp *bodyResponse) error
return err
}
resp.Form = r.PostForm
files, err := parseFiles(r.MultipartForm.File)
if err != nil {
return err
}
resp.Files = files
case ct == "application/json":
err := json.NewDecoder(r.Body).Decode(&resp.JSON)
if err != nil && err != io.EOF {
Expand Down
20 changes: 20 additions & 0 deletions httpbin/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import (
"crypto/tls"
"fmt"
"io"
"io/fs"
"mime/multipart"
"net/http"
"net/url"
"reflect"
Expand Down Expand Up @@ -293,3 +295,21 @@ func Test_getClientIP(t *testing.T) {
})
}
}

func TestParseFileDoesntExist(t *testing.T) {
// set up a headers map where the filename doesn't exist, to test `f.Open`
// throwing an error
headers := map[string][]*multipart.FileHeader{
"fieldname": {
{
Filename: "bananas",
},
},
}

// expect a patherror
_, err := parseFiles(headers)
if _, ok := err.(*fs.PathError); !ok {
t.Fatalf("Open(nonexist): error is %T, want *PathError", err)
}
}

0 comments on commit 69e0f18

Please sign in to comment.