View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  package org.apache.hc.client5.testing.sync;
28  
29  import java.io.ByteArrayInputStream;
30  import java.io.IOException;
31  import java.net.URI;
32  import java.util.Random;
33  import java.util.concurrent.Executors;
34  import java.util.concurrent.ScheduledExecutorService;
35  import java.util.concurrent.TimeUnit;
36  import java.util.function.Consumer;
37  
38  import org.apache.hc.client5.http.HttpRequestRetryStrategy;
39  import org.apache.hc.client5.http.classic.methods.HttpGet;
40  import org.apache.hc.client5.http.classic.methods.HttpPost;
41  import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
42  import org.apache.hc.client5.http.impl.classic.HttpClientBuilder;
43  import org.apache.hc.client5.http.protocol.HttpClientContext;
44  import org.apache.hc.client5.http.protocol.RedirectLocations;
45  import org.apache.hc.client5.http.utils.URIUtils;
46  import org.apache.hc.client5.testing.sync.extension.TestClientResources;
47  import org.apache.hc.core5.http.ClassicHttpRequest;
48  import org.apache.hc.core5.http.ClassicHttpResponse;
49  import org.apache.hc.core5.http.Header;
50  import org.apache.hc.core5.http.HttpException;
51  import org.apache.hc.core5.http.HttpHost;
52  import org.apache.hc.core5.http.HttpRequest;
53  import org.apache.hc.core5.http.HttpRequestInterceptor;
54  import org.apache.hc.core5.http.HttpResponse;
55  import org.apache.hc.core5.http.HttpStatus;
56  import org.apache.hc.core5.http.URIScheme;
57  import org.apache.hc.core5.http.impl.io.HttpRequestExecutor;
58  import org.apache.hc.core5.http.io.HttpClientConnection;
59  import org.apache.hc.core5.http.io.HttpRequestHandler;
60  import org.apache.hc.core5.http.io.HttpResponseInformationCallback;
61  import org.apache.hc.core5.http.io.entity.EntityUtils;
62  import org.apache.hc.core5.http.io.entity.InputStreamEntity;
63  import org.apache.hc.core5.http.io.entity.StringEntity;
64  import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
65  import org.apache.hc.core5.http.protocol.HttpContext;
66  import org.apache.hc.core5.net.URIBuilder;
67  import org.apache.hc.core5.testing.classic.ClassicTestServer;
68  import org.apache.hc.core5.util.TimeValue;
69  import org.apache.hc.core5.util.Timeout;
70  import org.junit.jupiter.api.Assertions;
71  import org.junit.jupiter.api.Disabled;
72  import org.junit.jupiter.api.Test;
73  import org.junit.jupiter.api.extension.RegisterExtension;
74  
75  /**
76   * Client protocol handling tests.
77   */
78  public abstract class TestClientRequestExecution {
79  
80      public static final Timeout TIMEOUT = Timeout.ofMinutes(1);
81  
82      @RegisterExtension
83      private TestClientResources testResources;
84  
85      protected TestClientRequestExecution(final URIScheme scheme) {
86          this.testResources = new TestClientResources(scheme, TIMEOUT);
87      }
88  
89      public URIScheme scheme() {
90          return testResources.scheme();
91      }
92  
93      public ClassicTestServer startServer() throws IOException {
94          return testResources.startServer(null, null, null);
95      }
96  
97      public CloseableHttpClient startClient(final Consumer<HttpClientBuilder> clientCustomizer) throws Exception {
98          return testResources.startClient(clientCustomizer);
99      }
100 
101     public CloseableHttpClient startClient() throws Exception {
102         return testResources.startClient(builder -> {});
103     }
104 
105     public HttpHost targetHost() {
106         return testResources.targetHost();
107     }
108 
109     private static class SimpleService implements HttpRequestHandler {
110 
111         public SimpleService() {
112             super();
113         }
114 
115         @Override
116         public void handle(
117                 final ClassicHttpRequest request,
118                 final ClassicHttpResponse response,
119                 final HttpContext context) throws HttpException, IOException {
120             response.setCode(HttpStatus.SC_OK);
121             final StringEntity entity = new StringEntity("Whatever");
122             response.setEntity(entity);
123         }
124     }
125 
126     private static class FaultyHttpRequestExecutor extends HttpRequestExecutor {
127 
128         private static final String MARKER = "marker";
129 
130         private final String failureMsg;
131 
132         public FaultyHttpRequestExecutor(final String failureMsg) {
133             this.failureMsg = failureMsg;
134         }
135 
136         @Override
137         public ClassicHttpResponse execute(
138                 final ClassicHttpRequest request,
139                 final HttpClientConnection conn,
140                 final HttpContext context) throws IOException, HttpException {
141             return execute(request, conn, null, context);
142         }
143 
144         @Override
145         public ClassicHttpResponse execute(
146                 final ClassicHttpRequest request,
147                 final HttpClientConnection conn,
148                 final HttpResponseInformationCallback informationCallback,
149                 final HttpContext context) throws IOException, HttpException {
150 
151             final ClassicHttpResponse response = super.execute(request, conn, informationCallback, context);
152             final Object marker = context.getAttribute(MARKER);
153             if (marker == null) {
154                 context.setAttribute(MARKER, Boolean.TRUE);
155                 throw new IOException(failureMsg);
156             }
157             return response;
158         }
159 
160     }
161 
162     @Test
163     public void testAutoGeneratedHeaders() throws Exception {
164         final ClassicTestServer server = startServer();
165         server.registerHandler("*", new SimpleService());
166         final HttpHost target = targetHost();
167 
168         final HttpRequestInterceptor interceptor = (request, entityDetails, context) -> request.addHeader("my-header", "stuff");
169 
170         final HttpRequestRetryStrategy requestRetryStrategy = new HttpRequestRetryStrategy() {
171 
172             @Override
173             public boolean retryRequest(
174                     final HttpRequest request,
175                     final IOException exception,
176                     final int executionCount,
177                     final HttpContext context) {
178                 return true;
179             }
180 
181             @Override
182             public boolean retryRequest(
183                     final HttpResponse response,
184                     final int executionCount,
185                     final HttpContext context) {
186                 return false;
187             }
188 
189             @Override
190             public TimeValue getRetryInterval(
191                     final HttpResponse response,
192                     final int executionCount,
193                     final HttpContext context) {
194                 return TimeValue.ofSeconds(1L);
195             }
196 
197         };
198 
199         final CloseableHttpClient client = startClient(builder -> builder
200                 .addRequestInterceptorFirst(interceptor)
201                 .setRequestExecutor(new FaultyHttpRequestExecutor("Oppsie"))
202                 .setRetryStrategy(requestRetryStrategy)
203         );
204 
205         final HttpClientContext context = HttpClientContext.create();
206 
207         final HttpGet httpget = new HttpGet("/");
208 
209         client.execute(target, httpget, context, response -> {
210             EntityUtils.consume(response.getEntity());
211             final HttpRequest reqWrapper = context.getRequest();
212 
213             Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
214 
215             final Header[] myheaders = reqWrapper.getHeaders("my-header");
216             Assertions.assertNotNull(myheaders);
217             Assertions.assertEquals(1, myheaders.length);
218             return null;
219         });
220     }
221 
222     @Test
223     public void testNonRepeatableEntity() throws Exception {
224         final ClassicTestServer server = startServer();
225         server.registerHandler("*", new SimpleService());
226         final HttpHost target = targetHost();
227 
228         final HttpRequestRetryStrategy requestRetryStrategy = new HttpRequestRetryStrategy() {
229 
230             @Override
231             public boolean retryRequest(
232                     final HttpRequest request,
233                     final IOException exception,
234                     final int executionCount,
235                     final HttpContext context) {
236                 return true;
237             }
238 
239             @Override
240             public boolean retryRequest(
241                     final HttpResponse response,
242                     final int executionCount,
243                     final HttpContext context) {
244                 return false;
245             }
246 
247             @Override
248             public TimeValue getRetryInterval(
249                     final HttpResponse response,
250                     final int executionCount,
251                     final HttpContext context) {
252                 return TimeValue.ofSeconds(1L);
253             }
254 
255         };
256 
257         final CloseableHttpClient client = startClient(builder -> builder
258                 .setRequestExecutor(new FaultyHttpRequestExecutor("a message showing that this failed"))
259                 .setRetryStrategy(requestRetryStrategy)
260         );
261 
262         final HttpClientContext context = HttpClientContext.create();
263 
264         final HttpPost httppost = new HttpPost("/");
265         httppost.setEntity(new InputStreamEntity(
266                 new ByteArrayInputStream(
267                         new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 } ),
268                         -1, null));
269         Assertions.assertThrows(IOException.class, () ->
270                 client.execute(target, httppost, context, response -> null));
271     }
272 
273     @Test
274     public void testNonCompliantURI() throws Exception {
275         final ClassicTestServer server = startServer();
276         server.registerHandler("*", new SimpleService());
277         final HttpHost target = targetHost();
278 
279         final CloseableHttpClient client = startClient();
280 
281         final HttpClientContext context = HttpClientContext.create();
282         final ClassicHttpRequest request = new BasicClassicHttpRequest("GET", "{{|boom|}}");
283         client.execute(target, request, context, response -> {
284             Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
285             EntityUtils.consume(response.getEntity());
286             return null;
287         });
288 
289         final HttpRequest reqWrapper = context.getRequest();
290 
291         Assertions.assertEquals("{{|boom|}}", reqWrapper.getRequestUri());
292     }
293 
294     @Test
295     public void testRelativeRequestURIWithFragment() throws Exception {
296         final ClassicTestServer server = startServer();
297         server.registerHandler("*", new SimpleService());
298         final HttpHost target = targetHost();
299 
300         final CloseableHttpClient client = startClient();
301 
302         final HttpGet httpget = new HttpGet("/stuff#blahblah");
303         final HttpClientContext context = HttpClientContext.create();
304 
305         client.execute(target, httpget, context, response -> {
306             Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
307             EntityUtils.consume(response.getEntity());
308             return null;
309         });
310 
311         final HttpRequest request = context.getRequest();
312         Assertions.assertEquals("/stuff", request.getRequestUri());
313     }
314 
315     @Test
316     public void testAbsoluteRequestURIWithFragment() throws Exception {
317         final ClassicTestServer server = startServer();
318         server.registerHandler("*", new SimpleService());
319         final HttpHost target = targetHost();
320 
321         final CloseableHttpClient client = startClient();
322 
323         final URI uri = new URIBuilder()
324             .setHost(target.getHostName())
325             .setPort(target.getPort())
326             .setScheme(target.getSchemeName())
327             .setPath("/stuff")
328             .setFragment("blahblah")
329             .build();
330 
331         final HttpGet httpget = new HttpGet(uri);
332         final HttpClientContext context = HttpClientContext.create();
333 
334         client.execute(httpget, context, response -> {
335             Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
336             return null;
337         });
338 
339         final HttpRequest request = context.getRequest();
340         Assertions.assertEquals("/stuff", request.getRequestUri());
341 
342         final RedirectLocations redirectLocations = context.getRedirectLocations();
343         final URI location = URIUtils.resolve(uri, target, redirectLocations.getAll());
344         Assertions.assertEquals(uri, location);
345     }
346 
347     @Test @Disabled("Fails intermittently with GitHub Actions")
348     public void testRequestCancellation() throws Exception {
349         startServer();
350         final HttpHost target = targetHost();
351 
352         final CloseableHttpClient client = testResources.startClient(
353                 builder -> builder
354                         .setMaxConnPerRoute(1)
355                         .setMaxConnTotal(1),
356                 builder -> {});
357 
358         final ScheduledExecutorService executorService = Executors.newSingleThreadScheduledExecutor();
359         try {
360 
361             for (int i = 0; i < 20; i++) {
362                 final HttpGet httpget = new HttpGet("/random/1000");
363 
364                 executorService.schedule(httpget::cancel, 1, TimeUnit.MILLISECONDS);
365 
366                 try {
367                     client.execute(target, httpget, response -> {
368                         EntityUtils.consume(response.getEntity());
369                         return null;
370                     });
371 
372                 } catch (final Exception ignore) {
373                 }
374             }
375 
376             final Random rnd = new Random();
377             for (int i = 0; i < 20; i++) {
378                 final HttpGet httpget = new HttpGet("/random/1000");
379 
380                 executorService.schedule(httpget::cancel, rnd.nextInt(200), TimeUnit.MILLISECONDS);
381 
382                 try {
383                     client.execute(target, httpget, response -> {
384                         EntityUtils.consume(response.getEntity());
385                         return null;
386                     });
387                 } catch (final Exception ignore) {
388                 }
389 
390             }
391 
392             for (int i = 0; i < 5; i++) {
393                 final HttpGet httpget = new HttpGet("/random/1000");
394                 client.execute(target, httpget, response -> {
395                     EntityUtils.consume(response.getEntity());
396                     return null;
397                 });
398             }
399 
400         } finally {
401             executorService.shutdownNow();
402         }
403     }
404 
405 }