javaspringspring-boot

Interceptor for http header params


I need to create a code in Spring Boot for intercepting http header params. I have the following requirement for Java class body:

import java.util.HashMap;
import java.util.Map;

public final class InternalHeadersContext {

    public static final String X_INTERNAL_CORRELATION_ID_HEADER = "X-INTERNAL-CORRELATION-ID";
    public static final String X_INTERNAL_REQUEST_ID_HEADER = "X-INTERNAL-REQUEST-ID";
    public static final String X_INTERNAL_USER_ID_HEADER = "X-INTERNAL-USER-ID";

    public static void clear() {
        .....
    }

    public static void initContext() {
        ......
    }

    public static void setInternalHeaders(Map<String, String> internalHeaders) {
        .....
    }

    public static Map<String, String> getInternalHeaders() {
        ......
    }
}

....
String value = (String) InternalHeadersContext.getInternalHeaders().getOrDefault("X-INTERNAL-USER-ID", "test_value");

I implemented this:

public final class InternalHeadersContext {

    public static final String X_INTERNAL_CORRELATION_ID_HEADER = "X-INTERNAL-CORRELATION-ID";
    public static final String X_INTERNAL_REQUEST_ID_HEADER = "X-INTERNAL-REQUEST-ID";
    public static final String X_INTERNAL_USER_ID_HEADER = "X-INTERNAL-USER-ID";

    // Initialize ThreadLocal with a map containing all keys (values default to null)
    private static final ThreadLocal<Map<String, String>> context = ThreadLocal.withInitial(() -> {
        Map<String, String> initialMap = new HashMap<>();
        initialMap.put(X_INTERNAL_CORRELATION_ID_HEADER, null);
        initialMap.put(X_INTERNAL_REQUEST_ID_HEADER, null);
        initialMap.put(X_INTERNAL_USER_ID_HEADER, null);
        return initialMap;
    });

    public static void clear() {
        context.remove();
    }

    public static void initContext() {
        // No-op: ThreadLocal already initializes with all keys
    }

    public static void setInternalHeaders(Map<String, String> internalHeaders) {
        if (internalHeaders == null) {
            clear();
        } else {
            // Create a new map with all required keys, merging with incoming headers
            Map<String, String> newMap = new HashMap<>();
            newMap.put(X_INTERNAL_CORRELATION_ID_HEADER, internalHeaders.getOrDefault(X_INTERNAL_CORRELATION_ID_HEADER, null));
            newMap.put(X_INTERNAL_REQUEST_ID_HEADER, internalHeaders.getOrDefault(X_INTERNAL_REQUEST_ID_HEADER, null));
            newMap.put(X_INTERNAL_USER_ID_HEADER, internalHeaders.getOrDefault(X_INTERNAL_USER_ID_HEADER, null));
            context.set(newMap);
        }
    }

    public static Map<String, String> getInternalHeaders() {
        return context.get();
    }
}
.....
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

@Component
public class InternalHeadersFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest httpRequest = (HttpServletRequest) request;

        // Extract headers from the request
        Map<String, String> headers = new HashMap<>();
        headers.put(InternalHeadersContext.X_INTERNAL_CORRELATION_ID_HEADER,
                httpRequest.getHeader(InternalHeadersContext.X_INTERNAL_CORRELATION_ID_HEADER));
        headers.put(InternalHeadersContext.X_INTERNAL_REQUEST_ID_HEADER,
                httpRequest.getHeader(InternalHeadersContext.X_INTERNAL_REQUEST_ID_HEADER));
        headers.put(InternalHeadersContext.X_INTERNAL_USER_ID_HEADER,
                httpRequest.getHeader(InternalHeadersContext.X_INTERNAL_USER_ID_HEADER));

        // Update the context with headers
        InternalHeadersContext.setInternalHeaders(headers);

        try {
            chain.doFilter(request, response);
        } finally {
            InternalHeadersContext.clear(); // Cleanup to avoid memory leaks
        }
    }
}

Full code: https://github.com/rcbandit111/error_handler_poc

I have implemented the interceptor code into a separate jar and I use it into parent proejct.

When I run the code I can't get the http value. I get NPE. What is the proper way to get http params from request?


Solution

  • Your cause of the NPE is here:

    @Component
    @ConditionalOnClass({InternalHeadersContext.class})
    public class SpringSecurityAuditorAware implements AuditorAware<String>
    
    {
    
        public SpringSecurityAuditorAware() {
        }
    
        public Optional<String> getCurrentAuditor() {
            String userName = (String) InternalHeadersContext.getInternalHeaders().getOrDefault("X-INTERNAL-USER-ID", "test");
    
    
    
            return Optional.of(userName); // it throws NPE, because username is always null
        }
    }
    

    Solution:

    1. You need to add the InternalHeadersFilter to your configuration so that the filter is applied in the parent context. You can do it like this:
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Import({DatabaseConfiguration.class, InternalHeadersFilter.class}) // Filter added
    public @interface EnableTracing {
    }
    

    This will solve the issue with the NPE, but only if the X-INTERNAL-USER-ID header is present in the request.

    1. Why do we get NPE if the header is not present? It's because you're initializing the context with default null values for each expected key in InternalHeadersContext, like this:
        // Initialize ThreadLocal with a map containing all keys (values default to null)
        private static final ThreadLocal<Map<String, String>> context = ThreadLocal.withInitial(() -> {
            Map<String, String> initialMap = new HashMap<>();
            initialMap.put(X_INTERNAL_CORRELATION_ID_HEADER, null);
            initialMap.put(X_INTERNAL_REQUEST_ID_HEADER, null);
            initialMap.put(X_INTERNAL_USER_ID_HEADER, null);
            return initialMap;
        });
    
    

    or here:

    public static void setInternalHeaders(Map<String, String> internalHeaders) {
            if (internalHeaders == null) {
                clear();
            } else {
                // Create a new map with all required keys, merging with incoming headers
                Map<String, String> newMap = new HashMap<>();
                newMap.put(X_INTERNAL_CORRELATION_ID_HEADER, internalHeaders.getOrDefault(X_INTERNAL_CORRELATION_ID_HEADER, null));
                newMap.put(X_INTERNAL_REQUEST_ID_HEADER, internalHeaders.getOrDefault(X_INTERNAL_REQUEST_ID_HEADER, null));
                newMap.put(X_INTERNAL_USER_ID_HEADER, internalHeaders.getOrDefault(X_INTERNAL_USER_ID_HEADER, null));  // null can be set as value
                context.set(newMap);
            }
        }
    

    This causes a problem in SpringSecurityAuditorAware. Even if the header is not sent, the map still contains the key X-INTERNAL-USER-ID with a null value.

    So when you call .getOrDefault("X-INTERNAL-USER-ID", "test"), it doesn't fall back to "test" — because the key is present, even though the value is null.

    To fix this, either:

    or

    public Optional<String> getCurrentAuditor() {
            
            String userFromContext = InternalHeadersContext.getInternalHeaders().get("X-INTERNAL-USER-ID");
    
            String userName = userFromContext != null 
                    ? userFromContext 
                    : "test";
            
            return Optional.of(userName);
        }
    

    Some refactor suggestions:

    InternalHeadersContext:

    import java.util.HashMap;
    import java.util.Map;
    
    public final class InternalHeadersContext {
        
        // Initialize ThreadLocal with a new HashMap, no need to put null values there
        private static final ThreadLocal<Map<String, String>> context = ThreadLocal.withInitial(HashMap::new);
    
        public static void setInternalHeaders(Map<String, String> internalHeaders) {
            context.get().putAll(internalHeaders);
        }
    
        public static Map<String, String> getInternalHeaders() {
            return context.get();
        }
    
        public static void clear() {
            context.remove();
        }
    }
    

    InternalHeadersFilter:

    import jakarta.servlet.FilterChain;
    import jakarta.servlet.ServletException;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import org.springframework.stereotype.Component;
    import org.springframework.web.filter.OncePerRequestFilter;
    
    import java.io.IOException;
    import java.util.HashMap;
    import java.util.Map;
    
    @Component
    public class InternalHeadersFilter extends OncePerRequestFilter { // changed from Filter
    
        private static final String X_INTERNAL_CORRELATION_ID_HEADER = "X-INTERNAL-CORRELATION-ID";
        private static final String X_INTERNAL_REQUEST_ID_HEADER = "X-INTERNAL-REQUEST-ID";
        private static final String X_INTERNAL_USER_ID_HEADER = "X-INTERNAL-USER-ID";
    
    
        @Override
        public void doFilterInternal(HttpServletRequest request,
                                     HttpServletResponse response,
                                     FilterChain filterChain) throws ServletException, IOException {
            // Extract headers from the request
    
            Map<String, String> map = new HashMap<>();
    
            putIfNotNull(X_INTERNAL_CORRELATION_ID_HEADER, request, map); // put into map ONLY if not null to be able to use getOrDefault
            putIfNotNull(X_INTERNAL_REQUEST_ID_HEADER, request, map);
            putIfNotNull(X_INTERNAL_USER_ID_HEADER, request, map);
    
            // Update the context with headers
            InternalHeadersContext.setInternalHeaders(map);
    
            
            try {
                filterChain.doFilter(request, response);
            } finally {
                InternalHeadersContext.clear(); // Cleanup to avoid memory leaks 
            }
        }
    
        private void putIfNotNull(String headerName, HttpServletRequest request, Map<String, String> map) {
            String headerValue = request.getHeader(headerName);
    
            if (headerValue != null) {
                map.put(headerName, headerValue);
            }
        }
    }