How to get form parameters in request filter

2019-03-26 07:37发布

问题:

I'm trying to get the form parameters of a request in a request filter:

@Override
public ContainerRequest filter(final ContainerRequest request) {

    final Form formParameters = request.getFormParameters();

    //logic

    return request;
}

However, the form always seems to be empty. The HttpRequestContext.getFormParameters() documentation says:

Get the form parameters of the request entity.

This method will ensure that the request entity is buffered such that it may be consumed by the applicaton.

Returns: the form parameters, if there is a request entity and the content type is "application/x-www-form-urlencoded", otherwise an instance containing no parameters will be returned.

My resource is annotated with @Consumes("application/x-www-form-urlencoded"), although it won't have been matched until after the request filter - is that why this isn't working?

I tried doing some research but couldn't find any conclusive evidence of whether this is possible. There was this 4-year old discussion, in which Paul Sandoz says:

If you are working in Jersey filters or with the HttpRequestContext you can get the form parameters as follows: [broken link to Jersey 1.1.1 HttpRequestContext.getFormParameters]

I also found this 3-year-old discussion about how to get multipart/form-data form fields in a request filter. In it, Paul Sandoz uses the following code:

// Buffer
InputStream in = request.getEntityInputStream();
if (in.getClass() != ByteArrayInputStream.class) {
    // Buffer input
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try {
        ReaderWriter.writeTo(in, baos);
    } catch (IOException ex) {
        throw new ContainerException(ex);
    }
    in = new ByteArrayInputStream(baos.toByteArray());
    request.setEntityInputStream(in);
}

// Read entity
FormDataMultiPart multiPart = request.getEntity(FormDataMultiPart.class);

I tried emulating that approach for Form instead, but the result of request.getEntityInputStream() is always an empty stream. And looking at the source of getFormParameters, that method is in fact doing the same thing already:

@Override
public Form getFormParameters() {
    if (MediaTypes.typeEquals(MediaType.APPLICATION_FORM_URLENCODED_TYPE, getMediaType())) {
        InputStream in = getEntityInputStream();
        if (in.getClass() != ByteArrayInputStream.class) {
            // Buffer input
            ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
            try {
                ReaderWriter.writeTo(in, byteArrayOutputStream);
            } catch (IOException e) {
                throw new IllegalArgumentException(e);
            }

            in = new ByteArrayInputStream(byteArrayOutputStream.toByteArray());
            setEntityInputStream(in);
        }

        ByteArrayInputStream byteArrayInputStream = (ByteArrayInputStream) in;
        Form f = getEntity(Form.class);
        byteArrayInputStream.reset();
        return f;
    } else {
        return new Form();
    }
}

I can't figure out what's slurping up the entity input stream before I get to it. Something in Jersey must be consuming it because the form params are later passed into the resource method. What am I doing wrong here, or is this impossible (and why)?

EDIT: Here's an example of a request being sent:

POST /test/post-stuff HTTP/1.1
Host: local.my.application.com:8443
Cache-Control: no-cache
Content-Type: application/x-www-form-urlencoded

form_param_1=foo&form_param_2=bar

Here's the (somewhat redundant) request logging:

INFO: 1 * Server in-bound request
1 > POST https://local.my.application.com:8443/test/post-stuff
1 > host: local.my.application.com:8443
1 > connection: keep-alive
1 > content-length: 33
1 > cache-control: no-cache
1 > origin: chrome-extension://fdmmgilgnpjigdojojpjoooidkmcomcm
1 > user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/28.0.1500.95 Safari/537.36
1 > content-type: application/x-www-form-urlencoded
1 > accept: */*
1 > accept-encoding: gzip,deflate,sdch
1 > accept-language: en-US,en;q=0.8
1 > cookie: [omitted]
1 > 

Here are the response headers of that request, including the Jersey Trace:

Content-Type →application/json;charset=UTF-8
Date →Fri, 09 Aug 2013 18:00:17 GMT
Location →https://local.my.application.com:8443/test/post-stuff/
Server →Apache-Coyote/1.1
Transfer-Encoding →chunked
X-Jersey-Trace-000 →accept root resource classes: "/post-stuff"
X-Jersey-Trace-001 →match path "/post-stuff" -> "/post\-stuff(/.*)?", [...], "(/.*)?"
X-Jersey-Trace-002 →accept right hand path java.util.regex.Matcher[pattern=/post\-stuff(/.*)? region=0,11 lastmatch=/post-stuff]: "/post-stuff" -> "/post-stuff" : ""
X-Jersey-Trace-003 →accept resource: "post-stuff" -> @Path("/post-stuff") com.application.my.jersey.resource.TestResource@7612e9d2
X-Jersey-Trace-004 →match path "" -> ""
X-Jersey-Trace-005 →accept resource methods: "post-stuff", POST -> com.application.my.jersey.resource.TestResource@7612e9d2
X-Jersey-Trace-006 →matched resource method: public javax.ws.rs.core.Response com.application.my.jersey.resource.TestResource.execute(java.lang.String,java.lang.String)
X-Jersey-Trace-007 →matched message body reader: class com.sun.jersey.api.representation.Form, "application/x-www-form-urlencoded" -> com.sun.jersey.core.impl.provider.entity.FormProvider@b98df1f
X-Jersey-Trace-008 →matched message body writer: java.lang.String@f62, "application/json" -> com.sun.jersey.core.impl.provider.entity.StringProvider@1c5ddffa

Here is the (unremarkable) servlet config:

<servlet>
    <servlet-name>jersey</servlet-name>
    <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
    <init-param>
        <param-name>com.sun.jersey.config.property.packages</param-name>
        <param-value>com.application.my.jersey</param-value>
    </init-param>
    <init-param>
        <param-name>com.sun.jersey.spi.container.ResourceFilters</param-name>
        <param-value>com.application.my.jersey.MyFilterFactory</param-value>
    </init-param>
    <init-param>
        <param-name>com.sun.jersey.config.feature.Trace</param-name>
        <param-value>true</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

Here's the example resource:

@Path("/post-stuff")
@Produces(MediaType.APPLICATION_JSON)
public final class TestResource {

    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public Response execute(
            @FormParam("form_param_1") final String formParam1,
            @FormParam("form_param_2") final String formParam2
    ) {
        return Response.created(URI.create("/")).entity("{}").build();
    }
}

I'm using Jersey 1.17.


For those interested, I'm trying to roll my own required parameter validation, as described in JERSEY-351. My solution here worked for query, cookie, and header params - form params are holding out on me.

回答1:

This was a tricky one. I'd removed other Jersey filters to eliminate them from the problem, but missed a plain servlet filter hiding at the bottom of web.xml:

<filter>
    <filter-name>myFilter</filter-name>
    <filter-class>com.application.my.MyFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>myFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Removing this filter fixed the issue - form params showed up in the Jersey filter. But why? I dug deeper, narrowing down the problem to a single statement in MyFilter:

request.getParameter("some_param")

I tried to simplify the problem even more by removing MyFilter and making the same call in the Jersey filter (by injecting HttpServletRequest) - but the form parameters still showed up. The issue appears to happen specifically when calling getParameter on the org.apache.catalina.connector.RequestFacade instance that gets passed into javax.servlet.Filter.doFilter. So is this in fact a Tomcat bug?

The documentation of ServletRequest.getParameter says:

If the parameter data was sent in the request body, such as occurs with an HTTP POST request, then reading the body directly via getInputStream() or getReader() can interfere with the execution of this method.

So maybe the reverse is true too - that calling getParameter might be allowed to interfere with the entity input stream? It's unclear to me whether the method's contract allows for this behavior, and whether it indicates a bug in Tomcat, Jersey, or neither.

Anyway, that old filter wasn't actually needed so my issue is solved but just removing it.


Here's a full reproduction of the problem (Tomcat 7.0):

web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" id="WebApp_ID" version="3.0">
    <display-name>test</display-name>
    <servlet>
        <servlet-name>jersey</servlet-name>
        <servlet-class>com.sun.jersey.spi.container.servlet.ServletContainer</servlet-class>
        <init-param>
            <param-name>com.sun.jersey.config.property.packages</param-name>
            <param-value>com.application.my</param-value>
        </init-param>
        <init-param>
            <param-name>com.sun.jersey.spi.container.ResourceFilters</param-name>
            <param-value>com.application.my.TestFilterFactory</param-value>
        </init-param>
        <init-param>
            <param-name>com.sun.jersey.config.feature.Trace</param-name>
            <param-value>false</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>jersey</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>
    <filter>
        <filter-name>servletFilter</filter-name>
        <filter-class>com.application.my.TestServletFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>servletFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
</web-app>

TestServletFilter.java:

package com.application.my;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;

public final class TestServletFilter implements Filter {

    @Override
    public void init(FilterConfig config) { }

    @Override
    public void doFilter(
            final ServletRequest request,
            final ServletResponse response,
            final FilterChain chain
    ) throws IOException, ServletException {
        System.out.println("calling getParameter on " + request.getClass().getName());
        request.getParameter("blah");
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() { }
}

TestFilterFactory.java:

package com.application.my;

import java.util.Collections;
import java.util.List;

import com.sun.jersey.api.model.AbstractMethod;
import com.sun.jersey.spi.container.ContainerRequest;
import com.sun.jersey.spi.container.ContainerRequestFilter;
import com.sun.jersey.spi.container.ContainerResponseFilter;
import com.sun.jersey.spi.container.ResourceFilter;
import com.sun.jersey.spi.container.ResourceFilterFactory;

public final class TestFilterFactory implements ResourceFilterFactory {

    @Override
    public List<ResourceFilter> create(final AbstractMethod method) {
        return Collections.<ResourceFilter>singletonList(new ResourceFilter() {
            @Override
            public ContainerRequestFilter getRequestFilter() {
                return new ContainerRequestFilter() {
                    @Override
                    public ContainerRequest filter(final ContainerRequest request) {

                        System.out.println("form: " + request.getFormParameters());

                        return request;
                    }
                };
            }
            @Override
            public ContainerResponseFilter getResponseFilter() {
                return null;
            }
        });
    }
}

TestResource.java:

package com.application.my;

import java.net.URI;

import javax.ws.rs.Consumes;
import javax.ws.rs.FormParam;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;


@Path("/post-stuff")
@Produces(MediaType.APPLICATION_JSON)
public final class TestResource {

    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public Response execute(
            @FormParam("form_param_1") final String formParam1,
            @FormParam("form_param_2") final String formParam2
    ) {
        System.out.println("form param_1: " + formParam1);
        System.out.println("form param_2: " + formParam2);

        return Response.created(URI.create("/")).entity("{}").build();
    }
}


回答2:

Make sure your ResourceFilterFactory creates an instance of ResourceFilter for the TestResource#execute method, which then creates a ContainerRequestFilter instance:

public class MyFilterFactory implements ResourceFilterFactory {

    @Override
    public List<ResourceFilter> create(final AbstractMethod am) {
        return new ArrayList<ResourceFilter>() {{

            add(new ResourceFilter() {
                @Override
                public ContainerRequestFilter getRequestFilter() {
                    return new ContainerRequestFilter() {
                        @Override
                        public ContainerRequest filter(final ContainerRequest request) {
                            System.out.println(request.getFormParameters());
                            return request;
                        }
                    };
                }

                @Override
                public ContainerResponseFilter getResponseFilter() {
                    return null;
                }
            });
        }};
    }
}

From the trace you have provided I am not sure whether your ContainerRequestFilter is called. There should be one more trace header containing something like this:

→matched message body reader: class com.sun.jersey.api.representation.Form, "application/x-www-form-urlencoded" -> com.sun.jersey.core.impl.provider.entity.FormProvider@b98df1f

The whole trace from my test:

HTTP/1.1 201 Created
Location: http://localhost:8080/helloworld-webapp/helloworld/
Content-Type: text/plain
X-Jersey-Trace-000: accept root resource classes: "/helloworld"
X-Jersey-Trace-001: match path "/helloworld" -> "/application\.wadl(/.*)?", "/helloworld(/.*)?"
X-Jersey-Trace-002: accept right hand path java.util.regex.Matcher[pattern=/helloworld(/.*)? region=0,11 lastmatch=/helloworld]: "/helloworld" -> "/helloworld" : ""
X-Jersey-Trace-003: accept resource: "helloworld" -> @Path("/helloworld") com.sun.jersey.samples.helloworld.resources.HelloWorldResource@7449df0f
X-Jersey-Trace-004: match path "" -> ""
X-Jersey-Trace-005: accept resource methods: "helloworld", POST -> com.sun.jersey.samples.helloworld.resources.HelloWorldResource@7449df0f
X-Jersey-Trace-006: matched resource method: public javax.ws.rs.core.Response com.sun.jersey.samples.helloworld.resources.HelloWorldResource.execute(java.lang.String,java.lang.String)
X-Jersey-Trace-007: matched message body reader: class com.sun.jersey.api.representation.Form, "application/x-www-form-urlencoded" -> com.sun.jersey.core.impl.provider.entity.FormProvider@6bc1b916
X-Jersey-Trace-008: matched message body reader: class com.sun.jersey.api.representation.Form, "application/x-www-form-urlencoded" -> com.sun.jersey.core.impl.provider.entity.FormProvider@6bc1b916
X-Jersey-Trace-009: matched message body writer: java.lang.String@f62, "text/plain" -> com.sun.jersey.core.impl.provider.entity.StringProvider@4aae6c4e
Transfer-Encoding: chunked
Server: Jetty(6.1.24) 

EDIT 1:

Enable request LoggingFilter:

<init-param>
    <param-name>com.sun.jersey.spi.container.ContainerRequestFilters</param-name>
    <param-value>com.sun.jersey.api.container.filter.LoggingFilter</param-value>
</init-param>

EDIT 2:

Also make sure no other Servlet or Jersey filter has read the InputStream before. In such a case the entity input stream may no longer be available (but you can still inject @FormParam into your resource method - as in this case).