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.
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.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
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.