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() throws Exception {
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() throws Exception {
117 client.start();
118 }
119
120 void shutdown() throws Exception {
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<>(
289 new BasicHttpRequest(Method.GET, target, "/headers"),
290 null),
291 new Message<>(
292 new BasicHttpRequest(Method.POST, target, "/anything"),
293 new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)),
294 new Message<>(
295 new BasicHttpRequest(Method.PUT, target, "/anything"),
296 new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)),
297 new Message<>(
298 new BasicHttpRequest(Method.GET, target, "/drip"),
299 null),
300 new Message<>(
301 new BasicHttpRequest(Method.GET, target, "/bytes/20000"),
302 null),
303 new Message<>(
304 new BasicHttpRequest(Method.GET, target, "/delay/2"),
305 null),
306 new Message<>(
307 new BasicHttpRequest(Method.POST, target, "/delay/2"),
308 new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN)),
309 new Message<>(
310 new BasicHttpRequest(Method.PUT, target, "/delay/2"),
311 new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN))
312 );
313
314 for (final Message<HttpRequest, AsyncEntityProducer> message : requestMessages) {
315 final CountDownLatch countDownLatch = new CountDownLatch(1);
316 final HttpRequest request = message.getHead();
317 final AsyncEntityProducer entityProducer = message.getBody();
318 client.execute(
319 new BasicRequestProducer(request, entityProducer),
320 new BasicResponseConsumer<>(new StringAsyncEntityConsumer(CharCodingConfig.custom()
321 .setCharset(StandardCharsets.US_ASCII)
322 .setMalformedInputAction(CodingErrorAction.IGNORE)
323 .setUnmappableInputAction(CodingErrorAction.REPLACE)
324 .build())),
325 TIMEOUT,
326 new FutureCallback<Message<HttpResponse, String>>() {
327
328 @Override
329 public void completed(final Message<HttpResponse, String> responseMessage) {
330 final HttpResponse response = responseMessage.getHead();
331 final int code = response.getCode();
332 if (code == HttpStatus.SC_OK) {
333 logResult(TestResult.OK, target, request, response,
334 Objects.toString(response.getFirstHeader("server")));
335 } else {
336 logResult(TestResult.NOK, target, request, response, "(status " + code + ")");
337 }
338 countDownLatch.countDown();
339 }
340
341 @Override
342 public void failed(final Exception ex) {
343 logResult(TestResult.NOK, target, request, null, "(" + ex.getMessage() + ")");
344 countDownLatch.countDown();
345 }
346
347 @Override
348 public void cancelled() {
349 logResult(TestResult.NOK, target, request, null, "(cancelled)");
350 countDownLatch.countDown();
351 }
352 });
353 if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
354 logResult(TestResult.NOK, target, null, null, "(httpbin.org tests failed to complete in time)");
355 }
356 }
357 }
358 {
359 System.out.println("*** httpbin.org HTTP/1.1 pipelined request execution ***");
360
361 final Future<AsyncClientEndpoint> connectFuture = client.connect(target, TIMEOUT);
362 final AsyncClientEndpoint streamEndpoint = connectFuture.get();
363
364 final int n = 10;
365 final CountDownLatch countDownLatch = new CountDownLatch(n);
366 for (int i = 0; i < n; i++) {
367
368 final HttpRequest request;
369 final AsyncEntityProducer entityProducer;
370 if (i % 2 == 0) {
371 request = new BasicHttpRequest(Method.GET, target, "/headers");
372 entityProducer = null;
373 } else {
374 request = new BasicHttpRequest(Method.POST, target, "/anything");
375 entityProducer = new StringAsyncEntityProducer("some important message", ContentType.TEXT_PLAIN);
376 }
377
378 streamEndpoint.execute(
379 new BasicRequestProducer(request, entityProducer),
380 new BasicResponseConsumer<>(new StringAsyncEntityConsumer(CharCodingConfig.custom()
381 .setCharset(StandardCharsets.US_ASCII)
382 .setMalformedInputAction(CodingErrorAction.IGNORE)
383 .setUnmappableInputAction(CodingErrorAction.REPLACE)
384 .build())),
385 new FutureCallback<Message<HttpResponse, String>>() {
386
387 @Override
388 public void completed(final Message<HttpResponse, String> responseMessage) {
389 final HttpResponse response = responseMessage.getHead();
390 final int code = response.getCode();
391 if (code == HttpStatus.SC_OK) {
392 logResult(TestResult.OK, target, request, response,
393 "pipelined / " + response.getFirstHeader("server"));
394 } else {
395 logResult(TestResult.NOK, target, request, response, "(status " + code + ")");
396 }
397 countDownLatch.countDown();
398 }
399
400 @Override
401 public void failed(final Exception ex) {
402 logResult(TestResult.NOK, target, request, null, "(" + ex.getMessage() + ")");
403 countDownLatch.countDown();
404 }
405
406 @Override
407 public void cancelled() {
408 logResult(TestResult.NOK, target, request, null, "(cancelled)");
409 countDownLatch.countDown();
410 }
411 });
412 }
413 if (!countDownLatch.await(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit())) {
414 logResult(TestResult.NOK, target, null, null, "(httpbin.org tests failed to complete in time)");
415 }
416 }
417 }
418
419 enum TestResult {OK, NOK}
420
421 private void logResult(
422 final TestResult result,
423 final HttpHost httpHost,
424 final HttpRequest request,
425 final HttpResponse response,
426 final String message) {
427 final StringBuilder buf = new StringBuilder();
428 buf.append(result);
429 if (buf.length() == 2) {
430 buf.append(" ");
431 }
432 buf.append(": ").append(httpHost).append(" ");
433 if (response != null) {
434 buf.append(response.getVersion()).append(" ");
435 }
436 if (request != null) {
437 buf.append(request.getMethod()).append(" ").append(request.getRequestUri());
438 }
439 if (message != null && !TextUtils.isBlank(message)) {
440 buf.append(" -> ").append(message);
441 }
442 System.out.println(buf);
443 }
444
445 }