I have an existing REST API (has been around for years now and I'm the original author) which has multiple end-points. I just added a new end-point for uploading files. The end-point method resource is currently defined as (for just testing the end-point response):
@POST
@Path("/v1/upload")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response upload (
@FormDataParam("file")
InputStream file,
@FormDataParam("file")
FormDataContentDisposition disposition) {
return Response.ok().build();
}
As soon as a request comes in (sample shown below), Tomcat or Jersey sends back a 400 Bad Request response before even entering into the end-point method. The only thing written to any Tomcat log is in the general "access_log" where it inserts a log entry about the end-point request URL and the response code of 400. I have checked all other Tomcat logs and there is no other log entries anywhere which describe WHY a 400 is being returned.
Tomcat returns an HTML response, which seems to be a "one size fits all" error message, in the 400 response:
The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
Sample file upload request:
POST http://localhost/<removed path for SO post>/v1/upload?_=1744409038175&cid=m9dc0kkulvrjar8sxgl&tkn=974016a9-5a49-45d4-98c9-dba380735578 HTTP/1.1
Host: localhost
Connection: keep-alive
Content-Length: 717939
sec-ch-ua-platform: "Windows"
X-Requested-With: XMLHttpRequest
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36
Accept: application/json, text/plain, */*
sec-ch-ua: "Google Chrome";v="135", "Not-A.Brand";v="8", "Chromium";v="135"
Content-Type: multipart/form-data
sec-ch-ua-mobile: ?0
Origin: http://localhost
Sec-Fetch-Site: same-origin
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: http://localhost/<removed path for SO post>/
Accept-Encoding: gzip, deflate, br, zstd
Accept-Language: en-US,en-GB;q=0.9,en;q=0.8
Cookie: <a couple of cookies>
------WebKitFormBoundarydDDQsAC7Rqf3IM5I
Content-Disposition: form-data; name="file"; filename="Sample file.pdf"
Content-Type: application/pdf
<Encoded File Content Here>
----WebKitFormBoundarydDDQsAC7Rqf3IM5I--
If I change the end-point method signature (i.e., to remove the @FormDataParam annotations and only have 1 InputStream parameter) to the following:
@POST
@Path("/v1/upload")
@Consumes(MediaType.MULTIPART_FORM_DATA)
public Response upload (
InputStream file) {
return Response.ok().build();
}
The above will work and Tomcat won't send back a 400. However, I can't define any other method parameters or make use of the @FormDataParam annotation. Actually, with the above method signature, if I were to add the @FormDataParam("file")
to the above InputStream file
parameter, then I will get a 400 again.
I'm pretty sure I have all of the necessary JAR dependencies (relevant parts from the pom.xml below) for Jersey to handle multipart requests. I don't understand what is going wrong here, and I'm not getting enough log information to determine WHY!
pom.xml with Jersey-relevant dependencies:
<dependencyManagement>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.glassfish.jersey/jersey-bom -->
<dependency>
<groupId>org.glassfish.jersey</groupId>
<artifactId>jersey-bom</artifactId>
<version>3.1.10</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet-core</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.containers</groupId>
<artifactId>jersey-container-servlet</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.inject</groupId>
<artifactId>jersey-hk2</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-binding</artifactId>
</dependency>
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-multipart</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.glassfish.jaxb/jaxb-runtime -->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>4.0.5</version>
</dependency>
...
Actually, it seems that anytime the @FormDataParam annotation is used, a 400 response occurs.
OK - this took some digging to figure out what was going wrong.
The problem was that the "part" in the multipart request wasn't being recognized by Jersey (via any FormDataParam
annotations, OR if I were to directly access the request parts via the current HttpServletRequest
) because the request coming from the browser wasn't indicating the "boundary" for the first/primary Content-Type header (which the part and its boundary name is dynamically being added by the browser later in the request for the file "part", i.e., some random boundary name like "------WebKitFormBoundarymUBOEP84Y3yt6c4A").
The reason that the browser wasn't indicating the correct (or any) boundary for the primary/first Content-Type header in the request was because Angular (via the $http provider) will automatically set the Content-Type to "application/json;charset=utf-8" during a POST/PUT request if the "data" provided to the $http function is an Object that doesn't resolve to the string representation of "[object File]", and no other Content-Type header has been explicitly included/added in the $http call.
In my case, the data object's string representation is "[object FormData]", which causes the primary/first Content-Type header to be set to "application/json" by Angular (which leads to Jersey to not parse the request for any "parts" that may have been sent), instead of being set correctly to Content-Type: multipart/form-data; boundary=----WebKitFormBoundarymUBOEP84Y3yt6c4A
by the browser (by not including ANY Content-Type header in my JS code during the $http POST call). If I were to explicitly set the Content-Type header to "multipart/form-data", then it will still fail because it's missing the "boundary" attribute, and I don't know the value of the boundary because it's being dynamically generated by the browser.
To fix the issue: I needed to remove the default headers that Angular was automatically applying to all POST/PUT requests by deleting the associated default properties from the Angular "$httpProvider" config object:
delete $httpProvider.defaults.headers.post["Content-Type"];
delete $httpProvider.defaults.headers.put["Content-Type"];
Now, I didn't want to set another, default Content-Type header for ALL POST/PUT requests because I don't want some other incorrect content type to end up being sent in other, non-file-upload cases - so I just deleted the existing, hard-coded POST/PUT defaults (with the "delete" statements above), and then I ended up setting my own defaults for content type handling during my POST/PUT calls to $http based upon similar, but different, logic from what Angular was doing.
I also had to replace the default Angular request transformer during the Angular "config" hook with one that will properly handle FormData objects during POST/PUT requests, somewhat following Angular's original logic for parameterizing JSON objects to be added as form parameters to POST/PUT requests:
$httpProvider.defaults.transformRequest = function (data) {
if (angular.isObject(data)) {
let strData = String(data);
if (strData == "[object File]" || strData == "[object FormData]") {
return data;
}
return $httpParamSerializerProvider.$get()(data);
}
return data;
};
With the Content-Type header being set correctly with a boundary in the POST file upload requests from the browser, Jersey is now parsing the requests correctly, and I can use the @FormDataParam annotations without Jersey automatically sending back a 400 response when it thinks that the request is not a multipart request.