1. Overview

Most web applications have a use-case of performing operations like request logging, validation, or authentication. And, what’s more, such tasks are usually shared across a set of HTTP endpoints.

The good news is that the Spring web framework provides a filtering mechanism for precisely this purpose.

In this tutorial, we’ll learn how a filter-style task can be included or excluded from execution for a given set of URLs.

2. Filter for Specific URLs

Let’s say our web application needs to log some information about its requests, such as their paths and content types. One way to do this is by creating a logging filter.

2.1. Logging Filter

First, let’s create our logging filter in a LogFilter class that extends the OncePerRequestFilter class and implements the doFilterInternal method:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
  FilterChain filterChain) throws ServletException, IOException {
    String path = request.getRequestURI();
    String contentType = request.getContentType();
    logger.info("Request URL path : {}, Request content type: {}", path, contentType);
    filterChain.doFilter(request, response);
}

2.1. Rule-in Filter

Let’s assume that we need the logging task to be executed only for select URL patterns, namely /health, /faq/*. For this, we’ll register our logging filter using a FilterRegistrationBean such that it matches only the required URL patterns:

@Bean
public FilterRegistrationBean<LogFilter> logFilter() {
    FilterRegistrationBean<LogFilter> registrationBean = new FilterRegistrationBean<>();
    registrationBean.setFilter(new LogFilter());
    registrationBean.addUrlPatterns("/health","/faq/*");
    return registrationBean;
}

2.2. Rule-out Filter

If we want to exclude URLs from executing the logging task, we can achieve this easily in two ways:

  • For a new URL, ensure that it doesn’t match the URL patterns used by the filter
  • For an old URL for which logging was earlier enabled, we can modify the URL pattern to exclude this URL

3. Filter for All Possible URLs

We easily met our previous use case of including URLs in the LogFilter with minimal effort. However, it gets trickier if the Filter uses a wildcard (*) to match all the possible URL patterns.

In this circumstance, we’ll need to write the inclusion and exclusion logic ourselves.

3.1. Custom Filter

Clients can send useful information to the server by using the request headers. Let’s say our web application is currently operational only in the United States, which means we don’t want to process the requests coming from other countries.

Let’s further imagine that our web application indicates the locale via an X-Country-Code request header. Consequently, each request comes with this information, and we have a clear case for using a filter.

Let’s implement a Filter that checks for the header, rejecting requests that don’t meet our conditions:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {

    String countryCode = request.getHeader("X-Country-Code");
    if (!"US".equals(countryCode)) {
        response.sendError(HttpStatus.BAD_REQUEST.value(), "Invalid Locale");
        return;
    }

    filterChain.doFilter(request, response);
}

3.2. Filter Registration

To start with, let’s use the asterisk (*) wildcard to register our filter to match all possible URL patterns:

@Bean
public FilterRegistrationBean<HeaderValidatorFilter> headerValidatorFilter() {
    FilterRegistrationBean<HeaderValidatorFilter> registrationBean = new FilterRegistrationBean<>();
    registrationBean.setFilter(new HeaderValidatorFilter());
    registrationBean.addUrlPatterns("*");
    return registrationBean;
}

At a later point in time, we can exclude the URL patterns which are not required to execute the task of validating the locale request header information.

4. URL Exclusion

In this section, we’ll learn how to exclude URLs for our customer Filter.

4.1. Naive Strategy

Let’s again imagine that we have a web route at /health that can be used to do a ping-pong health check of the application.

So far, all requests will trigger our filter. As we can guess, it’s an overhead when it comes to our health check.

So, let’s simplify our /health requests by excluding them from the main body of our filter:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
  FilterChain filterChain) throws ServletException, IOException {
    String path = request.getRequestURI();
    if ("/health".equals(path)) {
        filterChain.doFilter(request, response);
        return;
    }

    String countryCode = request.getHeader("X-Country-Code");
    // ... same as before
}

We must note that adding this custom logic within the doFilter method introduces coupling between the /health endpoint and our filter. As such, it’s not optimal since we could break the filtering logic if we change the health check endpoint without making a corresponding change inside the doFilter method.

4.2. Using the shouldNotFilter Method

With the previous approach, we introduced tight coupling between the URL exclusion and the task execution logic for the filter. One could inadvertently introduce a bug in one part while intending to make changes to the other part.

Instead, we can isolate the two sets of logic by overriding the shouldNotFilter method:

@Override
protected boolean shouldNotFilter(HttpServletRequest request)
  throws ServletException {
    String path = request.getRequestURI();
    return "/health".equals(path);
}

As a result, the doInternalFilter() method honors the single responsibility principle:

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
  FilterChain filterChain) throws ServletException, IOException {
    String countryCode = request.getHeader("X-Country-Code");
    // ... same as before
}

5. Conclusion

In this tutorial, we’ve explored how to exclude URL pattern(s) from a servlet filter in a Spring Boot web application for two use cases, namely logging and request header validation.

Moreover, we learned that it gets tricky to rule-out a specific set of URLs for a filter that uses a * wildcard for matching all possible URL patterns.

As always, the complete source code for the tutorial is available over on GitHub.