26
26
*/
27
27
package org .apache .hc .client5 .http .examples ;
28
28
29
+ import java .net .URI ;
30
+ import java .net .URISyntaxException ;
31
+ import java .util .ArrayList ;
32
+ import java .util .List ;
29
33
import java .util .concurrent .Future ;
30
34
31
35
import org .apache .hc .client5 .http .Rfc6724AddressSelectingDnsResolver ;
37
41
import org .apache .hc .client5 .http .async .methods .SimpleResponseConsumer ;
38
42
import org .apache .hc .client5 .http .config .ConnectionConfig ;
39
43
import org .apache .hc .client5 .http .config .ProtocolFamilyPreference ;
44
+ import org .apache .hc .client5 .http .config .RequestConfig ;
40
45
import org .apache .hc .client5 .http .impl .async .CloseableHttpAsyncClient ;
41
46
import org .apache .hc .client5 .http .impl .async .HttpAsyncClients ;
42
47
import org .apache .hc .client5 .http .impl .nio .PoolingAsyncClientConnectionManager ;
49
54
import org .apache .hc .core5 .http .nio .ssl .TlsStrategy ;
50
55
import org .apache .hc .core5 .io .CloseMode ;
51
56
import org .apache .hc .core5 .util .TimeValue ;
57
+ import org .apache .hc .core5 .util .Timeout ;
52
58
53
59
/**
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>
55
61
*
56
- * <p>This example shows how to:
62
+ * <p>This example shows how to:</p>
57
63
* <ul>
58
64
* <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>
60
66
* <li>Use {@link org.apache.hc.client5.http.config.ConnectionConfig} to enable <b>Happy Eyeballs v2</b> pacing
61
67
* 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>
64
70
* </ul>
65
- * </p>
66
71
*
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
99
76
*
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/
101
79
*
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/
114
82
*
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)
126
88
*
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>
135
92
*
136
- * <p><b>Legend (how to read the resolver lines)</b>:</p >
93
+ * <h3>What to expect</h3 >
137
94
* <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>
148
99
* </ul>
149
100
*
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>
158
105
*/
159
-
160
106
public final class AsyncClientHappyEyeballs {
161
107
108
+ private AsyncClientHappyEyeballs () {
109
+ }
110
+
162
111
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) ---
164
144
final Rfc6724AddressSelectingDnsResolver dnsResolver =
165
- new Rfc6724AddressSelectingDnsResolver (SystemDefaultDnsResolver .INSTANCE );
145
+ new Rfc6724AddressSelectingDnsResolver (SystemDefaultDnsResolver .INSTANCE , pref );
166
146
167
- // Enable Happy Eyeballs pacing & family policy via ConnectionConfig
147
+ // --- Connection config enabling HEv2 pacing and family preference ---
168
148
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
+
173
154
.build ();
174
155
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.) ---
176
160
final TlsStrategy tls = ClientTlsStrategyBuilder .create ()
177
- .useSystemProperties ().buildAsync ();
161
+ .useSystemProperties ()
162
+ .buildAsync ();
178
163
179
- // Connection manager wires in DNS + ConnectionConfig + TLS
164
+ // --- Connection manager wires in DNS + ConnectionConfig + TLS ---
180
165
final PoolingAsyncClientConnectionManager cm =
181
166
PoolingAsyncClientConnectionManagerBuilder .create ()
182
167
.setDnsResolver (dnsResolver )
@@ -186,15 +171,24 @@ public static void main(final String[] args) throws Exception {
186
171
187
172
final CloseableHttpAsyncClient client = HttpAsyncClients .custom ()
188
173
.setConnectionManager (cm )
174
+ .setDefaultRequestConfig (requestConfig )
189
175
.build ();
190
176
191
177
client .start ();
192
178
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
+
195
189
final SimpleHttpRequest request = SimpleRequestBuilder .get ()
196
- .setHttpHost (new HttpHost ( scheme , "ipv6-test.com" ) )
197
- .setPath ("/" )
190
+ .setHttpHost (host )
191
+ .setPath (path )
198
192
.build ();
199
193
200
194
System .out .println ("Executing request " + request );
@@ -221,13 +215,95 @@ public void cancelled() {
221
215
222
216
try {
223
217
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 ());
226
221
}
227
222
}
228
223
229
224
System .out .println ("Shutting down" );
230
225
client .close (CloseMode .GRACEFUL );
231
226
cm .close (CloseMode .GRACEFUL );
232
227
}
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
+ }
233
309
}
0 commit comments