Backend Development 10 min read

Resolving Missing Request Headers in Spring MVC Multithreaded Environments

This article analyzes why Spring MVC fails to retrieve request header fields in multithreaded scenarios, explains the underlying ThreadLocal storage mechanism, critiques a common but flawed solution, and presents reliable approaches using CountDownLatch, manual RequestContextHolder propagation, and request caching to ensure header availability across threads.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Resolving Missing Request Headers in Spring MVC Multithreaded Environments

In typical Spring MVC development, custom request header parameters are accessed via RequestContextHolder and its getRequestAttributes method, which returns a ServletRequestAttributes object containing the current HttpServletRequest and HttpServletResponse . The following utility method demonstrates how the request is obtained:

public static HttpServletRequest getRequest() {
    HttpServletRequest httpServletRequest = null;
    try {
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        if (requestAttributes instanceof ServletRequestAttributes) {
            ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) requestAttributes;
            httpServletRequest = servletRequestAttributes.getRequest();
        }
    } catch (Exception e) {
        // Log the exception without propagating it to avoid breaking business logic
        log.error("Error obtaining HttpServletRequest:", e);
    }
    // Return the request object, or null if acquisition failed
    return httpServletRequest;
}

The request object is stored in a ThreadLocal , meaning each thread has its own copy. When a new child thread is started, it cannot access the parent thread's request, leading to a NullPointerException when the child attempts to read a custom header such as X-CLIENT-LANG . The following controller reproduces the problem:

@RestController
@RequestMapping("/test")
@Slf4j
public class TestController {
    @GetMapping("/missing-request-header")
    public String getMissingRequestHeader() {
        // Main thread reads the header
        String mainThreadLanguages = ServletUtils.getLanguagesExistProblem();
        log.info("Main thread header: {}", mainThreadLanguages);
        new Thread(() -> {
            // Child thread reads the header
            String subThreadLanguages = ServletUtils.getLanguagesExistProblem();
            log.info("Child thread header: {}", subThreadLanguages);
        }).start();
        return "success";
    }
}

The utility class used above simply fetches the header value:

@Slf4j
public class ServletUtils {
    private final static String X_CLIENT_LANG = "X-CLIENT-LANG";
    public static String getLanguagesExistProblem() {
        HttpServletRequest request = getRequest();
        Assert.notNull(request);
        String lang = request.getHeader(X_CLIENT_LANG);
        if (StrUtil.isNotBlank(lang)) {
            return lang;
        }
        return "zh-cn";
    }
}

Because the request is stored in a regular ThreadLocal , the child thread cannot inherit the header, causing the assertion to fail.

One widely circulated fix suggests setting RequestContextHolder.setRequestAttributes(attributes, true) so that the attributes are stored in an InheritableThreadLocal , allowing child threads to inherit the request. However, this approach only works if the parent thread remains alive until the child finishes, which is not guaranteed in real‑world scenarios.

A more reliable solution is to coordinate threads using a CountDownLatch so that the parent waits for the child to complete:

@GetMapping("/get-request-header-in-thread")
public String getRequestHeaderInThread() {
    String mainThreadLanguages = ServletUtils.getLanguages();
    CountDownLatch latch = new CountDownLatch(1);
    log.info("Main thread header: {}", mainThreadLanguages);
    new Thread(() -> {
        try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { throw new RuntimeException(e); }
        String subThreadLanguages = ServletUtils.getLanguages();
        log.info("Child thread header: {}", subThreadLanguages);
        latch.countDown();
    }).start();
    try { latch.await(); } catch (InterruptedException e) { throw new RuntimeException(e); }
    log.info("Both threads have finished");
    return "success";
}

When the child thread performs time‑consuming work, this synchronization can become a performance bottleneck. An alternative is to manually propagate the ServletRequestAttributes to the child thread:

@GetMapping("/get-request-header-in-async-thread/{isJoin}")
public String getRequestHeaderInThread() {
    String mainThreadLanguages = ServletUtils.getLanguages();
    log.info("Main thread header: {}", mainThreadLanguages);
    ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    new Thread(() -> {
        RequestContextHolder.setRequestAttributes(servletRequestAttributes);
        try { TimeUnit.SECONDS.sleep(5); } catch (InterruptedException e) { throw new RuntimeException(e); }
        String subThreadLanguages = ServletUtils.getLanguages();
        log.info("Child thread header: {}", subThreadLanguages);
    }).start();
    return "success";
}

By explicitly setting the request attributes in the child thread, the header becomes accessible without relying on inheritance.

Summary : The root cause of missing request headers in Spring MVC multithreaded contexts is the default use of ThreadLocal for request storage. Setting RequestContextHolder to use an InheritableThreadLocal can share the request but only while the parent thread lives. Safer approaches include synchronizing threads with CountDownLatch or manually transferring ServletRequestAttributes to child threads, ensuring reliable header access across concurrent execution paths.

backendJavamultithreadingthreadlocalSpring MVCRequestContextHolder
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

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