Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Jersey 2.0 Content-Length not set

I'm trying to post to a web service that requires the Content-Length header to be set using the following code:

// EDIT: added apache connector code
ClientConfig clientConfig = new ClientConfig();
ApacheConnector apache = new ApacheConnector(clientConfig);

// setup client to log requests and responses and their entities
client.register(new LoggingFilter(Logger.getLogger("com.example.app"), true));

Part part = new Part("123");
WebTarget target = client.target("https://api.thing.com/v1.0/thing/{thingId}");
Response jsonResponse = target.resolveTemplate("thingId", "abcdefg")
                .request(MediaType.APPLICATION_JSON)
                .header(HttpHeaders.AUTHORIZATION, "anauthcodehere")
                .post(Entity.json(part));

From the release notes https://java.net/jira/browse/JERSEY-1617 and the Jersey 2.0 documentation https://jersey.java.net/documentation/latest/message-body-workers.html it implies that Content-Length is automatically set. However, I get a 411 response code back from the server indicating that Content-Length is not present in the request.

Does anyone know the best way to get the Content-Length header set?

I've verified through setting up a logger that the Content-Length header is not generated in the request.

Thanks.

like image 264
Todd Avatar asked Aug 09 '13 23:08

Todd


3 Answers

I ran a quick test with Jersey Client 2.2 and Netcat, and it is showing me that Jersey is sending the Content-Length header, even though the LoggingFilter is not reporting it.

To do this test, I first ran netcat in one shell.

nc -l 8090

Then I executed the following Jersey code in another shell.

Response response = ClientBuilder.newClient()
    .register(new LoggingFilter(Logger.getLogger("com.example.app"), true))
    .target("http://localhost:8090/test")
    .request()
    .post(Entity.json(IOUtils.toInputStream("{key:\"value\"}")));

After running this code, the following lines get logged.

INFO: 1 * LoggingFilter - Request received on thread main
1 > POST http://localhost:8090/test
1 > Content-Type: application/json
{key:"value"}

However, netcat reports several more headers in the message.

POST /test HTTP/1.1
Content-Type: application/json
User-Agent: Jersey/2.0 (HttpUrlConnection 1.7.0_17)
Host: localhost:8090
Accept: text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2
Connection: keep-alive
Content-Length: 13

{key:"value"}

I ran this test on OSX with Java6 and Java7, with the same results. I also ran the test in Jersey 2.0, with similar results.

like image 155
Christian Trimble Avatar answered Nov 04 '22 12:11

Christian Trimble


After looking at the source code for the ApacheConnector class, I see the problem. When a ClientRequest is converted to a HttpUriRequest a private method getHttpEntity() is called that returns a HttpEntity. Unfortunately, this returns a HttpEntity whose getContentLength() always returns a -1.

When the Apache http client creates the request it will consult the HttpEntity object for a length and since it returns -1 no Content-Length header will be set.

I solved my problem by creating a new connector that is a copy of the source code for the ApacheConnector but has a different implementation of the getHttpEntity(). I read the entity from the original ClientRequest into a byte array and then wrap that byte array with a ByteArrayEntity. When the Apache Http client creates the request it will consult the entity and the ByteArrayEntity will respond with the correct content length which in turns allows the Content-Length header to be set.

Here's the relevant code:

private HttpEntity getHttpEntity(final ClientRequest clientRequest) {
    final Object entity = clientRequest.getEntity();

    if (entity == null) {
        return null;
    }

    byte[] content = getEntityContent(clientRequest);

    return new ByteArrayEntity(content);
}


private byte[] getEntityContent(final ClientRequest clientRequest) {

   // buffer into which entity will be serialized
   final ByteArrayOutputStream baos = new ByteArrayOutputStream();

   // set up a mock output stream to capture the output
   clientRequest.setStreamProvider(new OutboundMessageContext.StreamProvider() {

        @Override
        public OutputStream getOutputStream(int contentLength) throws IOException {
            return baos;
        }
    });

    try {
        clientRequest.writeEntity();
    } 
    catch (IOException e) {
        LOGGER.log(Level.SEVERE, null, e);
        // re-throw new exception
        throw new ProcessingException(e);
    }

    return baos.toByteArray();
}

WARNING: My problem space was constrained and only contained small entity bodies as part of requests. This method proposed above may be problematic with large entity bodies such as images so I don't think this is a general solution for all.

like image 38
Todd Avatar answered Nov 04 '22 12:11

Todd


I've tested with Jersey 2.25.1 a simpler solution that consists in setting setChunkedEncodingEnabled(false) in the Jersey Client configuration. Instead of using a chunked encoding, the whole entity is serialised in memory and the Content-Length is set on the request.

For reference, here is an example of a configuration I've used:

private Client createJerseyClient(Environment environment) {
    Logger logger = Logger.getLogger(getClass().getName());
    JerseyClientConfiguration clientConfig = new JerseyClientConfiguration();
    clientConfig.setProxyConfiguration(new ProxyConfiguration("localhost", 3333));
    clientConfig.setGzipEnabled(false);
    clientConfig.setGzipEnabledForRequests(false);
    clientConfig.setChunkedEncodingEnabled(false);
    return new JerseyClientBuilder(environment)
            .using(clientConfig)
            .build("RestClient")
            .register(new LoggingFeature(logger, Level.INFO, null, null));
}

I've used mitmproxy to verify the request headers and the Content-Length header was set correctly.

like image 4
stivlo Avatar answered Nov 04 '22 12:11

stivlo