15

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.

Todd
  • 261
  • 1
  • 2
  • 7
  • You might want to verify whether the request content has the Content-Length or not. – Lee Meador Aug 09 '13 at 23:50
  • 1
    Turn on logging to check the request (`client.addFilter(new LoggingFilter(System.out))`) that way you can be confident the issue is on your end. – DannyMo Aug 10 '13 at 04:46
  • I neglected to mention that I did set up a logger and verified that the Content-Length header is not generated. I've edited my question to reflect the new information. – Todd Aug 10 '13 at 05:45
  • It looks like the content-length will only be set if the size of the entity does not exceed the [configured buffer](https://jersey.java.net/apidocs/2.0/jersey/org/glassfish/jersey/CommonProperties.html#OUTBOUND_CONTENT_LENGTH_BUFFER). Can you check the size of your entity against this configuration? If the entity is larger than the buffer, then you can either 1) increase the buffer or 2) compute the content-length manually – DannyMo Aug 12 '13 at 22:18
  • Jersey 2 does not set `Content-Length` header on the client at the moment. Can you try to set `Content-Length` manually with value of `-1` whether the server can handle it? – Michal Gajdos Aug 13 '13 at 13:25
  • @DannyMo My content is only 23 bytes, so that doesn't exceed the limit for the content length configuration. Looking at the current source code, this is limit is set to 8kb. Thanks for the tip though. – Todd Aug 15 '13 at 21:13
  • Does the server sent `HTTP 411` even if the `Content-Length` is `-1`? – Michal Gajdos Aug 22 '13 at 12:12
  • @Michal our server requires a correct Content-Length and will return a 411 if the length is set to -1. – Todd Aug 28 '13 at 16:47

5 Answers5

5

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.

Christian Trimble
  • 2,126
  • 16
  • 27
  • I'll try to verify this today and then award the bounty. It doesn't sound like this explains why the OP was getting the 411s, but it does satisfy my own curiosity. I didn't think to check if the content-length was being added after the logging filter. Thanks! – DannyMo Aug 28 '13 at 16:02
  • Jersey is picking up this functionality from Java's HTTPUrlConnection object, at least in my environment. The OP may have some other HTTP Client configured that is using HTTP/1.0, where Content-Length headers (or chunking) are not required. – Christian Trimble Aug 28 '13 at 16:14
  • I did switch to the ApacheConnector to work around an issue with the default connector throwing an exception on a WWW-Authenticate header that works just fine with the Apache Http client. I did find the problem with the ApacheConnector and ended up writing my own connector that allows the Apache Http client correctly set the Content-Length. – Todd Aug 28 '13 at 16:52
  • Problem still exists in Jersey 2.25. If response length exceeds 8192 bytes (default), 'Content-Length' is not added. Of course, it is not visible on body of 13 bytes. Problem can be solved by setting server property https://eclipse-ee4j.github.io/jersey.github.io/apidocs/latest/jersey/org/glassfish/jersey/server/ServerProperties.html#OUTBOUND_CONTENT_LENGTH_BUFFER – Eugene Jun 01 '20 at 11:15
4

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.

Todd
  • 261
  • 1
  • 2
  • 7
4

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.

stivlo
  • 83,644
  • 31
  • 142
  • 199
  • 1
    for Jersey 2.26+ the configuration is `clientConfig.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED);` – satoshi Mar 28 '18 at 19:49
3

This is supported in Jersey 2.5 (https://java.net/jira/browse/JERSEY-2224). You could use https://jersey.java.net/apidocs/latest/jersey/org/glassfish/jersey/client/RequestEntityProcessing.html#BUFFERED to stream your content. I put together a simple example that shows both chunked and buffering content using ApacheConnector. Checkout this project: https://github.com/aruld/sof-18157218

public class EntityStreamingTest extends JerseyTest {

  private static final Logger LOGGER = Logger.getLogger(EntityStreamingTest.class.getName());

  @Path("/test")
  public static class HttpMethodResource {
    @POST
    @Path("chunked")
    public String postChunked(@HeaderParam("Transfer-Encoding") String transferEncoding, String entity) {
      assertEquals("POST", entity);
      assertEquals("chunked", transferEncoding);
      return entity;
    }

    @POST
    public String postBuffering(@HeaderParam("Content-Length") String contentLength, String entity) {
      assertEquals("POST", entity);
      assertEquals(entity.length(), Integer.parseInt(contentLength));
      return entity;
    }
  }

  @Override
  protected Application configure() {
    ResourceConfig config = new ResourceConfig(HttpMethodResource.class);
    config.register(new LoggingFilter(LOGGER, true));
    return config;
  }

  @Override
  protected void configureClient(ClientConfig config) {
    config.connectorProvider(new ApacheConnectorProvider());
  }

  @Test
  public void testPostChunked() {
    Response response = target().path("test/chunked").request().post(Entity.text("POST"));

    assertEquals(200, response.getStatus());
    assertTrue(response.hasEntity());
  }

  @Test
  public void testPostBuffering() {
    ClientConfig cc = new ClientConfig();
    cc.property(ClientProperties.REQUEST_ENTITY_PROCESSING, RequestEntityProcessing.BUFFERED);
    cc.connectorProvider(new ApacheConnectorProvider());
    JerseyClient client = JerseyClientBuilder.createClient(cc);
    WebTarget target = client.target(getBaseUri());
    Response response = target.path("test").request().post(Entity.text("POST"));

    assertEquals(200, response.getStatus());
    assertTrue(response.hasEntity());
  }
}
Arul Dhesiaseelan
  • 2,009
  • 23
  • 19
0
@Test
public void testForbiddenHeadersAllowed() {
    Client client = ClientBuilder.newClient();
    System.setProperty("sun.net.http.allowRestrictedHeaders", "true");

    Response response = testHeaders(client);
    System.out.println(response.readEntity(String.class));
    Assert.assertEquals(200, response.getStatus());
  • /** * Tests sending of restricted headers (Origin and Access-Control-Request-Method) which are * used for CORS. These headers are by default skipped by the {@link java.net.HttpURLConnection}. * The system property {@code sun.net.http.allowRestrictedHeaders} must be defined in order to * allow these headers. */ – shero yu Apr 30 '20 at 12:15
  • You may edit your answer instead of adding comment to it. – asyard Apr 30 '20 at 21:08