Джерси 2.0 Content-Length не установлен - программирование

Джерси 2.0 Content-Length не установлен

Я пытаюсь отправить сообщение в веб-службу, которая требует, чтобы заголовок Content-Length был установлен с помощью следующего кода:

// 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));

Из примечаний к выпуску https://java.net/jira/browse/JERSEY-1617 и документации Джерси 2.0 https://jersey.java.net/documentation/latest/message-body-workers.html подразумевается, что Content -Length автоматически устанавливается. Тем не менее, я получаю код ответа 411 с сервера, указывающий, что Content-Length не присутствует в запросе.

Кто-нибудь знает лучший способ получить набор заголовков Content-Length?

Я проверил настройку регистратора, что заголовок Content-Length не генерируется в запросе.

Спасибо.

4b9b3361

Ответ 1

Я провел быстрый тест с Jersey Client 2.2 и Netcat, и он показывает мне, что Джерси отправляет заголовок Content-Length, хотя LoggingFilter не сообщает об этом.

Чтобы выполнить этот тест, я сначала запускал netcat в одной оболочке.

nc -l 8090

Затем я выполнил следующий код Джерси в другой оболочке.

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\"}")));

После запуска этого кода регистрируются следующие строки.

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

Однако netcat сообщает еще несколько заголовков в сообщении.

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"}

Я провел этот тест на OSX с Java6 и Java7 с теми же результатами. Я также провел тест в Джерси 2.0 с аналогичными результатами.

Ответ 2

Посмотрев исходный код класса ApacheConnector, я вижу проблему. Когда ClientRequest преобразуется в HttpUriRequest, вызывается частный метод getHttpEntity(), который возвращает HttpEntity. К сожалению, это возвращает HttpEntity, чей getContentLength() всегда возвращает -1.

Когда клиент Apache http создает запрос, он будет консультироваться с объектом HttpEntity для длины, и поскольку он возвращает -1, заголовок no Content-Length будет установлен.

Я решил свою проблему, создав новый соединитель, который является копией исходного кода для ApacheConnector, но имеет другую реализацию getHttpEntity(). Я прочитал объект из исходного ClientRequest в массив байтов, а затем обернул этот массив байтов с помощью ByteArrayEntity. Когда клиент Apache Http создает запрос, он будет консультироваться с сущностью, а ByteArrayEntity ответит с правильной длиной содержимого, которая по очереди позволяет установить заголовок Content-Length.

Вот соответствующий код:

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();
}

ПРЕДУПРЕЖДЕНИЕ: Мое проблемное пространство было ограничено и содержало только небольшие тела сущностей как часть запросов. Этот метод, предложенный выше, может быть проблематичным с крупными объектами сущности, такими как изображения, поэтому я не думаю, что это общее решение для всех.

Ответ 3

Это поддерживается в Jersey 2.5 (https://java.net/jira/browse/JERSEY-2224). Вы можете использовать https://jersey.java.net/apidocs/latest/jersey/org/glassfish/jersey/client/RequestEntityProcessing.html#BUFFERED для потоковой передачи вашего контента. Я собрал простой пример, который показывает как упорядоченный, так и буферный контент с помощью ApacheConnector. Оформить заказ на этот проект: 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());
  }
}

Ответ 4

Я тестировал с помощью Джерси 2.25.1 более простое решение, которое заключается в настройке setChunkedEncodingEnabled(false) в конфигурации клиента Джерси. Вместо использования закодированного кодирования весь объект сериализуется в памяти, а Content-Length задается в запросе.

Для справки, вот пример конфигурации, которую я использовал:

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));
}

Я использовал mitmproxy для проверки правильности заголовков запросов и заголовка Content-Length.