/* Copyright (c) 2012 LinkedIn Corp. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ /** * $Id: $ */ package com.linkedin.r2.transport.http.client; import com.linkedin.r2.TestGroupNames; import com.linkedin.r2.filter.R2Constants; import com.linkedin.r2.message.rest.RestResponse; import com.linkedin.r2.message.stream.StreamRequest; import com.linkedin.r2.message.stream.StreamRequestBuilder; import com.linkedin.r2.message.stream.entitystream.ByteStringWriter; import com.linkedin.r2.message.stream.entitystream.EntityStreams; import com.linkedin.r2.transport.common.bridge.common.TransportResponse; import com.linkedin.r2.transport.http.common.HttpProtocolVersion; import io.netty.channel.Channel; import io.netty.channel.nio.NioEventLoopGroup; import io.netty.handler.codec.TooLongFrameException; import io.netty.handler.codec.http.HttpHeaderNames; import io.netty.handler.codec.http2.Http2Exception; import io.netty.util.AsciiString; import io.netty.util.concurrent.Promise; import java.io.IOException; import java.net.SocketAddress; import java.net.URI; import java.net.UnknownHostException; import java.security.NoSuchAlgorithmException; import java.util.HashMap; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLParameters; import com.linkedin.data.ByteString; import com.linkedin.r2.message.Messages; import com.linkedin.r2.message.stream.StreamResponse; import com.linkedin.r2.message.stream.entitystream.ReadHandle; import com.linkedin.r2.message.stream.entitystream.Reader; import org.eclipse.jetty.server.Server; import org.testng.Assert; import org.testng.annotations.AfterClass; import org.testng.annotations.BeforeClass; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import com.linkedin.common.callback.Callback; import com.linkedin.common.callback.FutureCallback; import com.linkedin.common.util.None; import com.linkedin.r2.RemoteInvocationException; import com.linkedin.r2.message.RequestContext; import com.linkedin.r2.message.rest.RestRequest; import com.linkedin.r2.message.rest.RestRequestBuilder; import com.linkedin.r2.transport.common.bridge.client.TransportCallbackAdapter; import com.linkedin.r2.transport.common.bridge.common.TransportCallback; /** * @author Steven Ihde * @author Ang Xu * @author Sean Sheng * @version $Revision: $ */ public class TestHttpNettyStreamClient { private static final String HOST = "127.0.0.1"; private static final String SCHEME = "http"; private static final int PORT = 8080; private static final String URL = SCHEME + "://" + HOST + ":" + PORT + "/echo"; private static final int REQUEST_COUNT = 100; private static final AsciiString HOST_NAME = new AsciiString(HOST + ':' + PORT); private static final String HTTP_GET = "GET"; private static final String HTTP_POST = "POST"; private static final int NO_CONTENT = 0; private static final int SMALL_CONTENT = 8 * 1024; private static final int LARGE_CONTENT = 128 * 1024; private NioEventLoopGroup _eventLoop; private ScheduledExecutorService _scheduler; private static final int TEST_MAX_RESPONSE_SIZE = 500000; private static final int TEST_MAX_HEADER_SIZE = 5000; private static final int TEST_HEADER_SIZE_BUFFER = 50; private static final int RESPONSE_OK = 1; private static final int TOO_LARGE = 2; @BeforeClass public void setup() { _eventLoop = new NioEventLoopGroup(); _scheduler = Executors.newSingleThreadScheduledExecutor(); } @AfterClass public void tearDown() { _scheduler.shutdown(); _eventLoop.shutdownGracefully(); } @Test public void testNoChannelTimeout() throws InterruptedException { HttpNettyStreamClient client = new HttpNettyStreamClient(new NoCreations(_scheduler), _scheduler, 500, 500, 1024 * 1024 * 2); RestRequest r = new RestRequestBuilder(URI.create("http://localhost/")).build(); FutureCallback<StreamResponse> cb = new FutureCallback<StreamResponse>(); TransportCallback<StreamResponse> callback = new TransportCallbackAdapter<StreamResponse>(cb); client.streamRequest(Messages.toStreamRequest(r), new RequestContext(), new HashMap<String, String>(), callback); try { // This timeout needs to be significantly larger than the getTimeout of the netty client; // we're testing that the client will generate its own timeout cb.get(30, TimeUnit.SECONDS); Assert.fail("Get was supposed to time out"); } catch (TimeoutException e) { // TimeoutException means the timeout for Future.get() elapsed and nothing happened. // Instead, we are expecting our callback to be invoked before the future timeout // with a timeout generated by the HttpNettyClient. Assert.fail("Unexpected TimeoutException, should have been ExecutionException", e); } catch (ExecutionException e) { verifyCauseChain(e, RemoteInvocationException.class, TimeoutException.class); } } @DataProvider(name = "slowReaderTimeoutClientProvider") public Object[][] slowReaderTimeoutClientProvider() { // Sets request timeout to be reasonable small since this unit test will await for the timeout duration // however increase the timeout if test is not stable HttpClientBuilder builder = new HttpClientBuilder(_eventLoop, _scheduler).setRequestTimeout(1000); return new Object[][] { { builder.buildStream() }, { builder.buildHttp2Stream() } }; } /** * Tests slow EntityStream {@link Reader} implementation should be subject to streaming timeout even * if the entire response entity can be buffered in memory. * * @throws Exception */ @Test(dataProvider = "slowReaderTimeoutClientProvider") public void testSlowReaderTimeout(AbstractNettyStreamClient client) throws Exception { // Sets the response size to be greater than zero but smaller than the in-memory buffer for HTTP/1.1 // and smaller than the receiving window size for HTTP/2 so the receiver will not block sender Server server = new HttpServerBuilder().responseSize(R2Constants.DEFAULT_DATA_CHUNK_SIZE).build(); StreamRequest request = new StreamRequestBuilder(new URI(URL)) .setHeader(HttpHeaderNames.HOST.toString(), HOST_NAME.toString()) .build(EntityStreams.emptyStream()); final CountDownLatch responseLatch = new CountDownLatch(1); final CountDownLatch streamLatch = new CountDownLatch(1); final AtomicReference<TransportResponse<StreamResponse>> atomicTransportResponse = new AtomicReference<>(); final AtomicReference<Throwable> atomicThrowable = new AtomicReference<>(); try { server.start(); client.streamRequest(request, new RequestContext(), new HashMap<>(), response -> { atomicTransportResponse.set(response); responseLatch.countDown(); // Sets a reader that does not consume any byte response.getResponse().getEntityStream().setReader(new Reader() { @Override public void onInit(ReadHandle rh) { } @Override public void onDataAvailable(ByteString data) { } @Override public void onDone() { } @Override public void onError(Throwable e) { atomicThrowable.set(e); streamLatch.countDown(); } }); }); } finally { responseLatch.await(5, TimeUnit.SECONDS); streamLatch.await(5, TimeUnit.SECONDS); server.stop(); } TransportResponse<StreamResponse> transportResponse = atomicTransportResponse.get(); Assert.assertNotNull(transportResponse, "Expected to receive a response"); Assert.assertFalse(transportResponse.hasError(), "Expected to receive a response without error"); Assert.assertNotNull(transportResponse.getResponse()); Assert.assertNotNull(transportResponse.getResponse().getEntityStream()); Throwable throwable = atomicThrowable.get(); Assert.assertNotNull(throwable, "Expected onError invoked with TimeoutException"); Assert.assertTrue(throwable instanceof RemoteInvocationException); Assert.assertNotNull(throwable.getCause()); Assert.assertTrue(throwable.getCause() instanceof TimeoutException); } @DataProvider(name = "noResponseClients") public Object[][] noResponseClientProvider() { HttpClientBuilder builder = new HttpClientBuilder(_eventLoop, _scheduler) .setRequestTimeout(500) .setIdleTimeout(10000) .setShutdownTimeout(500); return new Object[][] { { builder.buildStream() }, { builder.buildHttp2Stream() }, }; } @Test(dataProvider = "noResponseClients") public void testNoResponseTimeout(AbstractNettyStreamClient client) throws Exception { CountDownLatch responseLatch = new CountDownLatch(1); Server server = new HttpServerBuilder().responseLatch(responseLatch).build(); try { server.start(); RestRequest r = new RestRequestBuilder(new URI(URL)).build(); FutureCallback<StreamResponse> cb = new FutureCallback<StreamResponse>(); TransportCallback<StreamResponse> callback = new TransportCallbackAdapter<StreamResponse>(cb); client.streamRequest(Messages.toStreamRequest(r), new RequestContext(), new HashMap<String, String>(), callback); // This timeout needs to be significantly larger than the getTimeout of the netty client; // we're testing that the client will generate its own timeout cb.get(30, TimeUnit.SECONDS); Assert.fail("Get was supposed to time out"); } catch (TimeoutException e) { // TimeoutException means the timeout for Future.get() elapsed and nothing happened. // Instead, we are expecting our callback to be invoked before the future timeout // with a timeout generated by the HttpNettyClient. Assert.fail("Unexpected TimeoutException, should have been ExecutionException", e); } catch (ExecutionException e) { verifyCauseChain(e, RemoteInvocationException.class, TimeoutException.class); } finally { responseLatch.countDown(); server.stop(); } } @DataProvider(name = "badAddressClients") public Object[][] badAddressClientsProvider() { HttpClientBuilder builder = new HttpClientBuilder(_eventLoop, _scheduler) .setRequestTimeout(30000) .setIdleTimeout(10000) .setShutdownTimeout(500); return new Object[][] { { builder.buildStream() }, { builder.buildHttp2Stream() }, }; } @Test(dataProvider = "badAddressClients") public void testBadAddress(AbstractNettyStreamClient client) throws InterruptedException, IOException, TimeoutException { RestRequest r = new RestRequestBuilder(URI.create("http://this.host.does.not.exist.linkedin.com")).build(); FutureCallback<StreamResponse> cb = new FutureCallback<StreamResponse>(); TransportCallback<StreamResponse> callback = new TransportCallbackAdapter<StreamResponse>(cb); client.streamRequest(Messages.toStreamRequest(r), new RequestContext(), new HashMap<String, String>(), callback); try { cb.get(30, TimeUnit.SECONDS); Assert.fail("Get was supposed to fail"); } catch (ExecutionException e) { verifyCauseChain(e, RemoteInvocationException.class, UnknownHostException.class); } } @DataProvider(name = "remoteClientAddressClients") public Object[][] remoteClientAddressClientsProvider() { HttpClientBuilder builder = new HttpClientBuilder(_eventLoop, _scheduler); return new Object[][] { { builder.buildStream() }, { builder.buildHttp2Stream() }, }; } @Test(dataProvider = "remoteClientAddressClients") public void testRequestContextAttributes(AbstractNettyStreamClient client) throws InterruptedException, IOException, TimeoutException { RestRequest r = new RestRequestBuilder(URI.create("http://localhost")).build(); FutureCallback<StreamResponse> cb = new FutureCallback<>(); TransportCallback<StreamResponse> callback = new TransportCallbackAdapter<>(cb); RequestContext requestContext = new RequestContext(); client.streamRequest(Messages.toStreamRequest(r), requestContext, new HashMap<>(), callback); final String actualRemoteAddress = (String) requestContext.getLocalAttr(R2Constants.REMOTE_SERVER_ADDR); final HttpProtocolVersion actualProtocolVersion = (HttpProtocolVersion) requestContext.getLocalAttr(R2Constants.HTTP_PROTOCOL_VERSION); Assert.assertTrue("127.0.0.1".equals(actualRemoteAddress) || "0:0:0:0:0:0:0:1".equals(actualRemoteAddress), "Actual remote client address is not expected. " + "The local attribute field must be IP address in string type" + actualRemoteAddress); if (client instanceof HttpNettyStreamClient) { Assert.assertEquals(actualProtocolVersion, HttpProtocolVersion.HTTP_1_1); } else if (client instanceof Http2NettyStreamClient) { Assert.assertEquals(actualProtocolVersion, HttpProtocolVersion.HTTP_2); } else { Assert.fail("Unexpected client instance type"); } } @DataProvider(name = "responseSizeClients") public Object[][] responseSizeClientProvider() { HttpClientBuilder builder = new HttpClientBuilder(_eventLoop, _scheduler) .setRequestTimeout(50000) .setIdleTimeout(10000) .setShutdownTimeout(500) .setMaxResponseSize(TEST_MAX_RESPONSE_SIZE); return new Object[][] { { builder.buildStream() }, { builder.buildHttp2Stream() }, }; } @Test(dataProvider = "responseSizeClients") public void testMaxResponseSizeOK(AbstractNettyStreamClient client) throws Exception { testResponseSize(client, TEST_MAX_RESPONSE_SIZE - 1, RESPONSE_OK); testResponseSize(client, TEST_MAX_RESPONSE_SIZE, RESPONSE_OK); } @Test(dataProvider = "responseSizeClients") public void setTestMaxResponseSizeTooLarge(AbstractNettyStreamClient client) throws Exception { testResponseSize(client, TEST_MAX_RESPONSE_SIZE + 1, TOO_LARGE); } public void testResponseSize(AbstractNettyStreamClient client, int responseSize, int expectedResult) throws Exception { Server server = new HttpServerBuilder().responseSize(responseSize).build(); try { server.start(); RestRequest r = new RestRequestBuilder(new URI(URL)).build(); FutureCallback<StreamResponse> cb = new FutureCallback<StreamResponse>(); TransportCallback<StreamResponse> callback = new TransportCallbackAdapter<StreamResponse>(cb); client.streamRequest(Messages.toStreamRequest(r), new RequestContext(), new HashMap<String, String>(), callback); StreamResponse response = cb.get(30, TimeUnit.SECONDS); final CountDownLatch latch = new CountDownLatch(1); final AtomicReference<Throwable> error = new AtomicReference<Throwable>(); response.getEntityStream().setReader(new Reader() { @Override public void onInit(ReadHandle rh) { rh.request(Integer.MAX_VALUE); } @Override public void onDataAvailable(ByteString data) { } @Override public void onDone() { latch.countDown(); } @Override public void onError(Throwable e) { error.set(e); latch.countDown(); } }); if (!latch.await(30, TimeUnit.SECONDS)) { Assert.fail("Timeout waiting for response"); } if(expectedResult == TOO_LARGE) { Assert.assertNotNull(error.get(), "Max response size exceeded, expected exception. "); verifyCauseChain(error.get(), TooLongFrameException.class); } if (expectedResult == RESPONSE_OK) { Assert.assertNull(error.get(), "Unexpected Exception: response size <= max size"); } } catch (ExecutionException e) { if (expectedResult == RESPONSE_OK) { Assert.fail("Unexpected ExecutionException, response was <= max response size.", e); } verifyCauseChain(e, RemoteInvocationException.class, TooLongFrameException.class); } finally { server.stop(); } } @DataProvider(name = "maxHeaderSizeClients") public Object[][] maxHeaderSizeClientProvider() { HttpClientBuilder builder = new HttpClientBuilder(_eventLoop, _scheduler) .setRequestTimeout(5000) .setIdleTimeout(10000) .setShutdownTimeout(500) .setMaxHeaderSize(TEST_MAX_HEADER_SIZE); return new Object[][] { { builder.buildStream() }, { builder.buildHttp2Stream() }, }; } @Test(dataProvider = "maxHeaderSizeClients") public void testMaxHeaderSize(AbstractNettyStreamClient client) throws Exception { testHeaderSize(client, TEST_MAX_HEADER_SIZE - TEST_HEADER_SIZE_BUFFER, RESPONSE_OK); testHeaderSize(client, TEST_MAX_HEADER_SIZE + TEST_HEADER_SIZE_BUFFER, TOO_LARGE); } public void testHeaderSize(AbstractNettyStreamClient client, int headerSize, int expectedResult) throws Exception { Server server = new HttpServerBuilder().headerSize(headerSize).build(); try { server.start(); RestRequest r = new RestRequestBuilder(new URI(URL)).build(); FutureCallback<StreamResponse> cb = new FutureCallback<StreamResponse>(); TransportCallback<StreamResponse> callback = new TransportCallbackAdapter<StreamResponse>(cb); client.streamRequest(Messages.toStreamRequest(r), new RequestContext(), new HashMap<String, String>(), callback); cb.get(300, TimeUnit.SECONDS); if (expectedResult == TOO_LARGE) { Assert.fail("Max header size exceeded, expected exception. "); } } catch (ExecutionException e) { if (expectedResult == RESPONSE_OK) { Assert.fail("Unexpected ExecutionException, header was <= max header size.", e); } if (client instanceof HttpNettyStreamClient) { verifyCauseChain(e, RemoteInvocationException.class, TooLongFrameException.class); } else if (client instanceof Http2NettyStreamClient) { verifyCauseChain(e, RemoteInvocationException.class, Http2Exception.class); } else { Assert.fail("Unrecognized client"); } } finally { server.stop(); } } @DataProvider(name = "shutdownClients") public Object[][] shutdownClientProvider() { HttpClientBuilder builder = new HttpClientBuilder(_eventLoop, _scheduler) .setRequestTimeout(500) .setIdleTimeout(10000) .setShutdownTimeout(500); return new Object[][] { { builder.buildStream() }, { builder.buildHttp2Stream() }, }; } @Test(dataProvider = "shutdownClients") public void testShutdown(AbstractNettyStreamClient client) throws Exception { FutureCallback<None> shutdownCallback = new FutureCallback<None>(); client.shutdown(shutdownCallback); shutdownCallback.get(30, TimeUnit.SECONDS); // Now verify a new request will also fail RestRequest r = new RestRequestBuilder(URI.create("http://no.such.host.linkedin.com")).build(); FutureCallback<StreamResponse> callback = new FutureCallback<StreamResponse>(); client.streamRequest(Messages.toStreamRequest(r), new RequestContext(), new HashMap<String, String>(), new TransportCallbackAdapter<StreamResponse>(callback)); try { callback.get(30, TimeUnit.SECONDS); } catch (ExecutionException e) { // Expected } } @Test public void testShutdownStuckInPool() throws InterruptedException, ExecutionException, TimeoutException { // Test that shutdown works when the outstanding request is stuck in the pool waiting for a channel HttpNettyStreamClient client = new HttpNettyStreamClient(new NoCreations(_scheduler), _scheduler, 60000, 1, 1024 * 1024 * 2); RestRequest r = new RestRequestBuilder(URI.create("http://some.host/")).build(); FutureCallback<StreamResponse> futureCallback = new FutureCallback<StreamResponse>(); client.streamRequest(Messages.toStreamRequest(r), new RequestContext(), new HashMap<String, String>(), new TransportCallbackAdapter<StreamResponse>(futureCallback)); FutureCallback<None> shutdownCallback = new FutureCallback<None>(); client.shutdown(shutdownCallback); shutdownCallback.get(30, TimeUnit.SECONDS); try { futureCallback.get(30, TimeUnit.SECONDS); Assert.fail("get should have thrown exception"); } catch (ExecutionException e) { verifyCauseChain(e, RemoteInvocationException.class, TimeoutException.class); } } @Test public void testShutdownRequestOutstanding() throws Exception { // Test that it works when the shutdown kills the outstanding request... HttpClientBuilder builder = new HttpClientBuilder(_eventLoop, _scheduler) .setShutdownTimeout(500) .setRequestTimeout(60000); testShutdownRequestOutstanding(builder.buildStream(), RemoteInvocationException.class, TimeoutException.class); testShutdownRequestOutstanding(builder.buildHttp2Stream(), RemoteInvocationException.class, TimeoutException.class); } @Test public void testShutdownRequestOutstanding2() throws Exception { // Test that it works when the request timeout kills the outstanding request... HttpClientBuilder builder = new HttpClientBuilder(_eventLoop, _scheduler) .setShutdownTimeout(60000) .setRequestTimeout(500); testShutdownRequestOutstanding(builder.buildStream(), RemoteInvocationException.class, // sometimes the test fails with ChannelClosedException // TimeoutException.class Exception.class); testShutdownRequestOutstanding(builder.buildHttp2Stream(), RemoteInvocationException.class, // sometimes the test fails with ChannelClosedException // TimeoutException.class Exception.class); } private void testShutdownRequestOutstanding(AbstractNettyStreamClient client, Class<?>... causeChain) throws Exception { CountDownLatch responseLatch = new CountDownLatch(1); Server server = new HttpServerBuilder().responseLatch(responseLatch).build(); try { server.start(); RestRequest r = new RestRequestBuilder(new URI(URL)).build(); FutureCallback<StreamResponse> cb = new FutureCallback<StreamResponse>(); TransportCallback<StreamResponse> callback = new TransportCallbackAdapter<StreamResponse>(cb); client.streamRequest(Messages.toStreamRequest(r), new RequestContext(), new HashMap<String,String>(), callback); FutureCallback<None> shutdownCallback = new FutureCallback<None>(); client.shutdown(shutdownCallback); shutdownCallback.get(30, TimeUnit.SECONDS); // This timeout needs to be significantly larger than the getTimeout of the netty client; // we're testing that the client will generate its own timeout cb.get(30, TimeUnit.SECONDS); Assert.fail("Get was supposed to time out"); } catch (TimeoutException e) { // TimeoutException means the timeout for Future.get() elapsed and nothing happened. // Instead, we are expecting our callback to be invoked before the future timeout // with a timeout generated by the HttpNettyClient. Assert.fail("Get timed out, should have thrown ExecutionException", e); } catch (ExecutionException e) { verifyCauseChain(e, causeChain); } finally { responseLatch.countDown(); server.stop(); } } private static void verifyCauseChain(Throwable throwable, Class<?>... causes) { Throwable t = throwable; for (Class<?> c : causes) { Throwable cause = t.getCause(); if (cause == null) { Assert.fail("Cause chain ended too early", throwable); } if (!c.isAssignableFrom(cause.getClass())) { Assert.fail("Expected cause " + c.getName() + " not " + cause.getClass().getName(), throwable); } t = cause; } } // Test that cannot pass pass SSLParameters without SSLContext. // This in fact tests HttpClientPipelineFactory constructor through HttpNettyClient // constructor. @Test public void testClientPipelineFactory1() throws NoSuchAlgorithmException { try { new HttpClientBuilder(_eventLoop, _scheduler) .setSSLParameters(new SSLParameters()).buildStream(); } catch (IllegalArgumentException e) { // Check exception message to make sure it's the expected one. Assert.assertEquals(e.getMessage(), "SSLParameters passed with no SSLContext"); } } // Test that cannot pass pass SSLParameters without SSLContext. // This in fact tests HttpClientPipelineFactory constructor through HttpNettyClient // constructor. @Test public void testHttp2ClientPipelineFactory1() throws NoSuchAlgorithmException { try { new HttpClientBuilder(_eventLoop, _scheduler) .setSSLParameters(new SSLParameters()).buildHttp2Stream(); } catch (IllegalArgumentException e) { // Check exception message to make sure it's the expected one. Assert.assertEquals(e.getMessage(), "SSLParameters passed with no SSLContext"); } } // Test that cannot set cipher suites in SSLParameters that don't have any match in // SSLContext. // This in fact tests HttpClientPipelineFactory constructor through HttpNettyClient // constructor. @Test public void testClientPipelineFactory2Fail() throws NoSuchAlgorithmException { String[] requestedCipherSuites = {"Unsupported"}; SSLParameters sslParameters = new SSLParameters(); sslParameters.setCipherSuites(requestedCipherSuites); try { new HttpClientBuilder(_eventLoop, _scheduler) .setSSLContext(SSLContext.getDefault()) .setSSLParameters(sslParameters) .buildStream(); } catch (IllegalArgumentException e) { // Check exception message to make sure it's the expected one. Assert.assertEquals(e.getMessage(), "None of the requested cipher suites: [Unsupported] are found in SSLContext"); } } // Test that cannot set cipher suites in SSLParameters that don't have any match in // SSLContext. // This in fact tests HttpClientPipelineFactory constructor through HttpNettyClient // constructor. @Test public void testHttp2ClientPipelineFactory2Fail() throws NoSuchAlgorithmException { String[] requestedCipherSuites = {"Unsupported"}; SSLParameters sslParameters = new SSLParameters(); sslParameters.setCipherSuites(requestedCipherSuites); try { new HttpClientBuilder(_eventLoop, _scheduler) .setSSLContext(SSLContext.getDefault()) .setSSLParameters(sslParameters) .buildHttp2Stream(); } catch (IllegalArgumentException e) { // Check exception message to make sure it's the expected one. Assert.assertEquals(e.getMessage(), "None of the requested cipher suites: [Unsupported] are found in SSLContext"); } } // Test that can set cipher suites in SSLParameters that have at least one match in // SSLContext. // This in fact tests HttpClientPipelineFactory constructor through HttpNettyClient // constructor. @Test public void testClientPipelineFactory2Pass() throws NoSuchAlgorithmException { String[] requestedCipherSuites = {"Unsupported", "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA"}; SSLParameters sslParameters = new SSLParameters(); sslParameters.setCipherSuites(requestedCipherSuites); new HttpClientBuilder(_eventLoop, _scheduler) .setSSLContext(SSLContext.getDefault()) .setSSLParameters(sslParameters).buildStream(); } // Test that can set cipher suites in SSLParameters that have at least one match in // SSLContext. // This in fact tests HttpClientPipelineFactory constructor through HttpNettyClient // constructor. @Test public void testHttp2ClientPipelineFactory2Pass() throws NoSuchAlgorithmException { String[] requestedCipherSuites = {"Unsupported", "TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA"}; SSLParameters sslParameters = new SSLParameters(); sslParameters.setCipherSuites(requestedCipherSuites); new HttpClientBuilder(_eventLoop, _scheduler) .setSSLContext(SSLContext.getDefault()) .setSSLParameters(sslParameters) .buildHttp2Stream(); } // Test that cannot set protocols in SSLParameters that don't have any match in // SSLContext. // This in fact tests HttpClientPipelineFactory constructor through HttpNettyClient // constructor. @Test public void testClientPipelineFactory3Fail() throws NoSuchAlgorithmException { String[] requestedProtocols = {"Unsupported"}; SSLParameters sslParameters = new SSLParameters(); sslParameters.setProtocols(requestedProtocols); try { new HttpClientBuilder(_eventLoop, _scheduler) .setSSLContext(SSLContext.getDefault()) .setSSLParameters(sslParameters) .buildStream(); } catch (IllegalArgumentException e) { // Check exception message to make sure it's the expected one. Assert.assertEquals(e.getMessage(), "None of the requested protocols: [Unsupported] are found in SSLContext"); } } // Test that cannot set protocols in SSLParameters that don't have any match in // SSLContext. // This in fact tests HttpClientPipelineFactory constructor through HttpNettyClient // constructor. @Test public void testHttp2ClientPipelineFactory3Fail() throws NoSuchAlgorithmException { String[] requestedProtocols = {"Unsupported"}; SSLParameters sslParameters = new SSLParameters(); sslParameters.setProtocols(requestedProtocols); try { new HttpClientBuilder(_eventLoop, _scheduler) .setSSLContext(SSLContext.getDefault()) .setSSLParameters(sslParameters) .buildHttp2Stream(); } catch (IllegalArgumentException e) { // Check exception message to make sure it's the expected one. Assert.assertEquals(e.getMessage(), "None of the requested protocols: [Unsupported] are found in SSLContext"); } } // Test that can set protocols in SSLParameters that have at least one match in // SSLContext. // This in fact tests HttpClientPipelineFactory constructor through HttpNettyClient // constructor. @Test public void testClientPipelineFactory3Pass() throws NoSuchAlgorithmException { String[] requestedProtocols = {"Unsupported", "TLSv1"}; SSLParameters sslParameters = new SSLParameters(); sslParameters.setProtocols(requestedProtocols); new HttpClientBuilder(_eventLoop, _scheduler) .setSSLContext(SSLContext.getDefault()) .setSSLParameters(sslParameters) .buildStream(); } // Test that can set protocols in SSLParameters that have at least one match in // SSLContext. // This in fact tests HttpClientPipelineFactory constructor through HttpNettyClient // constructor. @Test public void testHttp2ClientPipelineFactory3Pass() throws NoSuchAlgorithmException { String[] requestedProtocols = {"Unsupported", "TLSv1"}; SSLParameters sslParameters = new SSLParameters(); sslParameters.setProtocols(requestedProtocols); new HttpClientBuilder(_eventLoop, _scheduler) .setSSLContext(SSLContext.getDefault()) .setSSLParameters(sslParameters) .buildHttp2Stream(); } @DataProvider(name = "poolStatsClients") public Object[][] poolStatsClientProvider() { final CountDownLatch setLatch = new CountDownLatch(1); final CountDownLatch removeLatch = new CountDownLatch(1); AbstractJmxManager manager = new AbstractJmxManager() { @Override public void onProviderCreate(PoolStatsProvider provider) { setLatch.countDown(); } @Override public void onProviderShutdown(PoolStatsProvider provider) { removeLatch.countDown(); } }; HttpClientBuilder builder = new HttpClientBuilder(_eventLoop, _scheduler).setJmxManager(manager); return new Object[][] { { builder.buildStream(), setLatch, removeLatch }, { builder.buildHttp2Stream(), setLatch, removeLatch }, }; } @Test(dataProvider = "poolStatsClients") public void testPoolStatsProviderManager( AbstractNettyStreamClient client, CountDownLatch setLatch, CountDownLatch removeLatch) throws Exception { // test setPoolStatsProvider try { setLatch.await(30, TimeUnit.SECONDS); } catch (InterruptedException e) { Assert.fail("PoolStatsAware setPoolStatsProvider didn't get called when creating channel pool."); } // test removePoolStatsProvider FutureCallback<None> shutdownCallback = new FutureCallback<None>(); client.shutdown(shutdownCallback); try { removeLatch.await(30, TimeUnit.SECONDS); } catch (InterruptedException e) { Assert.fail("PoolStatsAware removePoolStatsProvider didn't get called when shutting down channel pool."); } shutdownCallback.get(30, TimeUnit.SECONDS); } @Test (enabled = false) public void testMakingOutboundHttpsRequest() throws NoSuchAlgorithmException, InterruptedException, ExecutionException, TimeoutException { SSLContext context = SSLContext.getDefault(); SSLParameters sslParameters = context.getDefaultSSLParameters(); HttpNettyStreamClient client = new HttpClientBuilder(_eventLoop, _scheduler) .setSSLContext(context) .setSSLParameters(sslParameters) .buildStream(); RestRequest r = new RestRequestBuilder(URI.create("https://www.howsmyssl.com/a/check")).build(); FutureCallback<StreamResponse> cb = new FutureCallback<StreamResponse>(); TransportCallback<StreamResponse> callback = new TransportCallbackAdapter<StreamResponse>(cb); client.streamRequest(Messages.toStreamRequest(r), new RequestContext(), new HashMap<String, String>(), callback); cb.get(30, TimeUnit.SECONDS); } private static class NoCreations implements ChannelPoolFactory { private final ScheduledExecutorService _scheduler; public NoCreations(ScheduledExecutorService scheduler) { _scheduler = scheduler; } @Override public AsyncPool<Channel> getPool(SocketAddress address) { return new AsyncPoolImpl<Channel>("fake pool", new AsyncPool.Lifecycle<Channel>() { @Override public void create(Callback<Channel> channelCallback) { } @Override public boolean validateGet(Channel obj) { return false; } @Override public boolean validatePut(Channel obj) { return false; } @Override public void destroy(Channel obj, boolean error, Callback<Channel> channelCallback) { } @Override public PoolStats.LifecycleStats getStats() { return null; } }, 0, 0, _scheduler); } } @DataProvider(name = "requestResponseParameters") public Object[][] parametersProvider() { HttpClientBuilder builder = new HttpClientBuilder(_eventLoop, _scheduler); // Client, Request Method, Request Size, Response Size, RestOverStream return new Object[][] { { builder.buildHttp2Stream(), HTTP_GET, NO_CONTENT, NO_CONTENT, true }, { builder.buildHttp2Stream(), HTTP_GET, NO_CONTENT, NO_CONTENT, false }, { builder.buildHttp2Stream(), HTTP_GET, SMALL_CONTENT, SMALL_CONTENT, true }, { builder.buildHttp2Stream(), HTTP_GET, SMALL_CONTENT, SMALL_CONTENT, false }, { builder.buildHttp2Stream(), HTTP_GET, LARGE_CONTENT, LARGE_CONTENT, true }, { builder.buildHttp2Stream(), HTTP_GET, LARGE_CONTENT, LARGE_CONTENT, false }, { builder.buildHttp2Stream(), HTTP_POST, NO_CONTENT, NO_CONTENT, true }, { builder.buildHttp2Stream(), HTTP_POST, NO_CONTENT, NO_CONTENT, false }, { builder.buildHttp2Stream(), HTTP_POST, SMALL_CONTENT, SMALL_CONTENT, true }, { builder.buildHttp2Stream(), HTTP_POST, SMALL_CONTENT, SMALL_CONTENT, false }, { builder.buildHttp2Stream(), HTTP_POST, LARGE_CONTENT, LARGE_CONTENT, true }, { builder.buildHttp2Stream(), HTTP_POST, LARGE_CONTENT, LARGE_CONTENT, false }, { builder.buildStream(), HTTP_GET, NO_CONTENT, NO_CONTENT, true }, { builder.buildStream(), HTTP_GET, NO_CONTENT, NO_CONTENT, false }, { builder.buildStream(), HTTP_GET, SMALL_CONTENT, SMALL_CONTENT, true }, { builder.buildStream(), HTTP_GET, SMALL_CONTENT, SMALL_CONTENT, false }, { builder.buildStream(), HTTP_GET, LARGE_CONTENT, LARGE_CONTENT, true }, { builder.buildStream(), HTTP_GET, LARGE_CONTENT, LARGE_CONTENT, false }, { builder.buildStream(), HTTP_POST, NO_CONTENT, NO_CONTENT, true }, { builder.buildStream(), HTTP_POST, NO_CONTENT, NO_CONTENT, false }, { builder.buildStream(), HTTP_POST, SMALL_CONTENT, SMALL_CONTENT, true }, { builder.buildStream(), HTTP_POST, SMALL_CONTENT, SMALL_CONTENT, false }, { builder.buildStream(), HTTP_POST, LARGE_CONTENT, LARGE_CONTENT, true }, { builder.buildStream(), HTTP_POST, LARGE_CONTENT, LARGE_CONTENT, false }, }; } /** * Tests implementations of {@link AbstractNettyStreamClient} with different request dimensions. * * @param client Client implementation of {@link AbstractNettyStreamClient} * @param method HTTP request method * @param requestSize Request content size * @param responseSize Response content size * @param isFullRequest Whether to buffer a full request before stream * @throws Exception */ @Test(dataProvider = "requestResponseParameters") public void testStreamRequests( AbstractNettyStreamClient client, String method, int requestSize, int responseSize, boolean isFullRequest) throws Exception { AtomicInteger succeeded = new AtomicInteger(0); AtomicInteger failed = new AtomicInteger(0); Server server = new HttpServerBuilder().responseSize(responseSize).build(); try { server.start(); CountDownLatch latch = new CountDownLatch(REQUEST_COUNT); for (int i = 0; i < REQUEST_COUNT; i++) { StreamRequest request = new StreamRequestBuilder(new URI(URL)).setMethod(method) .setHeader(HttpHeaderNames.HOST.toString(), HOST_NAME.toString()) .build(EntityStreams.newEntityStream(new ByteStringWriter(ByteString.copy(new byte[requestSize])))); RequestContext context = new RequestContext(); context.putLocalAttr(R2Constants.IS_FULL_REQUEST, isFullRequest); client.streamRequest(request, context, new HashMap<>(), new TransportCallbackAdapter<>(new Callback<StreamResponse>() { @Override public void onSuccess(StreamResponse response) { response.getEntityStream().setReader(new Reader() { ReadHandle _rh; int _consumed = 0; @Override public void onDataAvailable(ByteString data) { _consumed += data.length(); _rh.request(1); } @Override public void onDone() { succeeded.incrementAndGet(); latch.countDown(); } @Override public void onError(Throwable e) { failed.incrementAndGet(); latch.countDown(); } @Override public void onInit(ReadHandle rh) { _rh = rh; _rh.request(1); } }); } @Override public void onError(Throwable e) { failed.incrementAndGet(); latch.countDown(); } })); } if (!latch.await(30, TimeUnit.SECONDS)) { Assert.fail("Timeout waiting for responses. " + succeeded + " requests succeeded and " + failed + " requests failed out of total " + REQUEST_COUNT + " requests"); } Assert.assertEquals(latch.getCount(), 0); Assert.assertEquals(failed.get(), 0); Assert.assertEquals(succeeded.get(), REQUEST_COUNT); FutureCallback<None> shutdownCallback = new FutureCallback<>(); client.shutdown(shutdownCallback); shutdownCallback.get(30, TimeUnit.SECONDS); } finally { server.stop(); } } @Test(dataProvider = "requestResponseParameters", groups = TestGroupNames.TESTNG_GROUP_KNOWN_ISSUE) public void testCancelStreamRequests( AbstractNettyStreamClient client, String method, int requestSize, int responseSize, boolean isFullRequest) throws Exception { AtomicInteger succeeded = new AtomicInteger(0); AtomicInteger failed = new AtomicInteger(0); Server server = new HttpServerBuilder().responseSize(responseSize).build(); try { server.start(); CountDownLatch latch = new CountDownLatch(REQUEST_COUNT); for (int i = 0; i < REQUEST_COUNT; i++) { StreamRequest request = new StreamRequestBuilder(new URI(URL)).setMethod(method) .setHeader(HttpHeaderNames.HOST.toString(), HOST_NAME.toString()) .build(EntityStreams.newEntityStream(new ByteStringWriter(ByteString.copy(new byte[requestSize])))); RequestContext context = new RequestContext(); context.putLocalAttr(R2Constants.IS_FULL_REQUEST, isFullRequest); client.streamRequest(request, context, new HashMap<>(), new TransportCallbackAdapter<>(new Callback<StreamResponse>() { @Override public void onSuccess(StreamResponse response) { response.getEntityStream().setReader(new Reader() { @Override public void onDataAvailable(ByteString data) { } @Override public void onDone() { failed.incrementAndGet(); latch.countDown(); } @Override public void onError(Throwable e) { failed.incrementAndGet(); latch.countDown(); } @Override public void onInit(ReadHandle rh) { rh.cancel(); succeeded.incrementAndGet(); latch.countDown(); } }); } @Override public void onError(Throwable e) { failed.incrementAndGet(); latch.countDown(); } })); } if (!latch.await(30, TimeUnit.SECONDS)) { Assert.fail("Timeout waiting for responses. " + succeeded + " requests succeeded and " + failed + " requests failed out of total " + REQUEST_COUNT + " requests"); } Assert.assertEquals(latch.getCount(), 0); Assert.assertEquals(failed.get(), 0); Assert.assertEquals(succeeded.get(), REQUEST_COUNT); FutureCallback<None> shutdownCallback = new FutureCallback<>(); client.shutdown(shutdownCallback); shutdownCallback.get(30, TimeUnit.SECONDS); } finally { server.stop(); } } @Test(dataProvider = "requestResponseParameters", expectedExceptions = UnsupportedOperationException.class) public void testRestRequests( AbstractNettyStreamClient client, String method, int requestSize, int responseSize, boolean isFullRequest) throws Exception { Server server = new HttpServerBuilder().responseSize(responseSize).build(); try { server.start(); for (int i = 0; i < REQUEST_COUNT; i++) { RestRequest request = new RestRequestBuilder(new URI(URL)).setMethod(method) .setHeader(HttpHeaderNames.HOST.toString(), HOST_NAME.toString()) .setEntity(ByteString.copy(new byte[requestSize])) .build(); RequestContext context = new RequestContext(); context.putLocalAttr(R2Constants.IS_FULL_REQUEST, isFullRequest); client.restRequest(request, context, new HashMap<>(), new TransportCallbackAdapter<>(new Callback<RestResponse>() { @Override public void onSuccess(RestResponse response) { } @Override public void onError(Throwable e) { } })); } } finally { server.stop(); } } }