diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/EarlyHintsListener.java b/httpclient5/src/main/java/org/apache/hc/client5/http/EarlyHintsListener.java
new file mode 100644
index 0000000000..2c45c526c9
--- /dev/null
+++ b/httpclient5/src/main/java/org/apache/hc/client5/http/EarlyHintsListener.java
@@ -0,0 +1,79 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ *
The listener is invoked zero or more times per request, once for each + * {@code 103} received. It is never invoked for the final + * (non-1xx) response.
+ * + *{@code + * HttpClients.custom() + * .setEarlyHintsListener((hints, context) -> { + * for (Header h : hints.getHeaders("Link")) { + * // inspect preload links, metrics, etc. + * } + * }) + * .build(); + * }+ * + * @since 5.6 + */ +@FunctionalInterface +public interface EarlyHintsListener { + + /** + * Called for each received {@code 103 Early Hints} informational response. + * + * @param hints the {@code 103} response object as received on the wire + * @param context the current execution context (never {@code null}) + * @throws HttpException to signal an HTTP-layer error while handling hints + * @throws IOException to signal an I/O error while handling hints + */ + void onEarlyHints(HttpResponse hints, HttpContext context) throws HttpException, IOException; +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/EarlyHintsAsyncExec.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/EarlyHintsAsyncExec.java new file mode 100644 index 0000000000..4f3190e631 --- /dev/null +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/EarlyHintsAsyncExec.java @@ -0,0 +1,108 @@ +/* + * ==================================================================== + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + *
This handler wraps the downstream {@link org.apache.hc.client5.http.async.AsyncExecCallback} + * and forwards any informational response with code + * {@link org.apache.hc.core5.http.HttpStatus#SC_EARLY_HINTS 103} to the listener. + * All other responses (including the final response) are delegated unchanged.
+ * + * + *For security and interoperability, applications typically act only on + * headers considered safe in Early Hints (for example, {@code Link} with + * {@code rel=preload} or {@code rel=preconnect}).
+ * + * @see org.apache.hc.client5.http.EarlyHintsListener + * @see org.apache.hc.core5.http.HttpStatus#SC_EARLY_HINTS + * @see org.apache.hc.core5.http.nio.ResponseChannel#sendInformation(HttpResponse, HttpContext) + * @since 5.6 + */ +public final class EarlyHintsAsyncExec implements AsyncExecChainHandler { + private final EarlyHintsListener listener; + + public EarlyHintsAsyncExec(final EarlyHintsListener listener) { + this.listener = listener; + } + + @Override + public void execute(final HttpRequest request, + final AsyncEntityProducer entityProducer, + final AsyncExecChain.Scope scope, + final AsyncExecChain chain, + final AsyncExecCallback callback) throws HttpException, java.io.IOException { + + if (listener == null) { + chain.proceed(request, entityProducer, scope, callback); + return; + } + + chain.proceed(request, entityProducer, scope, new AsyncExecCallback() { + @Override + public void handleInformationResponse(final HttpResponse response) + throws HttpException, java.io.IOException { + if (response.getCode() == HttpStatus.SC_EARLY_HINTS) { + listener.onEarlyHints(response, scope.clientContext); + } + callback.handleInformationResponse(response); + } + + @Override + public org.apache.hc.core5.http.nio.AsyncDataConsumer handleResponse( + final HttpResponse response, final EntityDetails entityDetails) + throws HttpException, java.io.IOException { + return callback.handleResponse(response, entityDetails); + } + + @Override + public void completed() { + callback.completed(); + } + + @Override + public void failed(final Exception cause) { + callback.failed(cause); + } + }); + } +} diff --git a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java index 028604744e..9dd325f109 100644 --- a/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java +++ b/httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java @@ -42,6 +42,7 @@ import org.apache.hc.client5.http.AuthenticationStrategy; import org.apache.hc.client5.http.ConnectionKeepAliveStrategy; +import org.apache.hc.client5.http.EarlyHintsListener; import org.apache.hc.client5.http.HttpRequestRetryStrategy; import org.apache.hc.client5.http.SchemePortResolver; import org.apache.hc.client5.http.UserTokenHandler; @@ -262,6 +263,8 @@ private ExecInterceptorEntry( private ProxySelector proxySelector; + private EarlyHintsListener earlyHintsListener; + /** * Maps {@code Content-Encoding} tokens to decoder factories in insertion order. */ @@ -889,6 +892,22 @@ public HttpAsyncClientBuilder disableContentCompression() { return this; } + /** + * Registers a global {@link org.apache.hc.client5.http.EarlyHintsListener} + * that will be notified when the client receives {@code 103 Early Hints} + * informational responses for any request executed by the built client. + * + * @param listener the listener to receive {@code 103 Early Hints} events, + * or {@code null} to remove the listener + * @return this builder + * @since 5.6 + */ + public final HttpAsyncClientBuilder setEarlyHintsListener(final EarlyHintsListener listener) { + this.earlyHintsListener = listener; + return this; + } + + /** * Request exec chain customization and extension. *
@@ -1026,6 +1045,11 @@ public CloseableHttpAsyncClient build() {
authCachingDisabled),
ChainElement.CONNECT.name());
+ if (earlyHintsListener != null) {
+ addExecInterceptorBefore(ChainElement.PROTOCOL.name(), "early-hints",
+ new EarlyHintsAsyncExec(earlyHintsListener));
+ }
+
execChainDefinition.addFirst(
new AsyncProtocolExec(
targetAuthStrategyCopy,
diff --git a/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/EarlyHintsAsyncExecTest.java b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/EarlyHintsAsyncExecTest.java
new file mode 100644
index 0000000000..f2e6a81ee5
--- /dev/null
+++ b/httpclient5/src/test/java/org/apache/hc/client5/http/impl/async/EarlyHintsAsyncExecTest.java
@@ -0,0 +1,202 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * > linkHeaders = new AtomicReference<>(new ArrayList<>());
+
+ final EarlyHintsListener listener = (hints, ctx) -> {
+ if (hints.getCode() == HttpStatus.SC_EARLY_HINTS) {
+ hintsCount.incrementAndGet();
+ final Header[] hs = hints.getHeaders("Link");
+ final ArrayList