Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions pkg/ffapi/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ func (hs *HandlerFactory) getFilePart(req *http.Request) (*multipartState, error
}
for {
part, err := mpr.NextPart()
// Not errors.Is: NextPart returns a bare io.EOF only at the final boundary, while a
// truncated body comes back wrapped and should keep the FF00161 read error
if err == io.EOF {
return nil, i18n.NewError(ctx, i18n.MsgMultiPartFormNoFile)
}
if err != nil {
return nil, i18n.WrapError(ctx, err, i18n.MsgMultiPartFormReadError)
}
Expand Down
108 changes: 108 additions & 0 deletions pkg/ffapi/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,114 @@ func TestMultipartBadData(t *testing.T) {
assert.Regexp(t, "FF00161", resJSON["error"])
}

func TestMultipartNoFilename(t *testing.T) {
s, _, done := newTestServer(t, []*Route{{
Name: "testRoute",
Path: "/test",
Method: "POST",
FormParams: []*FormParam{
{Name: "param1", Description: ExampleDesc},
},
JSONInputValue: nil,
JSONOutputValue: func() interface{} { return make(map[string]interface{}) },
JSONOutputCodes: []int{http.StatusCreated},
FormUploadHandler: func(r *APIRequest) (output interface{}, err error) {
return nil, nil
},
}}, "", nil)
defer done()

b := new(bytes.Buffer)
w := multipart.NewWriter(b)
writer, err := w.CreateFormField("param1")
assert.NoError(t, err)
writer.Write([]byte(`testing`))
writer, err = w.CreateFormFile("file", "")
assert.NoError(t, err)
writer.Write([]byte(`some data`))
w.Close()

req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/test", s.Addr()), b)
assert.NoError(t, err)
req.Header.Set("Content-Type", w.FormDataContentType())
res, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.Equal(t, 400, res.StatusCode)
var resJSON map[string]interface{}
json.NewDecoder(res.Body).Decode(&resJSON)
assert.Regexp(t, "FF00262", resJSON["error"])
}

func TestMultipartNoFilePart(t *testing.T) {
s, _, done := newTestServer(t, []*Route{{
Name: "testRoute",
Path: "/test",
Method: "POST",
FormParams: []*FormParam{
{Name: "param1", Description: ExampleDesc},
},
JSONInputValue: nil,
JSONOutputValue: func() interface{} { return make(map[string]interface{}) },
JSONOutputCodes: []int{http.StatusCreated},
FormUploadHandler: func(r *APIRequest) (output interface{}, err error) {
return nil, nil
},
}}, "", nil)
defer done()

b := new(bytes.Buffer)
w := multipart.NewWriter(b)
writer, err := w.CreateFormField("param1")
assert.NoError(t, err)
writer.Write([]byte(`testing`))
w.Close()

req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/test", s.Addr()), b)
assert.NoError(t, err)
req.Header.Set("Content-Type", w.FormDataContentType())
res, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.Equal(t, 400, res.StatusCode)
var resJSON map[string]interface{}
json.NewDecoder(res.Body).Decode(&resJSON)
assert.Regexp(t, "FF00262", resJSON["error"])
}

func TestMultipartTruncated(t *testing.T) {
s, _, done := newTestServer(t, []*Route{{
Name: "testRoute",
Path: "/test",
Method: "POST",
FormParams: []*FormParam{
{Name: "param1", Description: ExampleDesc},
},
JSONInputValue: nil,
JSONOutputValue: func() interface{} { return make(map[string]interface{}) },
JSONOutputCodes: []int{http.StatusCreated},
FormUploadHandler: func(r *APIRequest) (output interface{}, err error) {
return nil, nil
},
}}, "", nil)
defer done()

b := new(bytes.Buffer)
w := multipart.NewWriter(b)
writer, err := w.CreateFormField("param1")
assert.NoError(t, err)
writer.Write([]byte(`testing`))
// No w.Close(), so the body ends without a closing boundary

req, err := http.NewRequest("POST", fmt.Sprintf("http://%s/test", s.Addr()), b)
assert.NoError(t, err)
req.Header.Set("Content-Type", w.FormDataContentType())
res, err := http.DefaultClient.Do(req)
assert.NoError(t, err)
assert.Equal(t, 400, res.StatusCode)
var resJSON map[string]interface{}
json.NewDecoder(res.Body).Decode(&resJSON)
assert.Regexp(t, "FF00161", resJSON["error"])
}

func TestTextPlain201(t *testing.T) {
s, _, done := newTestServer(t, []*Route{{
Name: "testRoute",
Expand Down
1 change: 1 addition & 0 deletions pkg/i18n/en_base_error_messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,4 +196,5 @@ var (
MsgFFExtensionsInvalidEncoding = ffe("FF00259", "Invalid extension key '%s' - extension keys must follow the format 'x-<name>'", http.StatusBadRequest)
MsgInvalidCIDR = ffe("FF00260", "Invalid CIDR '%s' in denylist configuration", http.StatusBadRequest)
MsgConnectionToCIDRBlocked = ffe("FF00261", "Connection to '%s' blocked by CIDR denylist (%s)", http.StatusForbidden)
MsgMultiPartFormNoFile = ffe("FF00262", "No file found in multi-part form input. The file part must have a filename", http.StatusBadRequest)
)