How to get form parameters in request filter
Asked Answered
T

2

9

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.

Tolan answered 9/8, 2013 at 1:32 Comment(4)
Can you show us a request you're sending to the rest endpoint.Wainscoting
@michal.gajdos See my edit, and lemme know if there's anything else I can provide.Tolan
formParam1 and formParam2 are null in your resource method as well? Just to be sure MyFilterFactory contains #filter method from your first snippet?Wainscoting
@michal.gajdos formParam1 and formParam1 are passed into the resource method as expected (not null). Yes, MyFilterFactory creates the resource filter that creates the container request filter that has that filter implementation.Tolan
T
3

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();
    }
}
Tolan answered 15/8, 2013 at 17:1 Comment(0)
W
1

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).

Wainscoting answered 13/8, 2013 at 9:0 Comment(8)
The filter is definitely instantiated and called - that's how I'm verifying the Form is empty within its execution. I attached Jersey source and followed it through getFormParameters - it writes the input stream into the ByteArrayOutputStream correctly, but the stream is empty. I'm not sure why you're seeing that additional message body reader trace and I'm not - one comes from your calling getFormParameters but what is the other one from?Tolan
Also what version of Jersey are you using, for complete clarity?Tolan
I am using 1.18-SNAPSHOT. The first matched message body reader is from getEntity(Form.class) in ContainerRequest#getFormParameters() , the second one is from form param injector before entering the resource method. Can you, please, enable LoggingFilter (see EDIT 1 in my answer) and provide the log?Wainscoting
Sure, see my latest edit. So I wonder why I'm not seeing a trace for the param injector? The params are clearly showing up in the resource method...Tolan
Can you file a bug to our JIRA (java.net/jira/browse/JERSEY) with a reproducible testcase? I need to debug this. Thanks.Wainscoting
I created a test app from scratch using Jersey 1.17.1 and can't repro the issue. So the difference is either between 1.17 and 1.17.1, or there's something else wrong in my real application that I need to isolate. I'm going to sleuth some more before filing a bug.Tolan
The only difference between 1.17 and 1.17.1 is one WADL fix which has nothing to do with your use-case. Do you have any other jersey filters (or servlet filters) in your application that could precede the "form filter"? Aside from this issue, you might be interested in Jersey 2.x and Bean Validation support in there (see jersey.java.net/documentation/latest/bean-validation.html).Wainscoting
+1 Thanks for your time helping out with this - especially for reminding me about other servlet filters (could you add that to your answer?). See my answer for what it turned out to be.Tolan

© 2022 - 2024 — McMap. All rights reserved.