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 JS 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 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.