1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27 package org.apache.hc.core5.testing.compatibility.http2;
28
29 import java.io.IOException;
30 import java.nio.charset.CodingErrorAction;
31 import java.nio.charset.StandardCharsets;
32 import java.util.Arrays;
33 import java.util.List;
34 import java.util.Objects;
35 import java.util.concurrent.CountDownLatch;
36 import java.util.concurrent.ExecutionException;
37 import java.util.concurrent.Future;
38 import java.util.concurrent.TimeoutException;
39
40 import org.apache.hc.core5.concurrent.FutureCallback;
41 import org.apache.hc.core5.http.ContentType;
42 import org.apache.hc.core5.http.HttpException;
43 import org.apache.hc.core5.http.HttpHost;
44 import org.apache.hc.core5.http.HttpRequest;
45 import org.apache.hc.core5.http.HttpResponse;
46 import org.apache.hc.core5.http.HttpStatus;
47 import org.apache.hc.core5.http.Message;
48 import org.apache.hc.core5.http.Method;
49 import org.apache.hc.core5.http.config.CharCodingConfig;
50 import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester;
51 import org.apache.hc.core5.http.message.BasicHttpRequest;
52 import org.apache.hc.core5.http.nio.AsyncClientEndpoint;
53 import org.apache.hc.core5.http.nio.AsyncEntityProducer;
54 import org.apache.hc.core5.http.nio.entity.DiscardingEntityConsumer;
55 import org.apache.hc.core5.http.nio.entity.StringAsyncEntityConsumer;
56 import org.apache.hc.core5.http.nio.entity.StringAsyncEntityProducer;
57 import org.apache.hc.core5.http.nio.support.AbstractAsyncPushHandler;
58 import org.apache.hc.core5.http.nio.support.BasicRequestProducer;
59 import org.apache.hc.core5.http.nio.support.BasicResponseConsumer;
60 import org.apache.hc.core5.http2.HttpVersionPolicy;
61 import org.apache.hc.core5.http2.config.H2Config;
62 import org.apache.hc.core5.http2.impl.nio.bootstrap.H2RequesterBootstrap;
63 import org.apache.hc.core5.io.CloseMode;
64 import org.apache.hc.core5.reactor.IOReactorConfig;
65 import org.apache.hc.core5.testing.classic.LoggingConnPoolListener;
66 import org.apache.hc.core5.testing.nio.LoggingExceptionCallback;
67 import org.apache.hc.core5.testing.nio.LoggingH2StreamListener;
68 import org.apache.hc.core5.testing.nio.LoggingHttp1StreamListener;
69 import org.apache.hc.core5.testing.nio.LoggingIOSessionDecorator;
70 import org.apache.hc.core5.testing.nio.LoggingIOSessionListener;
71 import org.apache.hc.core5.util.TextUtils;
72 import org.apache.hc.core5.util.Timeout;
73
74 public class H2CompatibilityTest {
75
76 private final HttpAsyncRequester client;
77
78 public static void main(final String... args) throws Exception {
79
80 final HttpHost[] h2servers = new HttpHost[]{
81 new HttpHost("http", "localhost", 8080),
82 new HttpHost("http", "localhost", 8081)
83 };
84
85 final HttpHost httpbin = new HttpHost("http", "localhost", 8082);
86
87 final H2CompatibilityTest test = new H2CompatibilityTest();
88 try {
89 test.start();
90 for (final HttpHost h2server : h2servers) {
91 test.executeH2(h2server);
92 }
93 test.executeHttpBin(httpbin);
94 } finally {
95 test.shutdown();
96 }
97 }
98
99 H2CompatibilityTest() {
100 this.client = H2RequesterBootstrap.bootstrap()
101 .setIOReactorConfig(IOReactorConfig.custom()
102 .setSoTimeout(TIMEOUT)
103 .build())
104 .setH2Config(H2Config.custom()
105 .setPushEnabled(true)
106 .build())
107 .setStreamListener(LoggingHttp1StreamListener.INSTANCE_CLIENT)
108 .setStreamListener(LoggingH2StreamListener.INSTANCE)
109 .setConnPoolListener(LoggingConnPoolListener.INSTANCE)
110 .setIOSessionDecorator(LoggingIOSessionDecorator.INSTANCE)
111 .setExceptionCallback(LoggingExceptionCallback.INSTANCE)
112 .setIOSessionListener(LoggingIOSessionListener.INSTANCE)
113 .create();
114 }
115
116 void start() {
117 client.start();
118 }
119
120 void shutdown() {
121 client.close(CloseMode.GRACEFUL);
122 }
123
124 private static final Timeout TIMEOUT = Timeout.ofSeconds(5);
125
126 void executeH2(final HttpHost target) throws Exception {
127 {
128 System.out.println("*** HTTP/2 simple request execution ***");
129 final Future<AsyncClientEndpoint> connectFuture = client.connect(target, TIMEOUT, HttpVersionPolicy.FORCE_HTTP_2, null);
130 try {
131 final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
132
133 final CountDownLatch countDownLatch = new CountDownLatch(1);
134 final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/status.html");
135 endpoint.execute(
136 new BasicRequestProducer(httpget, null),
137 new BasicResponseConsumer<>(new StringAsyncEntityConsumer()),
138 new FutureCallback<Message<HttpResponse, String>>() {
139
140 @Override
141 public void completed(final Message<HttpResponse, String> responseMessage) {
142 final HttpResponse response = responseMessage.getHead();
143 final int code = response.getCode();
144 if (code == HttpStatus.SC_OK) {
145 logResult(TestResult.OK, target, httpget, response,
146 Objects.toString(response.getFirstHeader("server")));
147 } else {
148 logResult(TestResult.NOK, target, httpget, response, "(status " + code + ")");
149 }
150 countDownLatch.countDown();
151 }
152
153 @Override
154 public void failed(final Exception ex) {
155 logResult(TestResult.NOK, target, httpget, null, "(" + ex.getMessage() + ")");
156 countDownLatch.countDown();
157 }
158
159 @Override
160 public void cancelled() {
161 logResult(TestResult.NOK, target, httpget, null, "(cancelled)");
162 countDownLatch.countDown();
163 }
164
165 });
166 if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
167 logResult(TestResult.NOK, target, null, null, "(single request execution failed to complete in time)");
168 }
169 } catch (final ExecutionException ex) {
170 final Throwable cause = ex.getCause();
171 logResult(TestResult.NOK, target, null, null, "(" + cause.getMessage() + ")");
172 } catch (final TimeoutException ex) {
173 logResult(TestResult.NOK, target, null, null, "(time out)");
174 }
175 }
176 {
177 System.out.println("*** HTTP/2 multiplexed request execution ***");
178 final Future<AsyncClientEndpoint> connectFuture = client.connect(target, TIMEOUT, HttpVersionPolicy.FORCE_HTTP_2, null);
179 try {
180 final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
181
182 final int reqCount = 20;
183 final CountDownLatch countDownLatch = new CountDownLatch(reqCount);
184 for (int i = 0; i < reqCount; i++) {
185 final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/status.html");
186 endpoint.execute(
187 new BasicRequestProducer(httpget, null),
188 new BasicResponseConsumer<>(new StringAsyncEntityConsumer()),
189 new FutureCallback<Message<HttpResponse, String>>() {
190
191 @Override
192 public void completed(final Message<HttpResponse, String> responseMessage) {
193 final HttpResponse response = responseMessage.getHead();
194 final int code = response.getCode();
195 if (code == HttpStatus.SC_OK) {
196 logResult(TestResult.OK, target, httpget, response,
197 "multiplexed / " + response.getFirstHeader("server"));
198 } else {
199 logResult(TestResult.NOK, target, httpget, response, "(status " + code + ")");
200 }
201 countDownLatch.countDown();
202 }
203
204 @Override
205 public void failed(final Exception ex) {
206 logResult(TestResult.NOK, target, httpget, null, "(" + ex.getMessage() + ")");
207 countDownLatch.countDown();
208 }
209
210 @Override
211 public void cancelled() {
212 logResult(TestResult.NOK, target, httpget, null, "(cancelled)");
213 countDownLatch.countDown();
214 }
215
216 });
217 }
218 if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
219 logResult(TestResult.NOK, target, null, null, "(multiplexed request execution failed to complete in time)");
220 }
221 } catch (final ExecutionException ex) {
222 final Throwable cause = ex.getCause();
223 logResult(TestResult.NOK, target, null, null, "(" + cause.getMessage() + ")");
224 } catch (final TimeoutException ex) {
225 logResult(TestResult.NOK, target, null, null, "(time out)");
226 }
227 }
228 {
229 System.out.println("*** HTTP/2 request execution with push ***");
230 final Future<AsyncClientEndpoint> connectFuture = client.connect(target, TIMEOUT, HttpVersionPolicy.FORCE_HTTP_2, null);
231 try {
232 final AsyncClientEndpoint endpoint = connectFuture.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
233
234 final CountDownLatch countDownLatch = new CountDownLatch(5);
235 final HttpRequest httpget = new BasicHttpRequest(Method.GET, target, "/index.html");
236 final Future<Message<HttpResponse, String>> future = endpoint.execute(
237 new BasicRequestProducer(httpget, null),
238 new BasicResponseConsumer<>(new StringAsyncEntityConsumer()),
239 (request, context) -> new AbstractAsyncPushHandler<Message<HttpResponse, Void>>(
240 new BasicResponseConsumer<>(new DiscardingEntityConsumer<>())) {
241
242 @Override
243 protected void handleResponse(
244 final HttpRequest promise,
245 final Message<HttpResponse, Void> responseMessage) throws IOException, HttpException {
246 final HttpResponse response = responseMessage.getHead();
247 logResult(TestResult.OK, target, promise, response,
248 "pushed / " + response.getFirstHeader("server"));
249 countDownLatch.countDown();
250 }
251
252 @Override
253 protected void handleError(
254 final HttpRequest promise,
255 final Exception cause) {
256 logResult(TestResult.NOK, target, promise, null, "(" + cause.getMessage() + ")");
257 countDownLatch.countDown();
258 }
259 },
260 null,
261 null);
262 final Message<HttpResponse, String> message = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
263 final HttpResponse response = message.getHead();
264 final int code = response.getCode();
265 if (code == HttpStatus.SC_OK) {
266 logResult(TestResult.OK, target, httpget, response,
267 Objects.toString(response.getFirstHeader("server")));
268 if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
269 logResult(TestResult.NOK, target, null, null, "Push messages not received");
270 }
271 } else {
272 logResult(TestResult.NOK, target, httpget, response, "(status " + code + ")");
273 }
274 } catch (final ExecutionException ex) {
275 final Throwable cause = ex.getCause();
276 logResult(TestResult.NOK, target, null, null, "(" + cause.getMessage() + ")");
277 } catch (final TimeoutException ex) {
278 logResult(TestResult.NOK, target, null, null, "(time out)");
279 }
280 }
281 }
282
283 void executeHttpBin(final HttpHost target) throws Exception {
284 {
285 System.out.println("*** httpbin.org HTTP/1.1 simple request execution ***");
286
287 final List<Message<HttpRequest, AsyncEntityProducer>> requestMessages = Arrays.asList(
288 new Message<>(new BasicHttpRequest(Method.GET, target, "/headers")),
289 new Message<>(
290 new BasicHttpRequest(Method.POST, target, "/anything"),
291 new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)),
292 new Message<>(
293 new BasicHttpRequest(Method.PUT, target, "/anything"),
294 new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)),
295 new Message<>(new BasicHttpRequest(Method.GET, target, "/drip")),
296 new Message<>(new BasicHttpRequest(Method.GET, target, "/bytes/20000")),
297 new Message<>(new BasicHttpRequest(Method.GET, target, "/delay/2")),
298 new Message<>(
299 new BasicHttpRequest(Method.POST, target, "/delay/2"),
300 new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)),
301 new Message<>(
302 new BasicHttpRequest(Method.PUT, target, "/delay/2"),
303 new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN))
304 );
305
306 for (final Message<HttpRequest, AsyncEntityProducer> message : requestMessages) {
307 final CountDownLatch countDownLatch = new CountDownLatch(1);
308 final HttpRequest request = message.getHead();
309 final AsyncEntityProducer entityProducer = message.getBody();
310 client.execute(
311 new BasicRequestProducer(request, entityProducer),
312 new BasicResponseConsumer<>(new StringAsyncEntityConsumer(CharCodingConfig.custom()
313 .setCharset(StandardCharsets.US_ASCII)
314 .setMalformedInputAction(CodingErrorAction.IGNORE)
315 .setUnmappableInputAction(CodingErrorAction.REPLACE)
316 .build())),
317 TIMEOUT,
318 new FutureCallback<Message<HttpResponse, String>>() {
319
320 @Override
321 public void completed(final Message<HttpResponse, String> responseMessage) {
322 final HttpResponse response = responseMessage.getHead();
323 final int code = response.getCode();
324 if (code == HttpStatus.SC_OK) {
325 logResult(TestResult.OK, target, request, response,
326 Objects.toString(response.getFirstHeader("server")));
327 } else {
328 logResult(TestResult.NOK, target, request, response, "(status " + code + ")");
329 }
330 countDownLatch.countDown();
331 }
332
333 @Override
334 public void failed(final Exception ex) {
335 logResult(TestResult.NOK, target, request, null, "(" + ex.getMessage() + ")");
336 countDownLatch.countDown();
337 }
338
339 @Override
340 public void cancelled() {
341 logResult(TestResult.NOK, target, request, null, "(cancelled)");
342 countDownLatch.countDown();
343 }
344 });
345 if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
346 logResult(TestResult.NOK, target, null, null, "(httpbin.org tests failed to complete in time)");
347 }
348 }
349 }
350 {
351 System.out.println("*** httpbin.org HTTP/1.1 pipelined request execution ***");
352
353 final Future<AsyncClientEndpoint> connectFuture = client.connect(target, TIMEOUT);
354 final AsyncClientEndpoint streamEndpoint = connectFuture.get();
355
356 final int n = 10;
357 final CountDownLatch countDownLatch = new CountDownLatch(n);
358 for (int i = 0; i < n; i++) {
359
360 final HttpRequest request;
361 final AsyncEntityProducer entityProducer;
362 if (i % 2 == 0) {
363 request = new BasicHttpRequest(Method.GET, target, "/headers");
364 entityProducer = null;
365 } else {
366 request = new BasicHttpRequest(Method.POST, target, "/anything");
367 entityProducer = new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN);
368 }
369
370 streamEndpoint.execute(
371 new BasicRequestProducer(request, entityProducer),
372 new BasicResponseConsumer<>(new StringAsyncEntityConsumer(CharCodingConfig.custom()
373 .setCharset(StandardCharsets.US_ASCII)
374 .setMalformedInputAction(CodingErrorAction.IGNORE)
375 .setUnmappableInputAction(CodingErrorAction.REPLACE)
376 .build())),
377 new FutureCallback<Message<HttpResponse, String>>() {
378
379 @Override
380 public void completed(final Message<HttpResponse, String> responseMessage) {
381 final HttpResponse response = responseMessage.getHead();
382 final int code = response.getCode();
383 if (code == HttpStatus.SC_OK) {
384 logResult(TestResult.OK, target, request, response,
385 "pipelined / " + response.getFirstHeader("server"));
386 } else {
387 logResult(TestResult.NOK, target, request, response, "(status " + code + ")");
388 }
389 countDownLatch.countDown();
390 }
391
392 @Override
393 public void failed(final Exception ex) {
394 logResult(TestResult.NOK, target, request, null, "(" + ex.getMessage() + ")");
395 countDownLatch.countDown();
396 }
397
398 @Override
399 public void cancelled() {
400 logResult(TestResult.NOK, target, request, null, "(cancelled)");
401 countDownLatch.countDown();
402 }
403 });
404 }
405 if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
406 logResult(TestResult.NOK, target, null, null, "(httpbin.org tests failed to complete in time)");
407 }
408 }
409 }
410
411 enum TestResult {OK, NOK}
412
413 private void logResult(
414 final TestResult result,
415 final HttpHost httpHost,
416 final HttpRequest request,
417 final HttpResponse response,
418 final String message) {
419 final StringBuilder buf = new StringBuilder();
420 buf.append(result);
421 if (buf.length() == 2) {
422 buf.append(" ");
423 }
424 buf.append(": ").append(httpHost).append(" ");
425 if (response != null) {
426 buf.append(response.getVersion()).append(" ");
427 }
428 if (request != null) {
429 buf.append(request.getMethod()).append(" ").append(request.getRequestUri());
430 }
431 if (message != null && !TextUtils.isBlank(message)) {
432 buf.append(" -> ").append(message);
433 }
434 System.out.println(buf);
435 }
436
437 }