spring-bootjerseyjax-rsjersey-2.0spring-jersey

Can no longer obtain form data from HttpServletRequest SpringBoot 2.2, Jersey 2.29


We have a SpringBoot application and are using Jersey to audit incoming HTTP requests.

We implemented a Jersey ContainerRequestFilter to retrieve the incoming HttpServletRequest and use the HttpServletRequest's getParameterMap() method to extract both query and form data and place it in our audit.

This aligns with the javadoc for the getParameterMap():

"Request parameters are extra information sent with the request. For HTTP servlets, parameters are contained in the query string or posted form data."

And here is the documentation pertaining to the filter:

https://eclipse-ee4j.github.io/jersey.github.io/documentation/latest/user-guide.html#filters-and-interceptors

Upon updating SpringBoot, we found that the getParameterMap() no longer returned form data, but still returned query data.

We found that SpringBoot 2.1 is the last version to support our code. In SpringBoot 2.2 the version of Jersey was updated 2.29, but upon reviewing the release notes we don't see anything related to this.

What changed? What would we need to change to support SpringBoot 2.2 / Jersey 2.29?

Here is a simplified version of our code:

JerseyRequestFilter - our filter

import javax.annotation.Priority;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.ResourceInfo;
import javax.ws.rs.core.Context;
import javax.ws.rs.ext.Provider;
...

@Provider
@Priority(Priorities.AUTHORIZATION)
public class JerseyRequestFilter implements ContainerRequestFilter {

    @Context
    private ResourceInfo resourceInfo;

    @Context
    private HttpServletRequest httpRequest;
    ...
    
    public void filter(ContainerRequestContext context) throws IOException {
        ...
        requestData =  new RequestInterceptorModel(context, httpRequest, resourceInfo);
        ...
    }   
    ...
}   

RequestInterceptorModel - the map is not populating with form data, only query data

import lombok.Data;
import org.glassfish.jersey.server.ContainerRequest;
import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ResourceInfo;
...

@Data
public class RequestInterceptorModel {

    private Map<String, String[]> parameterMap;
    ...
    
    public RequestInterceptorModel(ContainerRequestContext context, HttpServletRequest httpRequest, ResourceInfo resourceInfo) throws AuthorizationException, IOException {
        ...
        setParameterMap(httpRequest.getParameterMap());
        ...
    }
    ...     
}

JerseyConfig - our config

import com.xyz.service.APIService;
import io.swagger.jaxrs.config.BeanConfig;
import io.swagger.jaxrs.listing.ApiListingResource;
import io.swagger.jaxrs.listing.SwaggerSerializers;
import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.wadl.internal.WadlResource;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
...

@Component
public class JerseyConfig extends ResourceConfig {
    ...

    public JerseyConfig() {
        this.register(APIService.class);
        ...
        // Access through /<Jersey's servlet path>/application.wadl
        this.register(WadlResource.class);
        this.register(AuthFilter.class);
        this.register(JerseyRequestFilter.class);
        this.register(JerseyResponseFilter.class);
        this.register(ExceptionHandler.class);
        this.register(ClientAbortExceptionWriterInterceptor.class);
    }

    @PostConstruct
    public void init() 
        this.configureSwagger();
    }

    private void configureSwagger() {
        ...
    }
}

Full Example

Here are the steps to recreate with our sample project:

  1. download the source from github here:
 git clone https://github.com/fei0x/so-jerseyBodyIssue
  1. navigate to the project directory with the pom.xml file
  2. run the project with:
 mvn -Prun
  1. in a new terminal run the following curl command to test the web service
  curl -X POST \
  http://localhost:8012/api/jerseyBody/ping \
  -H 'content-type: application/x-www-form-urlencoded' \
  -d param=Test%20String
  1. in the log you will see the form parameters
  2. stop the running project, ctrl-C
  3. update the pom's parent version to the newer version of SpringBoot
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.15.RELEASE</version>

to

<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.9.RELEASE</version>
  1. run the project again:
 mvn -Prun
  1. invoke the curl call again:
  curl -X POST \
  http://localhost:8012/api/jerseyBody/ping \
  -H 'content-type: application/x-www-form-urlencoded' \
  -d param=Test%20String
  1. This time the log will be missing the form parameters

Solution

  • I will post this answer, even though @Amir Schnell already posted a working solution. The reason is that I am not quite sure why that solution works. Definitely, I would rather have a solution that just requires adding a property to a property file, as opposed to having to alter code as my solution does. But I am not sure if I am comfortable with a solution that works opposite of how my logic sees it's supposed to work. Here's what I mean. In your current code (SBv 2.1.15), if you make a request, look at the log and you will see a Jersey log

    2020-12-15 11:43:04.858 WARN 5045 --- [nio-8012-exec-1] o.g.j.s.WebComponent : A servlet request to the URI http://localhost:8012/api/jerseyBody/ping contains form parameters in the request body but the request body has been consumed by the servlet or a servlet filter accessing the request parameters. Only resource methods using @FormParam will work as expected. Resource methods consuming the request body by other means will not work as expected.

    This has been a known problem with Jersey and I have seen a few people on here asking why they can't get the parameters from the HttpServletRequest (this message is almost always in their log). In your app though, even though this is logged, you are able to get the parameters. It is only after upgrading your SB version, and then not seeing the log, that the parameters are unavailable. So you see why I am confused.

    Here is another solution that doesn't require messing with filters. What you can do is use the same method that Jersey uses to get the @FormParams. Just add the following method to your RequestInterceptorModel class

    private static Map<String, String[]> getFormParameterMap(ContainerRequestContext context) {
        Map<String, String[]> paramMap = new HashMap<>();
        ContainerRequest request = (ContainerRequest) context;
        if (MediaTypes.typeEqual(MediaType.APPLICATION_FORM_URLENCODED_TYPE, request.getMediaType())) {
            request.bufferEntity();
            Form form = request.readEntity(Form.class);
            MultivaluedMap<String, String> multiMap = form.asMap();
            multiMap.forEach((key, list) -> paramMap.put(key, list.toArray(new String[0])));
        }
        return paramMap;
    }
    

    You don't need the HttpServletRequest at all for this. Now you can set your parameter map by calling this method instead

    setParameterMap(getFormParameterMap(context));
    

    Hopefully someone can explain this baffling case though.