| /* | |
| * Copyright (c) 2020, 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 8252374 | |
| * @library /test/lib /test/jdk/java/net/httpclient/lib | |
| * @build jdk.test.lib.net.SimpleSSLContext jdk.httpclient.test.lib.common.HttpServerAdapters | |
| * ReferenceTracker AggregateRequestBodyTest | |
| * @run testng/othervm -Djdk.internal.httpclient.debug=true | |
| * -Djdk.httpclient.HttpClient.log=requests,responses,errors | |
| * AggregateRequestBodyTest | |
| * @summary Tests HttpRequest.BodyPublishers::concat | |
| */ | |
| import java.net.URI; | |
| import java.net.http.HttpClient; | |
| import java.net.http.HttpRequest; | |
| import java.net.http.HttpRequest.BodyPublisher; | |
| import java.net.http.HttpRequest.BodyPublishers; | |
| import java.net.http.HttpResponse; | |
| import java.net.http.HttpResponse.BodyHandlers; | |
| import java.nio.ByteBuffer; | |
| import java.util.Arrays; | |
| import java.util.LinkedHashMap; | |
| import java.util.List; | |
| import java.util.Map; | |
| import java.util.concurrent.CompletableFuture; | |
| import java.util.concurrent.CompletionException; | |
| import java.util.concurrent.ConcurrentHashMap; | |
| import java.util.concurrent.ConcurrentLinkedDeque; | |
| import java.util.concurrent.ConcurrentMap; | |
| import java.util.concurrent.Executor; | |
| import java.util.concurrent.Executors; | |
| import java.util.concurrent.Flow; | |
| import java.util.concurrent.Flow.Subscriber; | |
| import java.util.concurrent.Flow.Subscription; | |
| import java.util.concurrent.Semaphore; | |
| import java.util.concurrent.TimeUnit; | |
| import java.util.concurrent.TimeoutException; | |
| import java.util.concurrent.atomic.AtomicLong; | |
| import java.util.concurrent.atomic.AtomicReference; | |
| import java.util.function.Consumer; | |
| import java.util.function.Supplier; | |
| import java.util.stream.Collectors; | |
| import java.util.stream.LongStream; | |
| import java.util.stream.Stream; | |
| import jdk.httpclient.test.lib.common.HttpServerAdapters; | |
| import javax.net.ssl.SSLContext; | |
| import jdk.test.lib.net.SimpleSSLContext; | |
| import org.testng.Assert; | |
| import org.testng.ITestContext; | |
| import org.testng.ITestResult; | |
| import org.testng.SkipException; | |
| import org.testng.annotations.AfterClass; | |
| import org.testng.annotations.AfterTest; | |
| import org.testng.annotations.BeforeMethod; | |
| import org.testng.annotations.BeforeTest; | |
| import org.testng.annotations.DataProvider; | |
| import org.testng.annotations.Test; | |
| import static java.lang.System.out; | |
| import static java.net.http.HttpClient.Version.HTTP_1_1; | |
| import static java.net.http.HttpClient.Version.HTTP_2; | |
| import static org.testng.Assert.assertEquals; | |
| import static org.testng.Assert.assertFalse; | |
| import static org.testng.Assert.assertTrue; | |
| import static org.testng.Assert.expectThrows; | |
| public class AggregateRequestBodyTest implements HttpServerAdapters { | |
| SSLContext sslContext; | |
| HttpTestServer http1TestServer; // HTTP/1.1 ( http ) | |
| HttpTestServer https1TestServer; // HTTPS/1.1 ( https ) | |
| HttpTestServer http2TestServer; // HTTP/2 ( h2c ) | |
| HttpTestServer https2TestServer; // HTTP/2 ( h2 ) | |
| String http1URI; | |
| String https1URI; | |
| String http2URI; | |
| String https2URI; | |
| static final int RESPONSE_CODE = 200; | |
| static final int ITERATION_COUNT = 4; | |
| static final Class<IllegalArgumentException> IAE = IllegalArgumentException.class; | |
| static final Class<CompletionException> CE = CompletionException.class; | |
| // a shared executor helps reduce the amount of threads created by the test | |
| static final Executor executor = new TestExecutor(Executors.newCachedThreadPool()); | |
| static final ConcurrentMap<String, Throwable> FAILURES = new ConcurrentHashMap<>(); | |
| static volatile boolean tasksFailed; | |
| static final AtomicLong serverCount = new AtomicLong(); | |
| static final AtomicLong clientCount = new AtomicLong(); | |
| static final long start = System.nanoTime(); | |
| public static String now() { | |
| long now = System.nanoTime() - start; | |
| long secs = now / 1000_000_000; | |
| long mill = (now % 1000_000_000) / 1000_000; | |
| long nan = now % 1000_000; | |
| return String.format("[%d s, %d ms, %d ns] ", secs, mill, nan); | |
| } | |
| final ReferenceTracker TRACKER = ReferenceTracker.INSTANCE; | |
| private volatile HttpClient sharedClient; | |
| static class TestExecutor implements Executor { | |
| final AtomicLong tasks = new AtomicLong(); | |
| Executor executor; | |
| TestExecutor(Executor executor) { | |
| this.executor = executor; | |
| } | |
| @Override | |
| public void execute(Runnable command) { | |
| long id = tasks.incrementAndGet(); | |
| executor.execute(() -> { | |
| try { | |
| command.run(); | |
| } catch (Throwable t) { | |
| tasksFailed = true; | |
| System.out.printf(now() + "Task %s failed: %s%n", id, t); | |
| System.err.printf(now() + "Task %s failed: %s%n", id, t); | |
| FAILURES.putIfAbsent("Task " + id, t); | |
| throw t; | |
| } | |
| }); | |
| } | |
| } | |
| protected boolean stopAfterFirstFailure() { | |
| return Boolean.getBoolean("jdk.internal.httpclient.debug"); | |
| } | |
| final AtomicReference<SkipException> skiptests = new AtomicReference<>(); | |
| void checkSkip() { | |
| var skip = skiptests.get(); | |
| if (skip != null) throw skip; | |
| } | |
| static String name(ITestResult result) { | |
| var params = result.getParameters(); | |
| return result.getName() | |
| + (params == null ? "()" : Arrays.toString(result.getParameters())); | |
| } | |
| @BeforeMethod | |
| void beforeMethod(ITestContext context) { | |
| if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { | |
| if (skiptests.get() == null) { | |
| SkipException skip = new SkipException("some tests failed"); | |
| skip.setStackTrace(new StackTraceElement[0]); | |
| skiptests.compareAndSet(null, skip); | |
| } | |
| } | |
| } | |
| @AfterClass | |
| static final void printFailedTests(ITestContext context) { | |
| out.println("\n========================="); | |
| try { | |
| var failed = context.getFailedTests().getAllResults().stream() | |
| .collect(Collectors.toMap(r -> name(r), ITestResult::getThrowable)); | |
| FAILURES.putAll(failed); | |
| out.printf("%n%sCreated %d servers and %d clients%n", | |
| now(), serverCount.get(), clientCount.get()); | |
| if (FAILURES.isEmpty()) return; | |
| out.println("Failed tests: "); | |
| FAILURES.entrySet().forEach((e) -> { | |
| out.printf("\t%s: %s%n", e.getKey(), e.getValue()); | |
| e.getValue().printStackTrace(out); | |
| e.getValue().printStackTrace(); | |
| }); | |
| if (tasksFailed) { | |
| System.out.println("WARNING: Some tasks failed"); | |
| } | |
| } finally { | |
| out.println("\n=========================\n"); | |
| } | |
| } | |
| private String[] uris() { | |
| return new String[] { | |
| http1URI, | |
| https1URI, | |
| http2URI, | |
| https2URI, | |
| }; | |
| } | |
| static AtomicLong URICOUNT = new AtomicLong(); | |
| @DataProvider(name = "variants") | |
| public Object[][] variants(ITestContext context) { | |
| if (stopAfterFirstFailure() && context.getFailedTests().size() > 0) { | |
| return new Object[0][]; | |
| } | |
| String[] uris = uris(); | |
| Object[][] result = new Object[uris.length * 2][]; | |
| int i = 0; | |
| for (boolean sameClient : List.of(false, true)) { | |
| for (String uri : uris()) { | |
| result[i++] = new Object[]{uri, sameClient}; | |
| } | |
| } | |
| assert i == uris.length * 2; | |
| return result; | |
| } | |
| private HttpClient makeNewClient() { | |
| clientCount.incrementAndGet(); | |
| HttpClient client = HttpClient.newBuilder() | |
| .proxy(HttpClient.Builder.NO_PROXY) | |
| .executor(executor) | |
| .sslContext(sslContext) | |
| .build(); | |
| return TRACKER.track(client); | |
| } | |
| HttpClient newHttpClient(boolean share) { | |
| if (!share) return makeNewClient(); | |
| HttpClient shared = sharedClient; | |
| if (shared != null) return shared; | |
| synchronized (this) { | |
| shared = sharedClient; | |
| if (shared == null) { | |
| shared = sharedClient = makeNewClient(); | |
| } | |
| return shared; | |
| } | |
| } | |
| static final List<String> BODIES = List.of( | |
| "Lorem ipsum", | |
| "dolor sit amet", | |
| "consectetur adipiscing elit, sed do eiusmod tempor", | |
| "quis nostrud exercitation ullamco", | |
| "laboris nisi", | |
| "ut", | |
| "aliquip ex ea commodo consequat." + | |
| "Duis aute irure dolor in reprehenderit in voluptate velit esse" + | |
| "cillum dolore eu fugiat nulla pariatur.", | |
| "Excepteur sint occaecat cupidatat non proident." | |
| ); | |
| static BodyPublisher[] publishers(String... content) { | |
| if (content == null) return null; | |
| BodyPublisher[] result = new BodyPublisher[content.length]; | |
| for (int i=0; i < content.length ; i++) { | |
| result[i] = content[i] == null ? null : BodyPublishers.ofString(content[i]); | |
| } | |
| return result; | |
| } | |
| static String[] strings(String... s) { | |
| return s; | |
| } | |
| @DataProvider(name = "sparseContent") | |
| Object[][] nulls() { | |
| return new Object[][] { | |
| {"null array", null}, | |
| {"null element", strings((String)null)}, | |
| {"null first element", strings(null, "one")}, | |
| {"null second element", strings( "one", null)}, | |
| {"null third element", strings( "one", "two", null)}, | |
| {"null fourth element", strings( "one", "two", "three", null)}, | |
| {"null random element", strings( "one", "two", "three", null, "five")}, | |
| }; | |
| } | |
| static List<Long> lengths(long... lengths) { | |
| return LongStream.of(lengths) | |
| .mapToObj(Long::valueOf) | |
| .collect(Collectors.toList()); | |
| } | |
| @DataProvider(name = "contentLengths") | |
| Object[][] contentLengths() { | |
| return new Object[][] { | |
| {-1, lengths(-1)}, | |
| {-42, lengths(-42)}, | |
| {42, lengths(42)}, | |
| {42, lengths(10, 0, 20, 0, 12)}, | |
| {-1, lengths(10, 0, 20, -1, 12)}, | |
| {-1, lengths(-1, 0, 20, 10, 12)}, | |
| {-1, lengths(10, 0, 20, 12, -1)}, | |
| {-1, lengths(10, 0, 20, -10, 12)}, | |
| {-1, lengths(-10, 0, 20, 10, 12)}, | |
| {-1, lengths(10, 0, 20, 12, -10)}, | |
| {-1, lengths(10, 0, Long.MIN_VALUE, -1, 12)}, | |
| {-1, lengths(-1, 0, Long.MIN_VALUE, 10, 12)}, | |
| {-1, lengths(10, Long.MIN_VALUE, 20, 12, -1)}, | |
| {Long.MAX_VALUE, lengths(10, Long.MAX_VALUE - 42L, 20, 0, 12)}, | |
| {-1, lengths(10, Long.MAX_VALUE - 40L, 20, 0, 12)}, | |
| {-1, lengths(10, Long.MAX_VALUE - 12L, 20, 0, 12)}, | |
| {-1, lengths(10, Long.MAX_VALUE/2L, Long.MAX_VALUE/2L + 1L, 0, 12)}, | |
| {-1, lengths(10, Long.MAX_VALUE/2L, -1, Long.MAX_VALUE/2L + 1L, 12)}, | |
| {-1, lengths(10, Long.MAX_VALUE, 12, Long.MAX_VALUE, 20)}, | |
| {-1, lengths(10, Long.MAX_VALUE, Long.MAX_VALUE, 12, 20)}, | |
| {-1, lengths(0, Long.MAX_VALUE, Long.MAX_VALUE, 12, 20)}, | |
| {-1, lengths(Long.MAX_VALUE, Long.MAX_VALUE, 12, 0, 20)} | |
| }; | |
| } | |
| @DataProvider(name="negativeRequests") | |
| Object[][] negativeRequests() { | |
| return new Object[][] { | |
| {0L}, {-1L}, {-2L}, {Long.MIN_VALUE + 1L}, {Long.MIN_VALUE} | |
| }; | |
| } | |
| static class ContentLengthPublisher implements BodyPublisher { | |
| final long length; | |
| ContentLengthPublisher(long length) { | |
| this.length = length; | |
| } | |
| @Override | |
| public long contentLength() { | |
| return length; | |
| } | |
| @Override | |
| public void subscribe(Subscriber<? super ByteBuffer> subscriber) { | |
| } | |
| static ContentLengthPublisher[] of(List<Long> lengths) { | |
| return lengths.stream() | |
| .map(ContentLengthPublisher::new) | |
| .toArray(ContentLengthPublisher[]::new); | |
| } | |
| } | |
| /** | |
| * A dummy publisher that allows to call onError on its subscriber (or not...). | |
| */ | |
| static class PublishWithError implements BodyPublisher { | |
| final ConcurrentHashMap<Subscriber<?>, ErrorSubscription> subscribers = new ConcurrentHashMap<>(); | |
| final long length; | |
| final List<String> content; | |
| final int errorAt; | |
| final Supplier<? extends Throwable> errorSupplier; | |
| PublishWithError(List<String> content, int errorAt, Supplier<? extends Throwable> supplier) { | |
| this.content = content; | |
| this.errorAt = errorAt; | |
| this.errorSupplier = supplier; | |
| length = content.stream().mapToInt(String::length).sum(); | |
| } | |
| boolean hasErrors() { | |
| return errorAt < content.size(); | |
| } | |
| @Override | |
| public long contentLength() { | |
| return length; | |
| } | |
| @Override | |
| public void subscribe(Subscriber<? super ByteBuffer> subscriber) { | |
| ErrorSubscription subscription = new ErrorSubscription(subscriber); | |
| subscribers.put(subscriber, subscription); | |
| subscriber.onSubscribe(subscription); | |
| } | |
| class ErrorSubscription implements Flow.Subscription { | |
| volatile boolean cancelled; | |
| volatile int at; | |
| final Subscriber<? super ByteBuffer> subscriber; | |
| ErrorSubscription(Subscriber<? super ByteBuffer> subscriber) { | |
| this.subscriber = subscriber; | |
| } | |
| @Override | |
| public void request(long n) { | |
| while (!cancelled && --n >= 0 && at < Math.min(errorAt+1, content.size())) { | |
| if (at++ == errorAt) { | |
| subscriber.onError(errorSupplier.get()); | |
| return; | |
| } else if (at <= content.size()){ | |
| subscriber.onNext(ByteBuffer.wrap( | |
| content.get(at-1).getBytes())); | |
| if (at == content.size()) { | |
| subscriber.onComplete(); | |
| return; | |
| } | |
| } | |
| } | |
| } | |
| @Override | |
| public void cancel() { | |
| cancelled = true; | |
| } | |
| } | |
| } | |
| static class RequestSubscriber implements Flow.Subscriber<ByteBuffer> { | |
| final CompletableFuture<Subscription> subscriptionCF = new CompletableFuture<>(); | |
| final ConcurrentLinkedDeque<ByteBuffer> items = new ConcurrentLinkedDeque<>(); | |
| final CompletableFuture<List<ByteBuffer>> resultCF = new CompletableFuture<>(); | |
| final Semaphore semaphore = new Semaphore(0); | |
| @Override | |
| public void onSubscribe(Subscription subscription) { | |
| this.subscriptionCF.complete(subscription); | |
| } | |
| @Override | |
| public void onNext(ByteBuffer item) { | |
| items.addLast(item); | |
| int available = semaphore.availablePermits(); | |
| if (available > Integer.MAX_VALUE - 8) { | |
| onError(new IllegalStateException("too many buffers in queue: " + available)); | |
| } | |
| semaphore.release(); | |
| } | |
| @Override | |
| public void onError(Throwable throwable) { | |
| resultCF.completeExceptionally(throwable); | |
| } | |
| @Override | |
| public void onComplete() { | |
| resultCF.complete(items.stream().collect(Collectors.toUnmodifiableList())); | |
| } | |
| public ByteBuffer take() { | |
| // it is not guaranteed that the buffer will be added to | |
| // the queue in the same thread that calls request(1). | |
| try { | |
| semaphore.acquire(); | |
| } catch (InterruptedException x) { | |
| Thread.currentThread().interrupt(); | |
| throw new CompletionException(x); | |
| } | |
| return items.pop(); | |
| } | |
| CompletableFuture<List<ByteBuffer>> resultCF() { return resultCF; } | |
| } | |
| static String stringFromBuffer(ByteBuffer buffer) { | |
| byte[] bytes = new byte[buffer.remaining()]; | |
| buffer.get(bytes); | |
| return new String(bytes); | |
| } | |
| String stringFromBytes(Stream<ByteBuffer> buffers) { | |
| return buffers.map(AggregateRequestBodyTest::stringFromBuffer) | |
| .collect(Collectors.joining()); | |
| } | |
| static PublishWithError withNoError(String content) { | |
| return new PublishWithError(List.of(content), 1, | |
| () -> new AssertionError("Should not happen!")); | |
| } | |
| static PublishWithError withNoError(List<String> content) { | |
| return new PublishWithError(content, content.size(), | |
| () -> new AssertionError("Should not happen!")); | |
| } | |
| @Test(dataProvider = "sparseContent") // checks that NPE is thrown | |
| public void testNullPointerException(String description, String[] content) { | |
| checkSkip(); | |
| BodyPublisher[] publishers = publishers(content); | |
| Assert.assertThrows(NullPointerException.class, () -> BodyPublishers.concat(publishers)); | |
| } | |
| // Verifies that an empty array creates a "noBody" publisher | |
| @Test | |
| public void testEmpty() { | |
| checkSkip(); | |
| BodyPublisher publisher = BodyPublishers.concat(); | |
| RequestSubscriber subscriber = new RequestSubscriber(); | |
| assertEquals(publisher.contentLength(), 0); | |
| publisher.subscribe(subscriber); | |
| subscriber.subscriptionCF.thenAccept(s -> s.request(1)); | |
| List<ByteBuffer> result = subscriber.resultCF.join(); | |
| assertEquals(result, List.of()); | |
| assertTrue(subscriber.items.isEmpty()); | |
| } | |
| // verifies that error emitted by upstream publishers are propagated downstream. | |
| @Test(dataProvider = "sparseContent") // nulls are replaced with error publisher | |
| public void testOnError(String description, String[] content) { | |
| checkSkip(); | |
| final RequestSubscriber subscriber = new RequestSubscriber(); | |
| final PublishWithError errorPublisher; | |
| final BodyPublisher[] publishers; | |
| String result = BODIES.stream().collect(Collectors.joining()); | |
| if (content == null) { | |
| content = List.of(result).toArray(String[]::new); | |
| errorPublisher = new PublishWithError(BODIES, BODIES.size(), | |
| () -> new AssertionError("Unexpected!!")); | |
| publishers = List.of(errorPublisher).toArray(new BodyPublisher[0]); | |
| description = "No error"; | |
| } else { | |
| publishers = publishers(content); | |
| description = description.replace("null", "error at"); | |
| errorPublisher = new PublishWithError(BODIES, 2, () -> new Exception("expected")); | |
| } | |
| result = ""; | |
| boolean hasErrors = false; | |
| for (int i=0; i < content.length; i++) { | |
| if (content[i] == null) { | |
| publishers[i] = errorPublisher; | |
| if (hasErrors) continue; | |
| if (!errorPublisher.hasErrors()) { | |
| result = result + errorPublisher | |
| .content.stream().collect(Collectors.joining()); | |
| } else { | |
| result = result + errorPublisher.content | |
| .stream().limit(errorPublisher.errorAt) | |
| .collect(Collectors.joining()); | |
| result = result + "<error>"; | |
| hasErrors = true; | |
| } | |
| } else if (!hasErrors) { | |
| result = result + content[i]; | |
| } | |
| } | |
| BodyPublisher publisher = BodyPublishers.concat(publishers); | |
| publisher.subscribe(subscriber); | |
| subscriber.subscriptionCF.thenAccept(s -> s.request(Long.MAX_VALUE)); | |
| if (errorPublisher.hasErrors()) { | |
| CompletionException ce = expectThrows(CompletionException.class, | |
| () -> subscriber.resultCF.join()); | |
| out.println(description + ": got expected " + ce); | |
| assertEquals(ce.getCause().getClass(), Exception.class); | |
| assertEquals(stringFromBytes(subscriber.items.stream()) + "<error>", result); | |
| } else { | |
| assertEquals(stringFromBytes(subscriber.resultCF.join().stream()), result); | |
| out.println(description + ": got expected result: " + result); | |
| } | |
| } | |
| // Verifies that if an upstream publisher has an unknown length, the | |
| // aggregate publisher will have an unknown length as well. Otherwise | |
| // the length should be known. | |
| @Test(dataProvider = "sparseContent") // nulls are replaced with unknown length | |
| public void testUnknownContentLength(String description, String[] content) { | |
| checkSkip(); | |
| if (content == null) { | |
| content = BODIES.toArray(String[]::new); | |
| description = "BODIES (known length)"; | |
| } else { | |
| description = description.replace("null", "length(-1)"); | |
| } | |
| BodyPublisher[] publishers = publishers(content); | |
| BodyPublisher nolength = new BodyPublisher() { | |
| final BodyPublisher missing = BodyPublishers.ofString("missing"); | |
| @Override | |
| public long contentLength() { return -1; } | |
| @Override | |
| public void subscribe(Subscriber<? super ByteBuffer> subscriber) { | |
| missing.subscribe(subscriber); | |
| } | |
| }; | |
| long length = 0; | |
| for (int i=0; i < content.length; i++) { | |
| if (content[i] == null) { | |
| publishers[i] = nolength; | |
| length = -1; | |
| } else if (length >= 0) { | |
| length += content[i].length(); | |
| } | |
| } | |
| out.printf("testUnknownContentLength(%s): %d%n", description, length); | |
| BodyPublisher publisher = BodyPublishers.concat(publishers); | |
| assertEquals(publisher.contentLength(), length, | |
| description.replace("null", "length(-1)")); | |
| } | |
| private static final Throwable completionCause(CompletionException x) { | |
| while (x.getCause() instanceof CompletionException) { | |
| x = (CompletionException)x.getCause(); | |
| } | |
| return x.getCause(); | |
| } | |
| @Test(dataProvider = "negativeRequests") | |
| public void testNegativeRequest(long n) { | |
| checkSkip(); | |
| assert n <= 0 : "test for negative request called with n > 0 : " + n; | |
| BodyPublisher[] publishers = ContentLengthPublisher.of(List.of(1L, 2L, 3L)); | |
| BodyPublisher publisher = BodyPublishers.concat(publishers); | |
| RequestSubscriber subscriber = new RequestSubscriber(); | |
| publisher.subscribe(subscriber); | |
| Subscription subscription = subscriber.subscriptionCF.join(); | |
| subscription.request(n); | |
| CompletionException expected = expectThrows(CE, () -> subscriber.resultCF.join()); | |
| Throwable cause = completionCause(expected); | |
| if (cause instanceof IllegalArgumentException) { | |
| System.out.printf("Got expected IAE for %d: %s%n", n, cause); | |
| } else { | |
| throw new AssertionError("Unexpected exception: " + cause, | |
| (cause == null) ? expected : cause); | |
| } | |
| } | |
| static BodyPublisher[] ofStrings(String... strings) { | |
| return Stream.of(strings).map(BodyPublishers::ofString).toArray(BodyPublisher[]::new); | |
| } | |
| @Test | |
| public void testPositiveRequests() { | |
| checkSkip(); | |
| // A composite array of publishers | |
| BodyPublisher[] publishers = Stream.of( | |
| Stream.of(ofStrings("Lorem", " ", "ipsum", " ")), | |
| Stream.of(BodyPublishers.concat(ofStrings("dolor", " ", "sit", " ", "amet", ", "))), | |
| Stream.<BodyPublisher>of(withNoError(List.of("consectetur", " ", "adipiscing"))), | |
| Stream.of(ofStrings(" ")), | |
| Stream.of(BodyPublishers.concat(ofStrings("elit", "."))) | |
| ).flatMap((s) -> s).toArray(BodyPublisher[]::new); | |
| BodyPublisher publisher = BodyPublishers.concat(publishers); | |
| // Test that we can request all 13 items in a single request call. | |
| RequestSubscriber requestSubscriber1 = new RequestSubscriber(); | |
| publisher.subscribe(requestSubscriber1); | |
| Subscription subscription1 = requestSubscriber1.subscriptionCF.join(); | |
| subscription1.request(16); | |
| // onNext() may not be called in the same thread than request() | |
| List<ByteBuffer> list1 = requestSubscriber1.resultCF().join(); | |
| assertTrue(requestSubscriber1.resultCF().isDone()); | |
| String result1 = stringFromBytes(list1.stream()); | |
| assertEquals(result1, "Lorem ipsum dolor sit amet, consectetur adipiscing elit."); | |
| System.out.println("Got expected sentence with one request: \"%s\"".formatted(result1)); | |
| // Test that we can split our requests call any which way we want | |
| // (whether in the 'middle of a publisher' or at the boundaries. | |
| RequestSubscriber requestSubscriber2 = new RequestSubscriber(); | |
| publisher.subscribe(requestSubscriber2); | |
| Subscription subscription2 = requestSubscriber2.subscriptionCF.join(); | |
| subscription2.request(1); | |
| assertFalse(requestSubscriber2.resultCF().isDone()); | |
| subscription2.request(10); | |
| assertFalse(requestSubscriber2.resultCF().isDone()); | |
| subscription2.request(4); | |
| assertFalse(requestSubscriber2.resultCF().isDone()); | |
| subscription2.request(1); | |
| List<ByteBuffer> list2 = requestSubscriber2.resultCF().join(); | |
| assertTrue(requestSubscriber2.resultCF().isDone()); | |
| String result2 = stringFromBytes(list2.stream()); | |
| assertEquals(result2, "Lorem ipsum dolor sit amet, consectetur adipiscing elit."); | |
| System.out.println("Got expected sentence with 4 requests: \"%s\"".formatted(result1)); | |
| } | |
| @Test(dataProvider = "contentLengths") | |
| public void testContentLength(long expected, List<Long> lengths) { | |
| checkSkip(); | |
| BodyPublisher[] publishers = ContentLengthPublisher.of(lengths); | |
| BodyPublisher aggregate = BodyPublishers.concat(publishers); | |
| assertEquals(aggregate.contentLength(), expected, | |
| "Unexpected result for %s".formatted(lengths)); | |
| } | |
| // Verifies that cancelling the subscription ensure that downstream | |
| // publishers are no longer subscribed etc... | |
| @Test | |
| public void testCancel() { | |
| checkSkip(); | |
| BodyPublisher[] publishers = BODIES.stream() | |
| .map(BodyPublishers::ofString) | |
| .toArray(BodyPublisher[]::new); | |
| BodyPublisher publisher = BodyPublishers.concat(publishers); | |
| assertEquals(publisher.contentLength(), | |
| BODIES.stream().mapToInt(String::length).sum()); | |
| Map<RequestSubscriber, String> subscribers = new LinkedHashMap<>(); | |
| for (int n=0; n < BODIES.size(); n++) { | |
| String description = String.format( | |
| "cancel after %d/%d onNext() invocations", | |
| n, BODIES.size()); | |
| RequestSubscriber subscriber = new RequestSubscriber(); | |
| publisher.subscribe(subscriber); | |
| Subscription subscription = subscriber.subscriptionCF.join(); | |
| subscribers.put(subscriber, description); | |
| // receive half the data | |
| for (int i = 0; i < n; i++) { | |
| subscription.request(1); | |
| ByteBuffer buffer = subscriber.take(); | |
| } | |
| // cancel subscription | |
| subscription.cancel(); | |
| // request the rest... | |
| subscription.request(Long.MAX_VALUE); | |
| } | |
| CompletableFuture[] results = subscribers.keySet() | |
| .stream().map(RequestSubscriber::resultCF) | |
| .toArray(CompletableFuture[]::new); | |
| CompletableFuture<?> any = CompletableFuture.anyOf(results); | |
| // subscription was cancelled, so nothing should be received... | |
| try { | |
| TimeoutException x = Assert.expectThrows(TimeoutException.class, | |
| () -> any.get(5, TimeUnit.SECONDS)); | |
| out.println("Got expected " + x); | |
| } finally { | |
| subscribers.keySet().stream() | |
| .filter(rs -> rs.resultCF.isDone()) | |
| .forEach(rs -> System.err.printf( | |
| "Failed: %s completed with %s", | |
| subscribers.get(rs), rs.resultCF)); | |
| } | |
| Consumer<RequestSubscriber> check = (rs) -> { | |
| Assert.assertTrue(rs.items.isEmpty(), subscribers.get(rs) + " has items"); | |
| Assert.assertFalse(rs.resultCF.isDone(), subscribers.get(rs) + " was not cancelled"); | |
| out.println(subscribers.get(rs) + ": PASSED"); | |
| }; | |
| subscribers.keySet().stream().forEach(check); | |
| } | |
| // Verifies that cancelling the subscription is propagated downstream | |
| @Test | |
| public void testCancelSubscription() { | |
| checkSkip(); | |
| PublishWithError upstream = new PublishWithError(BODIES, BODIES.size(), | |
| () -> new AssertionError("should not come here")); | |
| BodyPublisher publisher = BodyPublishers.concat(upstream); | |
| assertEquals(publisher.contentLength(), | |
| BODIES.stream().mapToInt(String::length).sum()); | |
| Map<RequestSubscriber, String> subscribers = new LinkedHashMap<>(); | |
| for (int n=0; n < BODIES.size(); n++) { | |
| String description = String.format( | |
| "cancel after %d/%d onNext() invocations", | |
| n, BODIES.size()); | |
| RequestSubscriber subscriber = new RequestSubscriber(); | |
| publisher.subscribe(subscriber); | |
| Subscription subscription = subscriber.subscriptionCF.join(); | |
| subscribers.put(subscriber, description); | |
| // receive half the data | |
| for (int i = 0; i < n; i++) { | |
| subscription.request(1); | |
| ByteBuffer buffer = subscriber.items.pop(); | |
| } | |
| // cancel subscription | |
| subscription.cancel(); | |
| // request the rest... | |
| subscription.request(Long.MAX_VALUE); | |
| assertTrue(upstream.subscribers.get(subscriber).cancelled, | |
| description + " upstream subscription not cancelled"); | |
| out.println(description + " upstream subscription was properly cancelled"); | |
| } | |
| CompletableFuture[] results = subscribers.keySet() | |
| .stream().map(RequestSubscriber::resultCF) | |
| .toArray(CompletableFuture[]::new); | |
| CompletableFuture<?> any = CompletableFuture.anyOf(results); | |
| // subscription was cancelled, so nothing should be received... | |
| try { | |
| TimeoutException x = Assert.expectThrows(TimeoutException.class, | |
| () -> any.get(5, TimeUnit.SECONDS)); | |
| out.println("Got expected " + x); | |
| } finally { | |
| subscribers.keySet().stream() | |
| .filter(rs -> rs.resultCF.isDone()) | |
| .forEach(rs -> System.err.printf( | |
| "Failed: %s completed with %s", | |
| subscribers.get(rs), rs.resultCF)); | |
| } | |
| Consumer<RequestSubscriber> check = (rs) -> { | |
| Assert.assertTrue(rs.items.isEmpty(), subscribers.get(rs) + " has items"); | |
| Assert.assertFalse(rs.resultCF.isDone(), subscribers.get(rs) + " was not cancelled"); | |
| out.println(subscribers.get(rs) + ": PASSED"); | |
| }; | |
| subscribers.keySet().stream().forEach(check); | |
| } | |
| @Test(dataProvider = "variants") | |
| public void test(String uri, boolean sameClient) throws Exception { | |
| checkSkip(); | |
| System.out.printf("Request to %s (sameClient: %s)%n", uri, sameClient); | |
| System.err.printf("Request to %s (sameClient: %s)%n", uri, sameClient); | |
| HttpClient client = newHttpClient(sameClient); | |
| BodyPublisher publisher = BodyPublishers.concat( | |
| BODIES.stream() | |
| .map(BodyPublishers::ofString) | |
| .toArray(HttpRequest.BodyPublisher[]::new) | |
| ); | |
| HttpRequest request = HttpRequest.newBuilder(URI.create(uri)) | |
| .POST(publisher) | |
| .build(); | |
| for (int i = 0; i < ITERATION_COUNT; i++) { | |
| System.out.println(uri + ": Iteration: " + i); | |
| System.err.println(uri + ": Iteration: " + i); | |
| HttpResponse<String> response = client.send(request, BodyHandlers.ofString()); | |
| int expectedResponse = RESPONSE_CODE; | |
| if (response.statusCode() != expectedResponse) | |
| throw new RuntimeException("wrong response code " + Integer.toString(response.statusCode())); | |
| assertEquals(response.body(), BODIES.stream().collect(Collectors.joining())); | |
| } | |
| System.out.println("test: DONE"); | |
| } | |
| @BeforeTest | |
| public void setup() throws Exception { | |
| sslContext = new SimpleSSLContext().get(); | |
| if (sslContext == null) | |
| throw new AssertionError("Unexpected null sslContext"); | |
| HttpTestHandler handler = new HttpTestEchoHandler(); | |
| http1TestServer = HttpTestServer.create(HTTP_1_1); | |
| http1TestServer.addHandler(handler, "/http1/echo/"); | |
| http1URI = "http://" + http1TestServer.serverAuthority() + "/http1/echo/x"; | |
| https1TestServer = HttpTestServer.create(HTTP_1_1, sslContext); | |
| https1TestServer.addHandler(handler, "/https1/echo/"); | |
| https1URI = "https://" + https1TestServer.serverAuthority() + "/https1/echo/x"; | |
| // HTTP/2 | |
| http2TestServer = HttpTestServer.create(HTTP_2); | |
| http2TestServer.addHandler(handler, "/http2/echo/"); | |
| http2URI = "http://" + http2TestServer.serverAuthority() + "/http2/echo/x"; | |
| https2TestServer = HttpTestServer.create(HTTP_2, sslContext); | |
| https2TestServer.addHandler(handler, "/https2/echo/"); | |
| https2URI = "https://" + https2TestServer.serverAuthority() + "/https2/echo/x"; | |
| serverCount.addAndGet(4); | |
| http1TestServer.start(); | |
| https1TestServer.start(); | |
| http2TestServer.start(); | |
| https2TestServer.start(); | |
| } | |
| @AfterTest | |
| public void teardown() throws Exception { | |
| String sharedClientName = | |
| sharedClient == null ? null : sharedClient.toString(); | |
| sharedClient = null; | |
| Thread.sleep(100); | |
| AssertionError fail = TRACKER.check(500); | |
| try { | |
| http1TestServer.stop(); | |
| https1TestServer.stop(); | |
| http2TestServer.stop(); | |
| https2TestServer.stop(); | |
| } finally { | |
| if (fail != null) { | |
| if (sharedClientName != null) { | |
| System.err.println("Shared client name is: " + sharedClientName); | |
| } | |
| throw fail; | |
| } | |
| } | |
| } | |
| } |