2626 */
2727package org .apache .hc .client5 .http .examples ;
2828
29+ import java .net .URI ;
30+ import java .net .URISyntaxException ;
31+ import java .util .ArrayList ;
32+ import java .util .List ;
2933import java .util .concurrent .Future ;
3034
3135import org .apache .hc .client5 .http .Rfc6724AddressSelectingDnsResolver ;
3741import org .apache .hc .client5 .http .async .methods .SimpleResponseConsumer ;
3842import org .apache .hc .client5 .http .config .ConnectionConfig ;
3943import org .apache .hc .client5 .http .config .ProtocolFamilyPreference ;
44+ import org .apache .hc .client5 .http .config .RequestConfig ;
4045import org .apache .hc .client5 .http .impl .async .CloseableHttpAsyncClient ;
4146import org .apache .hc .client5 .http .impl .async .HttpAsyncClients ;
4247import org .apache .hc .client5 .http .impl .nio .PoolingAsyncClientConnectionManager ;
4954import org .apache .hc .core5 .http .nio .ssl .TlsStrategy ;
5055import org .apache .hc .core5 .io .CloseMode ;
5156import org .apache .hc .core5 .util .TimeValue ;
57+ import org .apache .hc .core5 .util .Timeout ;
5258
5359/**
54- * <h2>Example: RFC 6724 DNS ordering + Happy Eyeballs (with logs )</h2>
60+ * <h2>Example: RFC 6724 DNS ordering + Happy Eyeballs (with console output )</h2>
5561 *
56- * <p>This example shows how to:
62+ * <p>This example shows how to:</p>
5763 * <ul>
5864 * <li>Wrap the system DNS resolver with {@link org.apache.hc.client5.http.Rfc6724AddressSelectingDnsResolver}
59- * to apply <b>RFC 6724</b> destination address selection (v6/v4 ordering).</li>
65+ * to apply <b>RFC 6724</b> destination address selection (IPv6/IPv4 ordering).</li>
6066 * <li>Use {@link org.apache.hc.client5.http.config.ConnectionConfig} to enable <b>Happy Eyeballs v2</b> pacing
6167 * and set a <b>protocol family preference</b> (e.g., {@code IPV4_ONLY}, {@code IPV6_ONLY}, {@code PREFER_IPV6},
62- * {@code INTERLEAVE}).</li>
63- * <li>Turn on DEBUG logs that make the resolver’s ordering and the connection layer’s decisions observable .</li>
68+ * {@code PREFER_IPV4}, {@code INTERLEAVE}).</li>
69+ * <li>Control the connect timeout so demos don’t stall on slow/broken networks .</li>
6470 * </ul>
65- * </p>
6671 *
67- * <p><b>Logging categories</b> (enable at DEBUG):</p>
68- * <ul>
69- * <li>{@code org.apache.hc.client5.http.Rfc6724AddressSelectingDnsResolver} — resolver decisions
70- * (delegate results, RFC 6724 attributes, output order, family bias).</li>
71- * <li>{@code org.apache.hc.client5.http.impl.nio.MultihomeIOSessionRequester} — Happy Eyeballs scheduling and the winning attempt.</li>
72- * <li>(optional) {@code org.apache.hc.client5.http.impl.nio}, {@code org.apache.hc.client5.http.impl.async},
73- * {@code org.apache.hc.core5} — connection pool, protocol exec, TLS, and I/O transport.</li>
74- * </ul>
75- *
76- * <p><b>Log4j2 quick config</b> (trimmed):</p>
77- * <pre>{@code
78- * <Configuration status="WARN">
79- * <Appenders>
80- * <Console name="Console" target="SYSTEM_OUT">
81- * <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss,SSS} %-5level [%t][%c] %msg%n"/>
82- * </Console>
83- * </Appenders>
84- * <Loggers>
85- * <Logger name="org.apache.hc.client5.http.Rfc6724AddressSelectingDnsResolver" level="debug" additivity="false">
86- * <AppenderRef ref="Console"/>
87- * </Logger>
88- * <Logger name="org.apache.hc.client5.http.impl.nio.MultihomeIOSessionRequester" level="debug" additivity="false">
89- * <AppenderRef ref="Console"/>
90- * </Logger>
91- * <!-- Optional plumbing -->
92- * <Logger name="org.apache.hc.client5.http.impl.nio" level="debug"/>
93- * <Logger name="org.apache.hc.client5.http.impl.async" level="debug"/>
94- * <Logger name="org.apache.hc.core5" level="debug"/>
95- * <Root level="info"><AppenderRef ref="Console"/></Root>
96- * </Loggers>
97- * </Configuration>
98- * }</pre>
72+ * <h3>How to run with the example runner</h3>
73+ * <pre>
74+ * # Default (no args): hits http://ipv6-test.com/ and https://ipv6-test.com/
75+ * ./run-example.sh AsyncClientHappyEyeballs
9976 *
100- * <p><b>What to expect in the logs</b> (trimmed, dual-stack target):</p>
77+ * # Pass one URI (runner supports command-line args)
78+ * ./run-example.sh AsyncClientHappyEyeballs http://neverssl.com/
10179 *
102- * <p><u>A) {@code IPV4_ONLY}</u> — v6 is present but is filtered out early</p>
103- * <pre>{@code
104- * ... Rfc6724Resolver resolving host 'ipv6-test.com' via delegate org.apache.hc.client5.http.SystemDefaultDnsResolver
105- * ... Rfc6724Resolver familyPreference=IPV4_ONLY
106- * ... delegate returned 2 addresses for 'ipv6-test.com': [IPv4(51.75.78.103), IPv6]
107- * ... after family filter IPV4_ONLY -> 1 candidate(s): [IPv4(51.75.78.103)]
108- * ... RFC6724 output order: [IPv4(51.75.78.103)]
109- * ... final ordered list for 'ipv6-test.com': [IPv4(51.75.78.103)]
110- * ... using Happy Eyeballs: attemptDelay=250ms, otherFamilyDelay=50ms, pref=IPV4_ONLY
111- * ... scheduling connect to IPv4 in 0 ms
112- * ... winner: connected to ipv6-test.com/51.75.78.103:80
113- * }</pre>
80+ * # Pass multiple URIs
81+ * ./run-example.sh AsyncClientHappyEyeballs http://neverssl.com/ https://example.org/
11482 *
115- * <p><u>B) {@code PREFER_IPV6}</u> — v6 attempted first, v4 shortly after</p>
116- * <pre>{@code
117- * ... familyPreference=PREFER_IPV6
118- * ... delegate returned 2 addresses: [IPv6(2001:db8::1234), IPv4(203.0.113.10)]
119- * ... RFC6724 inferred source addresses: [IPv6(2001:db8::1), IPv4(192.0.2.10)]
120- * ... RFC6724 output order: [IPv6(...), IPv4(...)]
121- * ... PREFER_IPV6 keeps IPv6 first
122- * ... using Happy Eyeballs ... pref=PREFER_IPV6
123- * ... scheduling connect to IPv6 in 0 ms, IPv4 in 50 ms
124- * ... winner: connected to IPv6(...)
125- * }</pre>
83+ * # Optional system properties (the runner forwards -D...):
84+ * # -Dhc.he.pref=INTERLEAVE|PREFER_IPV4|PREFER_IPV6|IPV4_ONLY|IPV6_ONLY (default: INTERLEAVE)
85+ * # -Dhc.he.delay.ms=250 (Happy Eyeballs attempt spacing; default 250)
86+ * # -Dhc.he.other.ms=50 (first other-family offset; default 50; clamped ≤ attempt delay)
87+ * # -Dhc.connect.ms=10000 (TCP connect timeout; default 10000)
12688 *
127- * <p><u>C) {@code INTERLEAVE}</u> — alternate families while honoring RFC-sorted groups</p>
128- * <pre>{@code
129- * ... familyPreference=INTERLEAVE
130- * ... RFC6724 output order: [IPv4(...), IPv6(...), IPv6(...), IPv4(...)]
131- * ... INTERLEAVE starting family=IPv4 -> [IPv4(...), IPv6(...), IPv4(...), IPv6(...)]
132- * ... final ordered list: [IPv4, IPv6, IPv4, IPv6]
133- * ... HEv2 pref=INTERLEAVE; attempts interleaved with 50 ms "other family" offset
134- * }</pre>
89+ * ./run-example.sh AsyncClientHappyEyeballs http://neverssl.com/ \
90+ * -Dhc.he.pref=INTERLEAVE -Dhc.he.delay.ms=250 -Dhc.he.other.ms=50 -Dhc.connect.ms=8000
91+ * </pre>
13592 *
136- * <p><b>Legend (how to read the resolver lines)</b>:</p >
93+ * <h3>What to expect</h3 >
13794 * <ul>
138- * <li><code>delegate returned N addresses</code> — raw A/AAAA from the underlying resolver.</li>
139- * <li><code>after family filter ...</code> — hard filter for {@code IPV4_ONLY}/{@code IPV6_ONLY}; no filter for {@code PREFER_*}/{@code INTERLEAVE}.</li>
140- * <li><code>RFC6724 inferred source addresses</code> — UDP DatagramSocket.connect() (no packets) to discover the OS-selected source per destination.</li>
141- * <li><code>candidate dst=... src=... dst[scope=..., prec=..., label=...]</code> — attributes used by RFC 6724 compare:
142- * Rules applied here: 1 (avoid unusable), 2 (scope match), 5 (label match), 6 (precedence), 8 (smaller scope),
143- * 9 (longest common prefix for v6). Equal → stable order.</li>
144- * <li><code>output order</code> — RFC 6724 ordering prior to preference reshaping.</li>
145- * <li><code>INTERLEAVE / PREFER_*</code> — final family reshaping before handing to the connection layer.</li>
146- * <li>Happy-Eyeballs lines show pacing (<code>attemptDelay</code>) and first other-family offset
147- * (<code>otherFamilyDelay</code>), plus the eventual <em>winner</em>.</li>
95+ * <li>For dual-stack hosts, the client schedules interleaved IPv6/IPv4 connects per the preference and delays.</li>
96+ * <li>On networks without working IPv6, the IPv6 attempt will likely fail quickly while IPv4 succeeds.</li>
97+ * <li>If you force {@code IPV6_ONLY} on a network without IPv6 routing, you’ll get
98+ * {@code java.net.SocketException: Network is unreachable} — that’s expected.</li>
14899 * </ul>
149100 *
150- * <p><b>Implementation tip</b>:
151- * for the clearest logs, keep the resolver’s bias and the client’s connection preference aligned
152- * (e.g., construct {@code new Rfc6724AddressSelectingDnsResolver(SystemDefaultDnsResolver.INSTANCE, pref)}
153- * and use the same {@code pref} in {@link org.apache.hc.client5.http.config.ConnectionConfig#setProtocolFamilyPreference}).</p>
154- *
155- * <p><b>Production note</b>: leave these categories at DEBUG and your root at INFO; enable on demand when diagnosing
156- * dual-stack reachability (e.g., broken IPv6 will typically show an inferred source of {@code 0.0.0.0} for the v6 dst,
157- * causing RFC 6724 Rule 1 to de-prioritize it).</p>
101+ * <h3>Tip</h3>
102+ * <p>For the clearest behavior, align the resolver bias and the connection preference:
103+ * construct the resolver with the same {@link ProtocolFamilyPreference} that you set in
104+ * {@link ConnectionConfig}.</p>
158105 */
159-
160106public final class AsyncClientHappyEyeballs {
161107
108+ private AsyncClientHappyEyeballs () {
109+ }
110+
162111 public static void main (final String [] args ) throws Exception {
163- // Wrap the system resolver with RFC 6724 destination selection
112+ // --- Read settings from system properties (with sensible defaults) ---
113+ final ProtocolFamilyPreference pref = parsePref (System .getProperty ("hc.he.pref" ), ProtocolFamilyPreference .INTERLEAVE );
114+ final long attemptDelayMs = parseLong (System .getProperty ("hc.he.delay.ms" ), 250L );
115+ final long otherFamilyDelayMs = Math .min (parseLong (System .getProperty ("hc.he.other.ms" ), 50L ), attemptDelayMs );
116+ final long connectMs = parseLong (System .getProperty ("hc.connect.ms" ), 10000L ); // 10s default
117+
118+ // --- Resolve targets from CLI args (or fall back to ipv6-test.com pair) ---
119+ final List <URI > targets = new ArrayList <URI >();
120+ if (args != null && args .length > 0 ) {
121+ for (int i = 0 ; i < args .length ; i ++) {
122+ final URI u = safeParse (args [i ]);
123+ if (u != null ) {
124+ targets .add (u );
125+ } else {
126+ System .out .println ("Skipping invalid URI: " + args [i ]);
127+ }
128+ }
129+ } else {
130+ try {
131+ targets .add (new URI ("http://ipv6-test.com/" ));
132+ targets .add (new URI ("https://ipv6-test.com/" ));
133+ } catch (final URISyntaxException ignore ) {
134+ }
135+ }
136+
137+ // --- Print banner so the runner shows the configuration up front ---
138+ System .out .println ("Happy Eyeballs: pref=" + pref
139+ + ", attemptDelay=" + attemptDelayMs + "ms"
140+ + ", otherFamilyDelay=" + otherFamilyDelayMs + "ms"
141+ + ", connectTimeout=" + connectMs + "ms" );
142+
143+ // --- DNS resolver with RFC 6724 selection (biased using the same pref for clarity) ---
164144 final Rfc6724AddressSelectingDnsResolver dnsResolver =
165- new Rfc6724AddressSelectingDnsResolver (SystemDefaultDnsResolver .INSTANCE );
145+ new Rfc6724AddressSelectingDnsResolver (SystemDefaultDnsResolver .INSTANCE , pref );
166146
167- // Enable Happy Eyeballs pacing & family policy via ConnectionConfig
147+ // --- Connection config enabling HEv2 pacing and family preference ---
168148 final ConnectionConfig connectionConfig = ConnectionConfig .custom ()
169- .setStaggeredConnectEnabled (true ) // off by default
170- .setHappyEyeballsAttemptDelay (TimeValue .ofMilliseconds (250 )) // pacing between attempts
171- .setHappyEyeballsOtherFamilyDelay (TimeValue .ofMilliseconds (50 )) // delay before first other-family try
172- .setProtocolFamilyPreference (ProtocolFamilyPreference .IPV6_ONLY ) // or PREFER_IPV6 / IPV4_ONLY / etc.
149+ .setStaggeredConnectEnabled (true )
150+ .setHappyEyeballsAttemptDelay (TimeValue .ofMilliseconds (attemptDelayMs ))
151+ .setHappyEyeballsOtherFamilyDelay (TimeValue .ofMilliseconds (otherFamilyDelayMs ))
152+ .setProtocolFamilyPreference (pref ).setConnectTimeout (Timeout .ofMilliseconds (connectMs ))
153+
173154 .build ();
174155
175- // TLS strategy (the builder's build() may be deprecated in your snapshot; it's fine for examples)
156+ final RequestConfig requestConfig = RequestConfig .custom ()
157+ .build ();
158+
159+ // --- TLS strategy (uses system properties for trust/key stores, ALPN, etc.) ---
176160 final TlsStrategy tls = ClientTlsStrategyBuilder .create ()
177- .useSystemProperties ().buildAsync ();
161+ .useSystemProperties ()
162+ .buildAsync ();
178163
179- // Connection manager wires in DNS + ConnectionConfig + TLS
164+ // --- Connection manager wires in DNS + ConnectionConfig + TLS ---
180165 final PoolingAsyncClientConnectionManager cm =
181166 PoolingAsyncClientConnectionManagerBuilder .create ()
182167 .setDnsResolver (dnsResolver )
@@ -186,15 +171,24 @@ public static void main(final String[] args) throws Exception {
186171
187172 final CloseableHttpAsyncClient client = HttpAsyncClients .custom ()
188173 .setConnectionManager (cm )
174+ .setDefaultRequestConfig (requestConfig )
189175 .build ();
190176
191177 client .start ();
192178
193- final String [] schemes = {URIScheme .HTTP .id , URIScheme .HTTPS .id };
194- for (final String scheme : schemes ) {
179+ // --- Execute each target once ---
180+ for (int i = 0 ; i < targets .size (); i ++) {
181+ final URI uri = targets .get (i );
182+ final HttpHost host = new HttpHost (
183+ uri .getScheme (),
184+ uri .getHost (),
185+ computePort (uri )
186+ );
187+ final String path = buildPathAndQuery (uri );
188+
195189 final SimpleHttpRequest request = SimpleRequestBuilder .get ()
196- .setHttpHost (new HttpHost ( scheme , "ipv6-test.com" ) )
197- .setPath ("/" )
190+ .setHttpHost (host )
191+ .setPath (path )
198192 .build ();
199193
200194 System .out .println ("Executing request " + request );
@@ -221,13 +215,95 @@ public void cancelled() {
221215
222216 try {
223217 future .get ();
224- } catch (java .util .concurrent .ExecutionException e ) {
225- System .out .println (request + " -> " + e .getCause ());
218+ } catch (final java .util .concurrent .ExecutionException ex ) {
219+ // Show the root cause without a giant stack trace in the example
220+ System .out .println (request + " -> " + ex .getCause ());
226221 }
227222 }
228223
229224 System .out .println ("Shutting down" );
230225 client .close (CloseMode .GRACEFUL );
231226 cm .close (CloseMode .GRACEFUL );
232227 }
228+
229+ // ------------ helpers (Java 8 friendly) ------------
230+
231+ private static int computePort (final URI uri ) {
232+ final int p = uri .getPort ();
233+ if (p >= 0 ) {
234+ return p ;
235+ }
236+ final String scheme = uri .getScheme ();
237+ if ("http" .equalsIgnoreCase (scheme )) {
238+ return 80 ;
239+ }
240+ if ("https" .equalsIgnoreCase (scheme )) {
241+ return 443 ;
242+ }
243+ return -1 ;
244+ }
245+
246+ private static String buildPathAndQuery (final URI uri ) {
247+ String path = uri .getRawPath ();
248+ if (path == null || path .isEmpty ()) {
249+ path = "/" ;
250+ }
251+ final String query = uri .getRawQuery ();
252+ if (query != null && !query .isEmpty ()) {
253+ return path + "?" + query ;
254+ }
255+ return path ;
256+ }
257+
258+ private static long parseLong (final String s , final long defVal ) {
259+ if (s == null ) {
260+ return defVal ;
261+ }
262+ try {
263+ return Long .parseLong (s .trim ());
264+ } catch (final NumberFormatException ignore ) {
265+ return defVal ;
266+ }
267+ }
268+
269+ private static ProtocolFamilyPreference parsePref (final String s , final ProtocolFamilyPreference defVal ) {
270+ if (s == null ) {
271+ return defVal ;
272+ }
273+ final String u = s .trim ().toUpperCase (java .util .Locale .ROOT );
274+ if ("IPV6_ONLY" .equals (u )) {
275+ return ProtocolFamilyPreference .IPV6_ONLY ;
276+ }
277+ if ("IPV4_ONLY" .equals (u )) {
278+ return ProtocolFamilyPreference .IPV4_ONLY ;
279+ }
280+ if ("PREFER_IPV6" .equals (u )) {
281+ return ProtocolFamilyPreference .PREFER_IPV6 ;
282+ }
283+ if ("PREFER_IPV4" .equals (u )) {
284+ return ProtocolFamilyPreference .PREFER_IPV4 ;
285+ }
286+ if ("INTERLEAVE" .equals (u )) {
287+ return ProtocolFamilyPreference .INTERLEAVE ;
288+ }
289+ return defVal ;
290+ }
291+
292+ private static URI safeParse (final String s ) {
293+ try {
294+ final URI u = new URI (s );
295+ final String scheme = u .getScheme ();
296+ if (!URIScheme .HTTP .same (scheme ) && !URIScheme .HTTPS .same (scheme )) {
297+ System .out .println ("Unsupported scheme (only http/https): " + s );
298+ return null ;
299+ }
300+ if (u .getHost () == null ) {
301+ System .out .println ("Missing host in URI: " + s );
302+ return null ;
303+ }
304+ return u ;
305+ } catch (final URISyntaxException ex ) {
306+ return null ;
307+ }
308+ }
233309}
0 commit comments