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.

Shepherd Advanced Notes
Shepherd Advanced Notes
Shepherd Advanced Notes
Why Spring MVC File Upload Parameters Disappear: Multipart, Filters, and Request Body Pitfalls

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, files

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

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

file-uploadtoken-authenticationspring-mvcfilterrequest-bodymultiparthiddenhttpmethodfilter
Shepherd Advanced Notes
Written by

Shepherd Advanced Notes

Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.