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