Uploading a malformed multi-part file results in 500 Internal Server Error even though sending malformed data is clearly a client error. Status code should be 400 Bad Request.

When a malformed multi-part file is received, the parsing fails in org.apache.tomcat.util.http.fileupload.FileUploadBase#parseRequest and IOFileUploadException is thrown. The IOFileUploadException is later wrapped into MultipartException in DispatcherServlet. Spring's ResponseEntityExceptionHandler does not handle MultipartException and therefore the response status code is ultimately set to 500 Internal Server Error.

Workaround

One workaround for this issue is to add an ExceptionHandler for MultipartException

@ControllerAdvice  
public class RestResponseExceptionHandler extends ResponseEntityExceptionHandler {  

    @ExceptionHandler(MultipartException.class)  
    public ResponseEntity<?> handleMultipartException(MultipartException exception) {  
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);  
 }  

}

Example of current behavior

package malformedmultipartfile;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.multipart.MultipartException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class RestResponseExceptionHandler extends ResponseEntityExceptionHandler {

    @ExceptionHandler(MultipartException.class)
    public ResponseEntity<?> handleMultipartException(MultipartException exception) {
        return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }

}
package malformedmultipartfile;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.*;

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class FileUploadControllerTest {

    @Autowired
    private TestRestTemplate testRestTemplate;

    @Test
    public void sendingMalformedMultipartFileResultsIn500() {
        HttpHeaders headers = new HttpHeaders();
        headers.add(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE + "; boundary=boundary-value");
        String body = "--boundary-value\r\n" +
                      "Content-Disposition: form-data; name=\"file\"; filename=\"testFile\"\r\n" +
                      "Content-Type: application/octet-stream\r\n\r\n" +
                      "something\r\n"; //"--boundary-value--\r\n" missing from the end
        ResponseEntity<Void> responseEntity = testRestTemplate.postForEntity(
                "/",
                new HttpEntity<>(body, headers),
                Void.class);

        assertEquals(HttpStatus.INTERNAL_SERVER_ERROR, responseEntity.getStatusCode());
    }
}

Comment From: poutsma

Unfortunately, MultipartException is not just thrown for malformed multipart messages, but also in other cases. For instance, it is also thrown when multipart functionality is used while the current request is not a multipart request. In that scenario a 5xx response code makes a lot more sense.

We could solve this by introducing a new subclass of MultipartException, and throw that new MalformedMultipartException whenever a malformed message is received, so that we can resolve it with a 4xx status. Unfortunately, we cannot do so either, because we do not have enough information to determine whether such a message is received. The only thing the Servlet container provides in such a case is an IOException, and as you can read in the Javadoc, that exception is thrown "if an I/O error occurred during the retrieval of the Part components of this request". That seems to imply a lot more scenarios than malformed multipart messages, so resolving all of these with a 4xx seems wrong.

Comment From: albertus82

Unfortunately, MultipartException is not just thrown for malformed multipart messages, but also in other cases. For instance, it is also thrown when multipart functionality is used while the current request is not a multipart request. In that scenario a 5xx response code makes a lot more sense.

Maybe I'm missing something, but if an API expects a multipart request and the client sends something else, in my opinion this should be HTTP 400 Bad Request.

Comment From: poutsma

Maybe I'm missing something, but if an API expects a multipart request and the client sends something else, in my opinion this should be HTTP 400 Bad Request.

Those kind of scenarios are resolved through content negotiation, and will result in 4xx errors.

The fact remains that Spring cannot determine whether a multi-part message is malformed in a Servlet environment, so I am closing this issue.