source: https://www.elegantthemes.com/blog/wp-content/uploads/2014/10/UploadLimit-Header.png

Spring, file upload via multipart/form-data

piotr szybicki
12 developer labors

--

Two days ago I started on a new story on a project that i’m involved in. And frankly I told on stand up on Tuesday that I will be done in 2 hours and after that I will pick up a new task from the back log. Today is Saturday evening I spend total of 26 hours on that problem and ultimately I had to debug spring source code to get to the bottom of this. And I hadn't his much fun in a long time. And I thought I share what I have learned.

The requirement was: In a web page build a form that will contain one text input field and one input for file upload. Simple enough. This is the form:

<form id=”data” method=”PATCH” action=”/f” >
<input type="text" required name="company" >
<input type="file" required name="definition" />
</form>

and the metod in my RestController

@RequestMapping(value = "/f",method = PATCH)
public void upload(
@RequestPart("definition") MultipartFile definition,
@RequestPart("company") String company
) {...}

Notice the method it is PATCH (as per requirement) not POST it will become relevant in a little bit. And part of the requirement it was to be ajax request not form submit. At first i used jquery but as I was going lower in to the problem i used the following code:

var fileInput = ...; //this is html element that holds the files
var textInput = ...; //thi is the string
var fd = new FormData();
fd.append('definition',fileInput.files[0]);
fd.append('name', textInput );
xhr = new XMLHttpRequest();
xhr.open( 'PATCH', uploadForm.action, true );
xhr.send( fd );

But whatever I do I couldn’t make it to work. I always got the following exception:

MissingServletRequestPartException: Required request part ‘definition’ is not present

First thing i did was to streap this problem to the bare mimum. So I changed the request type to post and deleted the textInput. That seem to work after I changed my implementation of MultiPart resolver from org.springframework.web.multipart.support.StandardServletMultipartResolver to org.springframework.web.multipart.commons.CommonsMultipartResolver

@Configuration
public class MyConfig {

@Bean
public MultipartResolver multipartResolver() {
return new CommonsMultipartResolver();
}
}

That also requires addition of commons-fileupload library to your classpath. Ok so I had the basic example working but whenever I added a string variable back the error returned (now the the error was about the string field not the file field). For some reason the multi part request resolver couldn’t find that part. Here what actually came to rescue was the documentation to the FormData object. In it you can read that the append method (i was using it, as that is most of the examples on the net that I have seen). Append method invoked on the FormData object accepts two parameters name and value (there is a third but is not important) The value field can be a USVString or Blob (including subclasses such as File). When I changed the java code to:

var fileInput = ...; //this is html element that holds the files
var textInput = = new Blob(['the info'], {
type: 'text/plain'
});
; //thi is the stringvar fd = new FormData();
fd.append('definition',fileInput.files[0]);
fd.append('name', textInput );
xhr = new XMLHttpRequest();
xhr.open( 'PATCH', uploadForm.action, true );
xhr.send( fd );

It suddenly started to work :). So what was going on. As explanation is really helpful here. To answer this question we have to take a look at what browser is sending.

— — — WebKitFormBoundaryHGN3YjdgsELbgmZHContent-Disposition: form-data; name=”definition”; filename=”test.csv” Content-Type: text/csvthis is the content of a file, browser hides it.
— — — WebKitFormBoundaryHGN3YjdgsELbgmZH Content-Disposition: form-data; name=”name”
this is the string
— — — WebKitFormBoundaryHGN3YjdgsELbgmZH —

Can you notice what is missing in the content disposition header? Two things filename (that one is not so important, I told you that append accepts 3 parameters filename is that third). And much more important Content-Type. Every entry in multi-part form, during processing by the servlet , becomes a MultipartFile. And I order to be converted to that file it has to have a content type. Deep inside the commons-fileupload library there is a line of code:

String subContentType = headers.getHeader(CONTENT_TYPE);
if (subContentType != null ... ){}

This get’s the content type and if it’s null processing goes via different path that turn our part into MultipartParameter (that is put in a different map and spring is not looking at it) instead of MultipartFile. And later spring creates the separate instance for every parameter form the form that we want to bind when invoking our rest method. Using class RequestPartServletServerHttpRequest. In it constructor we can find the where the exception is thrown.

HttpHeaders headers = this.multipartRequest.getMultipartHeaders(this.partName);
if (headers == null) {
throw new MissingServletRequestPartException(partName);
}

I spare the reader the rest of the spring source code, it is just important to know that getMultipartHeaders only looks at multipart files not parameters. So that is why adding the blob with specific type solve issue number 1 (Most tutorials on the internet are missing that part, they only deal with the form that has one file field).

var textInput = = new Blob(['the info'], {
type: 'text/plain'
});

Now taking a step back i mentioned that i had to switch to using POST when i changed to PATCH the problem came back. Error was the same. And I was puzzled. So I dived back to the source code (After all thi is ultimate documentation). The answer was a bit simpler this time. Remember that I switched to the CommonsMultipartResolver at the beginning of this article. It turns out that during request processing this method is invoked:

public static final boolean isMultipartContent(
HttpServletRequest request) {
if (!POST_METHOD.equalsIgnoreCase(request.getMethod())) {
return false;
}
return FileUploadBase.isMultipartContent(new ServletRequestContext(request));
}

So where is waldo? If you study the code for 5 seconds it is obvious. If it’s not a POST request it is immediately determined that this request do not have multipart content. I got around that by overriding method that is calling the static methods above.

So now by config bean looks like this:

@Bean
public MultipartResolver multipartResolver() {
return new CommonsMultipartResolverMine();
}


public static class CommonsMultipartResolverMine extends CommonsMultipartResolver {

@Override
public boolean isMultipart(HttpServletRequest request) {
final String header = request.getHeader("Content-Type");
if(header == null){
return false;
}
return header.contains("multipart/form-data");
}

}

Summary

And that is it. The whole things works like a charm :) I think the biggest lesson from all of it is to not be afraid to investigate in depth. There is a crop of developers out there that would just try solution from Stack Overflow (you would not believe how much bullshit is there) until one of them works. This is not the correct approach. In order to be a good developer you have to understand both the consequences of a change and the reason for bug. Before you type a single letter.

--

--

piotr szybicki
12 developer labors

Piotr Szybicki’s, Programmer, Java Developer, ML Entusiast