diff --git a/httpbin/handlers_test.go b/httpbin/handlers_test.go index b54c904d21cf810a7d1304eb0346c55e660424fa..447ddd35af2465f10f76416d6013acf4ae712876 100644 --- a/httpbin/handlers_test.go +++ b/httpbin/handlers_test.go @@ -542,6 +542,7 @@ func testRequestWithBody(t *testing.T, verb, path string) { testRequestWithBodyBodyTooBig, testRequestWithBodyEmptyBody, testRequestWithBodyFormEncodedBody, + testRequestWithBodyMultiPartBodyFiles, testRequestWithBodyFormEncodedBodyNoContentType, testRequestWithBodyInvalidFormEncodedBody, testRequestWithBodyInvalidJSON, @@ -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") diff --git a/httpbin/helpers.go b/httpbin/helpers.go index 71cc6d07d25dc5201527dedc1da0da613b9a832b..606983af1ba580f2cd97c35633cb5181e656a157 100644 --- a/httpbin/helpers.go +++ b/httpbin/helpers.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "math/rand" + "mime/multipart" "net/http" "net/url" "strconv" @@ -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. @@ -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 { diff --git a/httpbin/helpers_test.go b/httpbin/helpers_test.go index 1057df464b70abfbc7db3e895714c5a9f0d6b9f9..a11c691d1b60d403da10fd7b0f7670bd6bb496db 100644 --- a/httpbin/helpers_test.go +++ b/httpbin/helpers_test.go @@ -4,6 +4,8 @@ import ( "crypto/tls" "fmt" "io" + "io/fs" + "mime/multipart" "net/http" "net/url" "reflect" @@ -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) + } +}