3232import java .util .List ;
3333import java .util .concurrent .CountDownLatch ;
3434import java .util .concurrent .TimeUnit ;
35+ import java .util .concurrent .atomic .AtomicBoolean ;
3536
3637import org .apache .hc .core5 .http .EntityDetails ;
3738import org .apache .hc .core5 .http .Header ;
3839import org .apache .hc .core5 .http .HttpConnection ;
3940import org .apache .hc .core5 .http .HttpException ;
41+ import org .apache .hc .core5 .http .HttpHost ;
4042import org .apache .hc .core5 .http .HttpResponse ;
4143import org .apache .hc .core5 .http .impl .bootstrap .HttpAsyncRequester ;
44+ import org .apache .hc .core5 .http .nio .AsyncClientEndpoint ;
4245import org .apache .hc .core5 .http .nio .AsyncClientExchangeHandler ;
4346import org .apache .hc .core5 .http .nio .AsyncRequestProducer ;
4447import org .apache .hc .core5 .http .nio .CapacityChannel ;
5053import org .apache .hc .core5 .http .protocol .HttpContext ;
5154import org .apache .hc .core5 .http .protocol .HttpCoreContext ;
5255import org .apache .hc .core5 .http2 .H2StreamResetException ;
56+ import org .apache .hc .core5 .http2 .H2StreamTimeoutException ;
5357import org .apache .hc .core5 .http2 .HttpVersionPolicy ;
5458import org .apache .hc .core5 .http2 .config .H2Config ;
5559import org .apache .hc .core5 .http2 .frame .RawFrame ;
6064import org .apache .hc .core5 .util .Timeout ;
6165
6266/**
63- * Example of an HTTP/2 client where a "slow" request gets aborted when the
64- * underlying HTTP/2 connection times out due to inactivity (socket timeout) .
67+ * Example of an HTTP/2 client where a "slow" request gets aborted by a
68+ * per-stream idle timeout enforced by the HTTP/2 multiplexer .
6569 * <p>
66- * The client opens a single HTTP/2 connection to {@code nghttp2.org} and
67- * executes two concurrent requests:
68- * <ul>
69- * <li>a "fast" request ({@code /httpbin/ip}), which completes before
70- * the connection idle timeout, and</li>
71- * <li>a "slow" request ({@code /httpbin/delay/5}), which keeps the
72- * connection idle long enough for the I/O reactor to trigger a timeout
73- * and close the HTTP/2 connection.</li>
74- * </ul>
75- * <p>
76- * When the reactor closes the connection due to inactivity, all active
77- * streams fail with {@link H2StreamResetException} reporting
78- * {@code "Timeout due to inactivity (...)"}. The already completed stream
79- * is not affected.
70+ * The connection socket timeout is set to 2 seconds and is used as the initial / default
71+ * per-stream idle timeout value. The example keeps the connection active by sending
72+ * small "keep-alive" requests on separate streams, so the connection itself does not time out.
73+ * The "slow" stream remains idle long enough to exceed the per-stream idle timeout and gets reset.
8074 *
8175 * @since 5.4
8276 */
@@ -85,8 +79,6 @@ public class H2StreamTimeoutClientExample {
8579 public static void main (final String [] args ) throws Exception {
8680
8781 final IOReactorConfig ioReactorConfig = IOReactorConfig .custom ()
88- // Connection-level inactivity timeout: keep it short so that
89- // /httpbin/delay/5 reliably triggers it.
9082 .setSoTimeout (2 , TimeUnit .SECONDS )
9183 .build ();
9284
@@ -167,52 +159,150 @@ public void onOutputFlowControl(
167159
168160 requester .start ();
169161
170- final URI fastUri = new URI ("https://nghttp2.org/httpbin/ip" );
171- final URI slowUri = new URI ("https://nghttp2.org/httpbin/delay/5" );
162+ final HttpHost target = new HttpHost ("https" , "nghttp2.org" , 443 );
163+ final AsyncClientEndpoint endpoint = requester .connect (target , Timeout .ofSeconds (10 )).get ();
164+
165+ try {
166+ final URI keepAliveUri = new URI ("https://nghttp2.org/httpbin/ip" );
167+ final URI slowUri = new URI ("https://nghttp2.org/httpbin/delay/5" );
168+
169+ final CountDownLatch latch = new CountDownLatch (2 );
170+ final AtomicBoolean stop = new AtomicBoolean (false );
171+
172+ // Keep the connection active with short requests on new streams,
173+ // so the connection does NOT hit "Timeout due to inactivity".
174+ final Thread keepAliveThread = new Thread (() -> {
175+ try {
176+ while (!stop .get ()) {
177+ executeKeepAliveOnce (endpoint , keepAliveUri );
178+ Thread .sleep (500 );
179+ }
180+ } catch (final Exception ignore ) {
181+ } finally {
182+ latch .countDown ();
183+ }
184+ });
185+ keepAliveThread .setDaemon (true );
186+ keepAliveThread .start ();
187+
188+ // Slow stream: should be reset by per-stream idle timeout while the connection stays active.
189+ executeWithLogging (
190+ endpoint ,
191+ slowUri ,
192+ "[slow]" ,
193+ latch ,
194+ stop );
195+
196+ latch .await (30 , TimeUnit .SECONDS );
197+
198+ } finally {
199+ endpoint .releaseAndReuse ();
200+ System .out .println ("Shutting down I/O reactor" );
201+ requester .initiateShutdown ();
202+ }
203+ }
204+
205+ private static void executeKeepAliveOnce (
206+ final AsyncClientEndpoint endpoint ,
207+ final URI requestUri ) throws InterruptedException {
208+
209+ final AsyncRequestProducer requestProducer = AsyncRequestBuilder .get (requestUri ).build ();
210+ final BasicResponseConsumer <String > responseConsumer = new BasicResponseConsumer <>(
211+ new StringAsyncEntityConsumer ());
212+
213+ final CountDownLatch done = new CountDownLatch (1 );
214+
215+ endpoint .execute (new AsyncClientExchangeHandler () {
216+
217+ @ Override
218+ public void releaseResources () {
219+ requestProducer .releaseResources ();
220+ responseConsumer .releaseResources ();
221+ done .countDown ();
222+ }
223+
224+ @ Override
225+ public void cancel () {
226+ done .countDown ();
227+ }
228+
229+ @ Override
230+ public void failed (final Exception cause ) {
231+ done .countDown ();
232+ }
233+
234+ @ Override
235+ public void produceRequest (
236+ final RequestChannel channel ,
237+ final HttpContext httpContext ) throws HttpException , IOException {
238+ requestProducer .sendRequest (channel , httpContext );
239+ }
240+
241+ @ Override
242+ public int available () {
243+ return requestProducer .available ();
244+ }
245+
246+ @ Override
247+ public void produce (final DataStreamChannel channel ) throws IOException {
248+ requestProducer .produce (channel );
249+ }
250+
251+ @ Override
252+ public void consumeInformation (
253+ final HttpResponse response ,
254+ final HttpContext httpContext ) throws HttpException , IOException {
255+ // No-op
256+ }
257+
258+ @ Override
259+ public void consumeResponse (
260+ final HttpResponse response ,
261+ final EntityDetails entityDetails ,
262+ final HttpContext httpContext ) throws HttpException , IOException {
263+ responseConsumer .consumeResponse (response , entityDetails , httpContext , null );
264+ }
172265
173- final CountDownLatch latch = new CountDownLatch (2 );
266+ @ Override
267+ public void updateCapacity (final CapacityChannel capacityChannel ) throws IOException {
268+ responseConsumer .updateCapacity (capacityChannel );
269+ }
174270
175- // --- Fast stream: expected to succeed
176- executeWithLogging (
177- requester ,
178- fastUri ,
179- "[fast]" ,
180- latch ,
181- false );
271+ @ Override
272+ public void consume (final ByteBuffer src ) throws IOException {
273+ responseConsumer .consume (src );
274+ }
182275
183- // --- Slow stream: /delay/5 sleeps 5 seconds and should exceed
184- // the 2-second connection idle timeout, resulting in a reset.
185- executeWithLogging (
186- requester ,
187- slowUri ,
188- "[slow]" ,
189- latch ,
190- true );
276+ @ Override
277+ public void streamEnd (final List <? extends Header > trailers )
278+ throws HttpException , IOException {
279+ responseConsumer .streamEnd (trailers );
280+ }
191281
192- latch . await ( );
282+ }, HttpCoreContext . create () );
193283
194- System .out .println ("Shutting down I/O reactor" );
195- requester .initiateShutdown ();
284+ done .await (5 , TimeUnit .SECONDS );
196285 }
197286
198287 private static void executeWithLogging (
199- final HttpAsyncRequester requester ,
288+ final AsyncClientEndpoint endpoint ,
200289 final URI requestUri ,
201290 final String label ,
202291 final CountDownLatch latch ,
203- final boolean expectTimeout ) {
292+ final AtomicBoolean stop ) {
204293
205294 final AsyncRequestProducer requestProducer = AsyncRequestBuilder .get (requestUri )
206295 .build ();
207296 final BasicResponseConsumer <String > responseConsumer = new BasicResponseConsumer <>(
208297 new StringAsyncEntityConsumer ());
209298
210- requester .execute (new AsyncClientExchangeHandler () {
299+ endpoint .execute (new AsyncClientExchangeHandler () {
211300
212301 @ Override
213302 public void releaseResources () {
214303 requestProducer .releaseResources ();
215304 responseConsumer .releaseResources ();
305+ stop .set (true );
216306 latch .countDown ();
217307 }
218308
@@ -223,11 +313,12 @@ public void cancel() {
223313
224314 @ Override
225315 public void failed (final Exception cause ) {
226- if (expectTimeout && cause instanceof H2StreamResetException ) {
227- final H2StreamResetException ex = (H2StreamResetException ) cause ;
228- System .out .println (label + " expected timeout reset: "
229- + requestUri
230- + " -> " + ex );
316+ if (cause instanceof H2StreamTimeoutException ) {
317+ System .out .println (label + " expected per-stream timeout reset: "
318+ + requestUri + " -> " + cause );
319+ } else if (cause instanceof H2StreamResetException ) {
320+ System .out .println (label + " stream reset: "
321+ + requestUri + " -> " + cause );
231322 } else {
232323 System .out .println (label + " failure: "
233324 + requestUri + " -> " + cause );
@@ -265,13 +356,8 @@ public void consumeResponse(
265356 final HttpResponse response ,
266357 final EntityDetails entityDetails ,
267358 final HttpContext httpContext ) throws HttpException , IOException {
268- if (expectTimeout ) {
269- System .out .println (label + " UNEXPECTED success: "
270- + requestUri + " -> " + response .getCode ());
271- } else {
272- System .out .println (label + " response: "
273- + requestUri + " -> " + response .getCode ());
274- }
359+ System .out .println (label + " response: "
360+ + requestUri + " -> " + response .getCode ());
275361 responseConsumer .consumeResponse (response , entityDetails , httpContext , null );
276362 }
277363
@@ -289,12 +375,9 @@ public void consume(final ByteBuffer src) throws IOException {
289375 public void streamEnd (final List <? extends Header > trailers )
290376 throws HttpException , IOException {
291377 responseConsumer .streamEnd (trailers );
292- if (!expectTimeout ) {
293- System .out .println (label + " body completed for " + requestUri );
294- }
295378 }
296379
297- }, Timeout . ofSeconds ( 10 ), HttpCoreContext .create ());
380+ }, HttpCoreContext .create ());
298381 }
299382
300383}
0 commit comments