ApacheConnector does not process request headers that were set in a WriterInterceptor
Asked Answered
S

1

3

I am experiencing problems when configurating my Jersey Client with the ApacheConnector. It seems to ignore all request headers that I define in a WriterInterceptor. I can tell that the WriterInterceptor is called when I set a break point within WriterInterceptor#aroundWriteTo(WriterInterceptorContext). Contrary to that, I can observe that the modification of an InputStream is preserved.

Here is a runnable example demonstrating my problem:

public class ApacheConnectorProblemDemonstration extends JerseyTest {

  private static final Logger LOGGER = Logger.getLogger(JerseyTest.class.getName());
  private static final String QUESTION = "baz", ANSWER = "qux";
  private static final String REQUEST_HEADER_NAME_CLIENT = "foo-cl", REQUEST_HEADER_VALUE_CLIENT = "bar-cl";
  private static final String REQUEST_HEADER_NAME_INTERCEPTOR = "foo-ic", REQUEST_HEADER_VALUE_INTERCEPTOR = "bar-ic";
  private static final int MAX_CONNECTIONS = 100;
  private static final String PATH = "/";

  @Path(PATH)
  public static class TestResource {
    @POST
    public String handle(InputStream questionStream,
                         @HeaderParam(REQUEST_HEADER_NAME_CLIENT) String client,
                         @HeaderParam(REQUEST_HEADER_NAME_INTERCEPTOR) String interceptor) 
        throws IOException {
      assertEquals(REQUEST_HEADER_VALUE_CLIENT, client);
      // Here, the header that was set in the client's writer interceptor is lost.
      assertEquals(REQUEST_HEADER_VALUE_INTERCEPTOR, interceptor);
      // However, the input stream got gzipped so the WriterInterceptor has been partly applied.
      assertEquals(QUESTION, new Scanner(new GZIPInputStream(questionStream)).nextLine());
      return ANSWER;
    }
  }

  @Provider
  @Priority(Priorities.ENTITY_CODER)
  public static class ClientInterceptor implements WriterInterceptor {
    @Override
    public void aroundWriteTo(WriterInterceptorContext context) 
        throws IOException, WebApplicationException {
      context.getHeaders().add(REQUEST_HEADER_NAME_INTERCEPTOR, REQUEST_HEADER_VALUE_INTERCEPTOR);
      context.setOutputStream(new GZIPOutputStream(context.getOutputStream()));
      context.proceed();
    }
  }

  @Override
  protected Application configure() {
    enable(TestProperties.LOG_TRAFFIC);
    enable(TestProperties.DUMP_ENTITY);
    return new ResourceConfig(TestResource.class);
  }

  @Override
  protected Client getClient(TestContainer tc, ApplicationHandler applicationHandler) {
    ClientConfig clientConfig = tc.getClientConfig() == null ? new ClientConfig() : tc.getClientConfig();
    clientConfig.property(ApacheClientProperties.CONNECTION_MANAGER, makeConnectionManager(MAX_CONNECTIONS));
    clientConfig.register(ClientInterceptor.class);
    // If I do not use the Apache connector, I avoid this problem.
    clientConfig.connector(new ApacheConnector(clientConfig));
    if (isEnabled(TestProperties.LOG_TRAFFIC)) {
      clientConfig.register(new LoggingFilter(LOGGER, isEnabled(TestProperties.DUMP_ENTITY)));
    }
    configureClient(clientConfig);
    return ClientBuilder.newClient(clientConfig);
  }

  private static ClientConnectionManager makeConnectionManager(int maxConnections) {
    PoolingClientConnectionManager connectionManager = new PoolingClientConnectionManager();
    connectionManager.setMaxTotal(maxConnections);
    connectionManager.setDefaultMaxPerRoute(maxConnections);
    return connectionManager;
  }

  @Test
  public void testInterceptors() throws Exception {
    Response response = target(PATH)
        .request()
        .header(REQUEST_HEADER_NAME_CLIENT, REQUEST_HEADER_VALUE_CLIENT)
        .post(Entity.text(QUESTION));
    assertEquals(200, response.getStatus());
    assertEquals(ANSWER, response.readEntity(String.class));
  }
}

I want to use the ApacheConnector in order to optimize for concurrent requests via the PoolingClientConnectionManager. Did I mess up the configuration?

PS: The exact same problem occurs when using the GrizzlyConnector.

Solemnize answered 6/11, 2013 at 13:10 Comment(0)
S
3

After further research, I assume that this is rather a misbehavior in the default Connector that uses a HttpURLConnection. As I explained in this other self-answered question of mine, the documentation states:

Whereas filters are primarily intended to manipulate request and response parameters like HTTP headers, URIs and/or HTTP methods, interceptors are intended to manipulate entities, via manipulating entity input/output streams

A WriterInterceptor is not supposed to manipulate the header values while a {Client,Server}RequestFilter is not supposed to manipulate the entity stream. If you need to use both, both components should be bundled within a javax.ws.rs.core.Feature or within the same class that implements two interfaces. (This can be problematic if you need to set two different Prioritys though.)

All this is very unfortunate though, since JerseyTest uses the Connector that uses a HttpURLConnection such that all my unit tests succeeded while the real life application misbehaved since it was configured with an ApacheConnector. Also, rather than suppressing changes, I wished, Jersey would throw me some exceptions. (This is a general issue I have with Jersey. When I for example used a too new version of the ClientConnectionManager where the interface was renamed to HttpClientConnectionManager I simply was informed in a one line log statement that all my configuration efforts were ignored. I did not discover this log statement til very late in development.)

Solemnize answered 7/11, 2013 at 9:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.