Sharing ServletRequest with Background Threads: Pitfalls and Solutions

Scenario Setup

Consider a minimal Spring Boot controller that offloads work to a simple background thread while passing the incoming HttpServletRequest object:

@GetMapping("/fetchData")
public String fetchData(HttpServletRequest incomingRequest) {
    String initialValue = incomingRequest.getParameter("code");
    System.out.println("Main thread value: " + initialValue);

    new Thread(() -> {
        try {
            // Simulate some delay to ensure the main request completes
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        
        // Attempting to read parameter in the background thread
        String backgroundValue = incomingRequest.getParameter("code");
        System.out.println("Background thread value: " + backgroundValue);
    }).start();

    return "Processing started";
}

When you hit the endpoint http://localhost:8080/fetchData?code=123 onnce, the output behaves as expected:

Main thread value: 123
Background thread value: 123

However, if you trigger the endpoint a second time immediately after, the backgroundValue in the new thread often prints null, even though the initialValue in the main thread correctly shows 123.

Root Cause Analysis

The issue stems from how Tomcat manages request objects. To optimize performance, Tomcat reuses request instances. When a request finishes processing, Tomcat calls a recycle() method on the request object.

  1. Parameter Parsing State: The request object contains a flag (e.g., didQueryParameters) indicating whether the query string has been parsed. It is set to false initially.
  2. First Call: The main thread processes the request, parses parameters (setting the flag to true), and returns. The background thread sleeps. Eventually, the background thread wakes up, calls getParameter(), sees the flag is true (already parsed), and returns the value successfully if the recycle hasn't happened yet.
  3. Recycling: Once the response is committed, Tomcat recycles the request object. This resets the internal buffer holding the raw query string (queryMB) to null and resets the parsed flag back to false, preparing the object for the next HTTP connection.
  4. The Conflict: If the background thread (from the first request) wakes up after Tomcat has recycled the object but before it processes a new request:
    • It calls getParameter().
    • The flag is false (due to recycle), so it tries to parse parameters.
    • However, the raw query buffer (queryMB) is now null because the object was cleaned up.
    • It finds no parameters, sets the flag to true (thinking it parsed an empty set), and returns null.
  5. Second Call Impact: When a new HTTP request arrives, Tomcat picks up the recycled request object. The background thread from the previous request is still running. When that old thread calls getParameter(), it sets the flag to true on the shared object. When the new main thread tries to get parameters, it sees the flag is true (set by the old background thread) and skips parsing the new query string entirely.

The Lifecycle Constraint

According to the Java Servlet Specification (Section 3.13), a request object is valid only within the scope of the service method or a filter's doFilter method.

"Containers common recycle request objects... maintaining references to request objects... outside the scope described above... may have indeterminate results."

Passing the raw request to a standard new Thread() violates this lifecycle because the thread runs outside the scope of the request processing.

The Correct Approach: AsyncContext

To handle asynchronous operations correctly without encountering recycling issues, use the standard Servlet asynchronous support:

@GetMapping("/fetchDataAsync")
public void fetchDataAsync(HttpServletRequest request) {
    String value = request.getParameter("code");
    System.out.println("Initial value: " + value);

    // Start the asynchronous context
    AsyncContext asyncContext = request.startAsync();
    
    // Set timeout if necessary, or handle in the thread
    asyncContext.setTimeout(10000);

    new Thread(() -> {
        try {
            // Perform background work
            Thread.sleep(5000);
            String backgroundValue = request.getParameter("code");
            System.out.println("Async thread value: " + backgroundValue);
            
            // Get response writer if needed
            asyncContext.getResponse().getWriter().write("Done");
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // Signal that the asynchronous operation is complete
            asyncContext.complete();
        }
    }).start();
}

By calling request.startAsync(), you extend the lifecycle of the request and response objects. Tomcat will not recycle the request object until asyncContext.complete() is invoked, ensuring that the background thread can safely access request attributes and parameters.

Tags: java Spring Boot servlet Concurrency Tomcat

Posted on Sun, 25 Oct 2026 16:45:11 +0000 by Teach