| /* | |
| * Copyright (c) 2018, 2025, Oracle and/or its affiliates. All rights reserved. | |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. | |
| * | |
| * This code is free software; you can redistribute it and/or modify it | |
| * under the terms of the GNU General Public License version 2 only, as | |
| * published by the Free Software Foundation. | |
| * | |
| * This code is distributed in the hope that it will be useful, but WITHOUT | |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or | |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License | |
| * version 2 for more details (a copy is included in the LICENSE file that | |
| * accompanied this code). | |
| * | |
| * You should have received a copy of the GNU General Public License version | |
| * 2 along with this work; if not, write to the Free Software Foundation, | |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. | |
| * | |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA | |
| * or visit www.oracle.com if you need additional information or have any | |
| * questions. | |
| */ | |
| /* | |
| * @test | |
| * @bug 8195823 | |
| * @summary Buffers given to response body subscribers should not contain | |
| * unprocessed HTTP data | |
| * @library /test/lib /test/jdk/java/net/httpclient/lib | |
| * @build jdk.httpclient.test.lib.http2.Http2TestServer jdk.test.lib.net.SimpleSSLContext | |
| * jdk.httpclient.test.lib.common.TestServerConfigurator | |
| * @run testng/othervm | |
| * -Djdk.httpclient.HttpClient.log=headers,errors,channel | |
| * ConcurrentResponses | |
| */ | |
| import java.io.IOException; | |
| import java.io.InputStream; | |
| import java.io.OutputStream; | |
| import java.net.InetAddress; | |
| import java.net.InetSocketAddress; | |
| import java.net.URI; | |
| import java.nio.ByteBuffer; | |
| import java.util.HashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.concurrent.CompletableFuture; | |
| import java.util.concurrent.CompletionStage; | |
| import java.util.concurrent.ExecutorService; | |
| import java.util.concurrent.Executors; | |
| import java.util.concurrent.Flow; | |
| import java.util.concurrent.atomic.AtomicInteger; | |
| import java.util.stream.IntStream; | |
| import javax.net.ssl.SSLContext; | |
| import com.sun.net.httpserver.HttpExchange; | |
| import com.sun.net.httpserver.HttpHandler; | |
| import com.sun.net.httpserver.HttpServer; | |
| import com.sun.net.httpserver.HttpsServer; | |
| import java.net.http.HttpClient; | |
| import java.net.http.HttpRequest; | |
| import java.net.http.HttpResponse; | |
| import java.net.http.HttpResponse.BodyHandler; | |
| import java.net.http.HttpResponse.BodyHandlers; | |
| import java.net.http.HttpResponse.BodySubscriber; | |
| import java.net.http.HttpResponse.BodySubscribers; | |
| import jdk.httpclient.test.lib.common.TestServerConfigurator; | |
| import jdk.httpclient.test.lib.http2.Http2TestServer; | |
| import jdk.httpclient.test.lib.http2.Http2TestExchange; | |
| import jdk.httpclient.test.lib.http2.Http2Handler; | |
| import jdk.test.lib.net.SimpleSSLContext; | |
| import org.testng.annotations.AfterTest; | |
| import org.testng.annotations.BeforeTest; | |
| import org.testng.annotations.DataProvider; | |
| import org.testng.annotations.Test; | |
| import static java.nio.charset.StandardCharsets.UTF_8; | |
| import static java.net.http.HttpResponse.BodyHandlers.discarding; | |
| import static org.testng.Assert.assertEquals; | |
| import static org.testng.Assert.assertFalse; | |
| import static org.testng.Assert.fail; | |
| public class ConcurrentResponses { | |
| SSLContext sslContext; | |
| HttpServer httpTestServer; // HTTP/1.1 [ 4 servers ] | |
| HttpsServer httpsTestServer; // HTTPS/1.1 | |
| Http2TestServer http2TestServer; // HTTP/2 ( h2c ) | |
| Http2TestServer https2TestServer; // HTTP/2 ( h2 ) | |
| String httpFixedURI, httpsFixedURI, httpChunkedURI, httpsChunkedURI; | |
| String http2FixedURI, https2FixedURI, http2VariableURI, https2VariableURI; | |
| static final int CONCURRENT_REQUESTS = 13; | |
| static final AtomicInteger IDS = new AtomicInteger(); | |
| static final String ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; | |
| static final int ALPHABET_LENGTH = ALPHABET.length(); | |
| static final String stringOfLength(int requiredLength) { | |
| StringBuilder sb = new StringBuilder(requiredLength); | |
| IntStream.range(0, requiredLength) | |
| .mapToObj(i -> ALPHABET.charAt(i % ALPHABET_LENGTH)) | |
| .forEach(c -> sb.append(c)); | |
| return sb.toString(); | |
| } | |
| /** An array of different Strings, to be used as bodies. */ | |
| static final String[] BODIES = bodies(); | |
| static String[] bodies() { | |
| String[] bodies = new String[CONCURRENT_REQUESTS]; | |
| for (int i=0;i<CONCURRENT_REQUESTS; i++) { | |
| // slightly, but still, different bodies | |
| bodies[i] = "Request-" + i + "-body-" + stringOfLength((1024) + i); | |
| } | |
| return bodies; | |
| } | |
| /** | |
| * Asserts the given response's status code is 200. | |
| * Returns a CF that completes with the given response. | |
| */ | |
| static final <T> CompletionStage<HttpResponse<T>> | |
| assert200ResponseCode(HttpResponse<T> response) { | |
| assertEquals(response.statusCode(), 200); | |
| return CompletableFuture.completedFuture(response); | |
| } | |
| /** | |
| * Asserts that the given response's body is equal to the given body. | |
| * Returns a CF that completes with the given response. | |
| */ | |
| static final <T> CompletionStage<HttpResponse<T>> | |
| assertbody(HttpResponse<T> response, T body) { | |
| assertEquals(response.body(), body); | |
| return CompletableFuture.completedFuture(response); | |
| } | |
| @DataProvider(name = "uris") | |
| public Object[][] variants() { | |
| return new Object[][]{ | |
| { httpFixedURI }, | |
| { httpsFixedURI }, | |
| { httpChunkedURI }, | |
| { httpsChunkedURI }, | |
| { http2FixedURI }, | |
| { https2FixedURI }, | |
| { http2VariableURI }, | |
| { https2VariableURI } | |
| }; | |
| } | |
| // The ofString implementation accumulates data, below a certain threshold | |
| // into the byte buffers it is given. | |
| @Test(dataProvider = "uris") | |
| void testAsString(String uri) throws Exception { | |
| int id = IDS.getAndIncrement(); | |
| ExecutorService virtualExecutor = Executors.newThreadPerTaskExecutor(Thread.ofVirtual() | |
| .name("HttpClient-" + id + "-Worker", 0).factory()); | |
| HttpClient client = HttpClient.newBuilder() | |
| .sslContext(sslContext) | |
| .executor(virtualExecutor) | |
| .build(); | |
| try { | |
| Map<HttpRequest, String> requests = new HashMap<>(); | |
| for (int i = 0; i < CONCURRENT_REQUESTS; i++) { | |
| HttpRequest request = HttpRequest.newBuilder(URI.create(uri + "?" + i)) | |
| .build(); | |
| requests.put(request, BODIES[i]); | |
| } | |
| // initial connection to seed the cache so next parallel connections reuse it | |
| client.sendAsync(HttpRequest.newBuilder(URI.create(uri)).build(), discarding()).join(); | |
| // will reuse connection cached from the previous request ( when HTTP/2 ) | |
| CompletableFuture.allOf(requests.keySet().parallelStream() | |
| .map(request -> client.sendAsync(request, BodyHandlers.ofString())) | |
| .map(cf -> cf.thenCompose(ConcurrentResponses::assert200ResponseCode)) | |
| .map(cf -> cf.thenCompose(response -> assertbody(response, requests.get(response.request())))) | |
| .toArray(CompletableFuture<?>[]::new)) | |
| .join(); | |
| } finally { | |
| client.close(); | |
| virtualExecutor.close(); | |
| } | |
| } | |
| // The custom subscriber aggressively attacks any area, between the limit | |
| // and the capacity, in the byte buffers it is given, by writing 'X' into it. | |
| @Test(dataProvider = "uris") | |
| void testWithCustomSubscriber(String uri) throws Exception { | |
| int id = IDS.getAndIncrement(); | |
| ExecutorService virtualExecutor = Executors.newThreadPerTaskExecutor(Thread.ofVirtual() | |
| .name("HttpClient-" + id + "-Worker", 0).factory()); | |
| HttpClient client = HttpClient.newBuilder() | |
| .executor(virtualExecutor) | |
| .sslContext(sslContext).build(); | |
| try { | |
| Map<HttpRequest, String> requests = new HashMap<>(); | |
| for (int i = 0; i < CONCURRENT_REQUESTS; i++) { | |
| HttpRequest request = HttpRequest.newBuilder(URI.create(uri + "?" + i)) | |
| .build(); | |
| requests.put(request, BODIES[i]); | |
| } | |
| // initial connection to seed the cache so next parallel connections reuse it | |
| client.sendAsync(HttpRequest.newBuilder(URI.create(uri)).build(), discarding()).join(); | |
| // will reuse connection cached from the previous request ( when HTTP/2 ) | |
| CompletableFuture.allOf(requests.keySet().parallelStream() | |
| .map(request -> client.sendAsync(request, CustomSubscriber.handler)) | |
| .map(cf -> cf.thenCompose(ConcurrentResponses::assert200ResponseCode)) | |
| .map(cf -> cf.thenCompose(response -> assertbody(response, requests.get(response.request())))) | |
| .toArray(CompletableFuture<?>[]::new)) | |
| .join(); | |
| } finally { | |
| client.close(); | |
| virtualExecutor.close(); | |
| } | |
| } | |
| /** | |
| * A subscriber that wraps ofString, but mucks with any data between limit | |
| * and capacity, if the client mistakenly passes it any that is should not. | |
| */ | |
| static class CustomSubscriber implements BodySubscriber<String> { | |
| static final BodyHandler<String> handler = (r) -> new CustomSubscriber(); | |
| private final BodySubscriber<String> ofString = BodySubscribers.ofString(UTF_8); | |
| @Override | |
| public CompletionStage<String> getBody() { | |
| return ofString.getBody(); | |
| } | |
| @Override | |
| public void onSubscribe(Flow.Subscription subscription) { | |
| ofString.onSubscribe(subscription); | |
| } | |
| @Override | |
| public void onNext(List<ByteBuffer> buffers) { | |
| // Muck any data beyond the give limit, since there shouldn't | |
| // be any of interest to the HTTP Client. | |
| for (ByteBuffer buffer : buffers) { | |
| if (buffer.isReadOnly()) | |
| continue; | |
| if (buffer.limit() != buffer.capacity()) { | |
| final int limit = buffer.limit(); | |
| final int position = buffer.position(); | |
| buffer.position(buffer.limit()); | |
| buffer.limit(buffer.capacity()); | |
| while (buffer.hasRemaining()) | |
| buffer.put((byte)'X'); | |
| buffer.position(position); // restore original position | |
| buffer.limit(limit); // restore original limit | |
| } | |
| } | |
| ofString.onNext(buffers); | |
| } | |
| @Override | |
| public void onError(Throwable throwable) { | |
| ofString.onError(throwable); | |
| throwable.printStackTrace(); | |
| fail("UNEXPECTED:" + throwable); | |
| } | |
| @Override | |
| public void onComplete() { | |
| ofString.onComplete(); | |
| } | |
| } | |
| static String serverAuthority(HttpServer server) { | |
| return InetAddress.getLoopbackAddress().getHostName() + ":" | |
| + server.getAddress().getPort(); | |
| } | |
| @BeforeTest | |
| public void setup() throws Exception { | |
| sslContext = new SimpleSSLContext().get(); | |
| if (sslContext == null) | |
| throw new AssertionError("Unexpected null sslContext"); | |
| InetSocketAddress sa = new InetSocketAddress(InetAddress.getLoopbackAddress(), 0); | |
| httpTestServer = HttpServer.create(sa, 0); | |
| httpTestServer.createContext("/http1/fixed", new Http1FixedHandler()); | |
| httpFixedURI = "http://" + serverAuthority(httpTestServer) + "/http1/fixed"; | |
| httpTestServer.createContext("/http1/chunked", new Http1ChunkedHandler()); | |
| httpChunkedURI = "http://" + serverAuthority(httpTestServer) + "/http1/chunked"; | |
| httpsTestServer = HttpsServer.create(sa, 0); | |
| httpsTestServer.setHttpsConfigurator(new TestServerConfigurator(sa.getAddress(), sslContext)); | |
| httpsTestServer.createContext("/https1/fixed", new Http1FixedHandler()); | |
| httpsFixedURI = "https://" + serverAuthority(httpsTestServer) + "/https1/fixed"; | |
| httpsTestServer.createContext("/https1/chunked", new Http1ChunkedHandler()); | |
| httpsChunkedURI = "https://" + serverAuthority(httpsTestServer) + "/https1/chunked"; | |
| http2TestServer = new Http2TestServer("localhost", false, 0); | |
| http2TestServer.addHandler(new Http2FixedHandler(), "/http2/fixed"); | |
| http2FixedURI = "http://" + http2TestServer.serverAuthority()+ "/http2/fixed"; | |
| http2TestServer.addHandler(new Http2VariableHandler(), "/http2/variable"); | |
| http2VariableURI = "http://" + http2TestServer.serverAuthority() + "/http2/variable"; | |
| https2TestServer = new Http2TestServer("localhost", true, sslContext); | |
| https2TestServer.addHandler(new Http2FixedHandler(), "/https2/fixed"); | |
| https2FixedURI = "https://" + https2TestServer.serverAuthority() + "/https2/fixed"; | |
| https2TestServer.addHandler(new Http2VariableHandler(), "/https2/variable"); | |
| https2VariableURI = "https://" + https2TestServer.serverAuthority() + "/https2/variable"; | |
| httpTestServer.start(); | |
| httpsTestServer.start(); | |
| http2TestServer.start(); | |
| https2TestServer.start(); | |
| } | |
| @AfterTest | |
| public void teardown() throws Exception { | |
| httpTestServer.stop(0); | |
| httpsTestServer.stop(0); | |
| http2TestServer.stop(); | |
| https2TestServer.stop(); | |
| } | |
| interface SendResponseHeadersFunction { | |
| void apply(int responseCode, long responseLength) throws IOException; | |
| } | |
| // A handler implementation that replies with 200 OK. If the exchange's uri | |
| // has a query, then it must be an integer, which is used as an index to | |
| // select the particular response body, e.g. /http2/x?5 -> BODIES[5] | |
| static void serverHandlerImpl(InputStream inputStream, | |
| OutputStream outputStream, | |
| URI uri, | |
| SendResponseHeadersFunction sendResponseHeadersFunction) | |
| throws IOException | |
| { | |
| try (InputStream is = inputStream; | |
| OutputStream os = outputStream) { | |
| is.readAllBytes(); | |
| String magicQuery = uri.getQuery(); | |
| if (magicQuery != null) { | |
| int bodyIndex = Integer.valueOf(magicQuery); | |
| String body = BODIES[bodyIndex]; | |
| byte[] bytes = body.getBytes(UTF_8); | |
| sendResponseHeadersFunction.apply(200, bytes.length); | |
| int offset = 0; | |
| // Deliberately attempt to reply with several relatively | |
| // small data frames ( each write corresponds to its own | |
| // data frame ). Additionally, yield, to encourage other | |
| // handlers to execute, therefore increasing the likelihood | |
| // of multiple different-stream related frames in the | |
| // client's read buffer. | |
| while (offset < bytes.length) { | |
| int length = Math.min(bytes.length - offset, 64); | |
| os.write(bytes, offset, length); | |
| os.flush(); | |
| offset += length; | |
| Thread.yield(); | |
| } | |
| } else { | |
| sendResponseHeadersFunction.apply(200, 1); | |
| os.write('A'); | |
| } | |
| } | |
| } | |
| static class Http1FixedHandler implements HttpHandler { | |
| @Override | |
| public void handle(HttpExchange t) throws IOException { | |
| serverHandlerImpl(t.getRequestBody(), | |
| t.getResponseBody(), | |
| t.getRequestURI(), | |
| (rcode, length) -> t.sendResponseHeaders(rcode, length)); | |
| } | |
| } | |
| static class Http1ChunkedHandler implements HttpHandler { | |
| @Override | |
| public void handle(HttpExchange t) throws IOException { | |
| serverHandlerImpl(t.getRequestBody(), | |
| t.getResponseBody(), | |
| t.getRequestURI(), | |
| (rcode, ignored) -> t.sendResponseHeaders(rcode, 0 /*chunked*/)); | |
| } | |
| } | |
| static class Http2FixedHandler implements Http2Handler { | |
| @Override | |
| public void handle(Http2TestExchange t) throws IOException { | |
| serverHandlerImpl(t.getRequestBody(), | |
| t.getResponseBody(), | |
| t.getRequestURI(), | |
| (rcode, length) -> t.sendResponseHeaders(rcode, length)); | |
| } | |
| } | |
| static class Http2VariableHandler implements Http2Handler { | |
| @Override | |
| public void handle(Http2TestExchange t) throws IOException { | |
| serverHandlerImpl(t.getRequestBody(), | |
| t.getResponseBody(), | |
| t.getRequestURI(), | |
| (rcode, ignored) -> t.sendResponseHeaders(rcode, 0 /* no Content-Length */)); | |
| } | |
| } | |
| } |