|
32 | 32 | import java.io.InputStream; |
33 | 33 | import java.io.InputStreamReader; |
34 | 34 | import java.io.OutputStream; |
| 35 | +import java.net.InetSocketAddress; |
| 36 | +import java.net.Socket; |
| 37 | +import java.net.SocketTimeoutException; |
35 | 38 | import java.nio.charset.Charset; |
36 | 39 | import java.nio.charset.StandardCharsets; |
37 | 40 | import java.util.ArrayList; |
38 | 41 | import java.util.List; |
39 | 42 | import java.util.Random; |
| 43 | +import java.util.concurrent.CountDownLatch; |
| 44 | +import java.util.function.Consumer; |
40 | 45 |
|
41 | 46 | import org.apache.hc.core5.http.ClassicHttpRequest; |
42 | 47 | import org.apache.hc.core5.http.ClassicHttpResponse; |
|
52 | 57 | import org.apache.hc.core5.http.ProtocolException; |
53 | 58 | import org.apache.hc.core5.http.URIScheme; |
54 | 59 | import org.apache.hc.core5.http.config.Http1Config; |
| 60 | +import org.apache.hc.core5.http.impl.HttpProcessors; |
| 61 | +import org.apache.hc.core5.http.impl.io.DefaultBHttpClientConnectionFactory; |
| 62 | +import org.apache.hc.core5.http.impl.io.HttpRequestExecutor; |
| 63 | +import org.apache.hc.core5.http.io.HttpClientConnection; |
| 64 | +import org.apache.hc.core5.http.io.HttpConnectionFactory; |
55 | 65 | import org.apache.hc.core5.http.io.entity.AbstractHttpEntity; |
56 | 66 | import org.apache.hc.core5.http.io.entity.ByteArrayEntity; |
57 | 67 | import org.apache.hc.core5.http.io.entity.EntityUtils; |
|
62 | 72 | import org.apache.hc.core5.http.protocol.DefaultHttpProcessor; |
63 | 73 | import org.apache.hc.core5.http.protocol.HttpContext; |
64 | 74 | import org.apache.hc.core5.http.protocol.HttpCoreContext; |
| 75 | +import org.apache.hc.core5.http.protocol.HttpProcessor; |
65 | 76 | import org.apache.hc.core5.http.protocol.RequestConnControl; |
66 | 77 | import org.apache.hc.core5.http.protocol.RequestContent; |
67 | 78 | import org.apache.hc.core5.http.protocol.RequestExpectContinue; |
68 | 79 | import org.apache.hc.core5.http.protocol.RequestTargetHost; |
69 | 80 | import org.apache.hc.core5.http.protocol.RequestUserAgent; |
| 81 | +import org.apache.hc.core5.net.URIAuthority; |
| 82 | +import org.apache.hc.core5.testing.SSLTestContexts; |
70 | 83 | import org.apache.hc.core5.testing.extension.classic.ClassicTestResources; |
71 | 84 | import org.apache.hc.core5.util.Timeout; |
72 | 85 | import org.junit.jupiter.api.Assertions; |
73 | 86 | import org.junit.jupiter.api.Test; |
74 | 87 | import org.junit.jupiter.api.extension.RegisterExtension; |
75 | 88 |
|
| 89 | +import javax.net.ssl.SSLSocket; |
| 90 | + |
76 | 91 | abstract class ClassicIntegrationTest { |
77 | 92 |
|
78 | 93 | private static final Timeout TIMEOUT = Timeout.ofMinutes(1); |
@@ -773,4 +788,86 @@ void testHeaderTooLargePost() throws Exception { |
773 | 788 | } |
774 | 789 | } |
775 | 790 |
|
| 791 | + @Test |
| 792 | + void testImmediateCloseUponSocketTimeout() throws Exception { |
| 793 | + final int socketTimeoutMillis = 1000; |
| 794 | + final int serverDelayMillis = 5 * socketTimeoutMillis; |
| 795 | + |
| 796 | + final ClassicTestServer server = testResources.server(); |
| 797 | + |
| 798 | + final CountDownLatch serverDelayStarted = new CountDownLatch(1); |
| 799 | + |
| 800 | + // Configure server to delay significantly before responding |
| 801 | + server.register("*", (request, response, context) -> { |
| 802 | + serverDelayStarted.countDown(); |
| 803 | + try { |
| 804 | + // Delay much longer than socket timeout |
| 805 | + Thread.sleep(serverDelayMillis); |
| 806 | + } catch (final InterruptedException ex) { |
| 807 | + Thread.currentThread().interrupt(); |
| 808 | + } |
| 809 | + response.setCode(HttpStatus.SC_OK); |
| 810 | + response.setEntity(new StringEntity("Delayed response", ContentType.TEXT_PLAIN)); |
| 811 | + }); |
| 812 | + |
| 813 | + server.start(); |
| 814 | + |
| 815 | + final HttpHost host = new HttpHost(scheme.id, "localhost", server.getPort()); |
| 816 | + final HttpCoreContext context = HttpCoreContext.create(); |
| 817 | + final HttpConnectionFactory<? extends HttpClientConnection> connFactory = |
| 818 | + DefaultBHttpClientConnectionFactory.builder().build(); |
| 819 | + |
| 820 | + final HttpRequestExecutor requestExecutor = new HttpRequestExecutor(); |
| 821 | + final HttpProcessor processor = HttpProcessors.client(); |
| 822 | + |
| 823 | + final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/"); |
| 824 | + request.setAuthority(new URIAuthority(host)); |
| 825 | + request.setScheme(host.getSchemeName()); |
| 826 | + |
| 827 | + runWithSocket(host, socketTimeoutMillis, socket -> { |
| 828 | + try { |
| 829 | + final HttpClientConnection connection = connFactory.createConnection(socket); |
| 830 | + |
| 831 | + requestExecutor.preProcess(request, processor, context); |
| 832 | + |
| 833 | + final long startTime = System.currentTimeMillis(); |
| 834 | + try (final ClassicHttpResponse response = requestExecutor.execute( |
| 835 | + request, connection, context)) { |
| 836 | + Assertions.fail("Expected SocketTimeoutException not thrown"); |
| 837 | + } catch (final SocketTimeoutException e) { |
| 838 | + // Expected due to server delay exceeding socket timeout |
| 839 | + } |
| 840 | + |
| 841 | + final long endTime = System.currentTimeMillis(); |
| 842 | + final long durationMillis = endTime - startTime; |
| 843 | + // Assert that the timeout occurred around the socket timeout duration |
| 844 | + Assertions.assertTrue(durationMillis >= socketTimeoutMillis && durationMillis < socketTimeoutMillis * 2, |
| 845 | + String.format("Socket timeout should occur around %dms (took %dms)", |
| 846 | + socketTimeoutMillis, durationMillis)); |
| 847 | + } catch (final IOException | HttpException e) { |
| 848 | + Assertions.fail("IOException during request execution: " + e.getMessage()); |
| 849 | + } |
| 850 | + }); |
| 851 | + } |
| 852 | + |
| 853 | + private static void runWithSocket( |
| 854 | + final HttpHost host, final int socketTimeoutMillis, final Consumer<Socket> socketConsumer) |
| 855 | + throws IOException { |
| 856 | + try (final Socket clientSocket = new Socket()) { |
| 857 | + clientSocket.setSoTimeout(socketTimeoutMillis); |
| 858 | + clientSocket.connect(new InetSocketAddress(host.getHostName(), host.getPort())); |
| 859 | + if (host.getSchemeName().equalsIgnoreCase("http")) { |
| 860 | + socketConsumer.accept(clientSocket); |
| 861 | + return; |
| 862 | + } |
| 863 | + |
| 864 | + try (final SSLSocket sslSocket = (SSLSocket) SSLTestContexts.createClientSSLContext() |
| 865 | + .getSocketFactory() |
| 866 | + .createSocket(clientSocket, host.getHostName(), -1, true)) { |
| 867 | + sslSocket.startHandshake(); |
| 868 | + |
| 869 | + socketConsumer.accept(sslSocket); |
| 870 | + } |
| 871 | + } |
| 872 | + } |
776 | 873 | } |
0 commit comments