Why Spring MVC File Upload Parameters Disappear: Multipart, Filters, and Request Body Pitfalls
The article details a puzzling Spring MVC file‑upload issue where parameters vanish when the token is sent in a header, tracing the problem through multipart/form‑data parsing, premature request‑body consumption by custom filters, request.getParameter() side effects, and the HiddenHttpMethodFilter, and provides the correct remediation steps.
Background
During a routine investigation of an old project's file‑upload endpoint, the author observed that placing a token in the query string allowed normal parsing of both ordinary fields and files, while placing the same token in the request header caused the backend to receive neither parameters nor files.
Two curl examples illustrate the difference:
curl --location --request PUT 'http://127.0.0.1:18000/test/upload?token=12345678910' \
--form 'id="123"' \
--form 'name="张三"' \
--form 'files=@"/path/to/file1.png"' \
--form 'files=@"/path/to/file2.png"' curl --location --request PUT 'http://127.0.0.1:18000/test/upload' \
--header 'token: 12345678910' \
--form 'id="123"' \
--form 'name="张三"' \
--form 'files=@"/path/to/file1.png"' \
--form 'files=@"/path/to/file2.png"'The controller method is a simple @PutMapping("/upload") that logs the received UserParam object, which contains ordinary fields and a List<MultipartFile> for the uploaded files.
@PutMapping("/upload")
public void testUploadFile(UserParam param) {
log.info("开始上传文件了");
log.info("参数:{}", param);
log.info("文件数:{}", param == null ? 0 : param.getFiles().size());
}How Spring MVC Parses multipart/form‑data
Spring MVC delegates multipart parsing to the servlet container. The simplified chain is:
DispatcherServlet → checkMultipart(request) → MultipartResolver.resolveMultipart(request) → StandardMultipartHttpServletRequest → request.getParts()The crucial call is request.getParts(). When the container (e.g., Tomcat) receives a multipart request, it eventually invokes:
org.apache.catalina.connector.RequestFacade#getParts()
org.apache.catalina.connector.Request#getParts()
org.apache.catalina.connector.Request#parseParts()If request.getParts() returns an empty collection, Spring MVC cannot bind any form fields or files.
Root Cause: A Filter Consumes the Original Request Body
The project defines a BodyTransferFilter that wraps the incoming HttpServletRequest to cache the request body:
@Component
@Slf4j
public class BodyTransferFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
RequestBodyWrapper requestBodyWrapper = null;
try {
requestBodyWrapper = new RequestBodyWrapper((HttpServletRequest) request);
} catch (Exception e) {
log.warn("requestBodyWrapper Error:", e);
}
chain.doFilter(requestBodyWrapper == null ? request : requestBodyWrapper, response);
}
}The wrapper reads the original input stream once in its constructor:
body = StreamUtils.copyToByteArray(request.getInputStream());Because the servlet container’s multipart parser relies on the original request’s input stream, this early consumption makes request.getParts() return null or an empty list.
Why the Token in Header Fails While the Query Param Works
A separate AuthFilter extracts the token. If the token is absent from the header, it falls back to request.getParameter("token"). Calling request.getParameter(...) on a multipart request triggers the servlet container to parse the multipart body early, caching the parts and parameters.
When the token is in the query string, the following chain occurs:
request.getHeader("token") → null
→ request.getParameter("token") → triggers early multipart parsing
→ parts are cached in the original request
→ later BodyTransferFilter reads the already‑consumed stream, but parts are already available
→ Controller receives id, name, filesWhen the token is in the header, request.getParameter("token") is never called, so the multipart body is not parsed before BodyTransferFilter consumes the stream, leading to empty parts.
Why Changing PUT to POST Makes All Tokens Work
The project also enables HiddenHttpMethodFilter, which reads the _method parameter on POST requests to support method overriding. Its core logic calls request.getParameter(this.methodParam), which again triggers early multipart parsing.
When the endpoint is changed to POST, the filter executes, causing the multipart body to be parsed before the custom BodyTransferFilter runs, so both header and query‑param tokens work. With PUT, the filter does not run, so the earlier problem resurfaces.
Correct Fix Direction
The essential rule is: Never read the raw request body before the servlet container has performed multipart parsing. If a request‑body‑caching filter is required, it should skip multipart requests:
@Component
@Slf4j
public class BodyTransferFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
String contentType = request.getContentType();
if (contentType != null && contentType.toLowerCase(Locale.ROOT).startsWith("multipart/")) {
chain.doFilter(request, response);
return;
}
RequestBodyWrapper wrapper = null;
try {
wrapper = new RequestBodyWrapper((HttpServletRequest) request);
} catch (Exception e) {
log.warn("requestBodyWrapper Error:", e);
}
chain.doFilter(wrapper == null ? request : wrapper, response);
}
}Additionally, declare the consumes type explicitly on upload endpoints and avoid relying on @ModelAttribute for multipart binding:
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public void testUploadFile(@ModelAttribute UserParam param) { ... }If the project does not need HTML form method overriding, disable HiddenHttpMethodFilter (e.g., spring.mvc.hiddenmethod.filter.enabled=false) to prevent its side‑effect of premature multipart parsing.
Summary
Spring MVC ultimately relies on request.getParts() for multipart parsing.
A custom RequestBodyWrapper that reads request.getInputStream() before request.getParts() destroys the container’s ability to parse the multipart body.
Overriding getInputStream() does not replace the container’s getParts() implementation.
Placing the token in a query parameter works because the authentication filter calls request.getParameter("token"), which incidentally triggers early multipart parsing.
Placing the token in a header avoids that call, exposing the premature‑read problem. HiddenHttpMethodFilter can also trigger early parsing via request.getParameter("_method"); disabling it removes this hidden dependency.
The proper solution is to let multipart requests pass untouched through any body‑caching filter and to configure the upload endpoint with an explicit consumes attribute.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Shepherd Advanced Notes
Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
