Skip to content

Commit cb650da

Browse files
committed
RFC 8297 Early Hints (103) support.
Expose EarlyHintsListener + builder hooks ( only for async) Deliver 103 via EarlyHintsAsyncExec; final response unchanged Add async tests and user-facing Javadocs drop the classic wiring
1 parent 1bfeaaf commit cb650da

File tree

4 files changed

+413
-0
lines changed

4 files changed

+413
-0
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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.http;
28+
29+
import java.io.IOException;
30+
31+
import org.apache.hc.core5.http.HttpException;
32+
import org.apache.hc.core5.http.HttpResponse;
33+
import org.apache.hc.core5.http.protocol.HttpContext;
34+
35+
/**
36+
* Callback interface for receiving {@code 103 Early Hints} (RFC&nbsp;8297)
37+
* informational responses emitted by the origin server before the final response.
38+
*
39+
* <p>The listener is invoked zero or more times per request, once for each
40+
* {@code 103} received. It is <strong>never</strong> invoked for the final
41+
* (non-1xx) response.</p>
42+
*
43+
* <h2>Semantics</h2>
44+
* <ul>
45+
* <li>Invoked on the I/O thread (async clients) or the request thread
46+
* (classic clients). Implementations should be fast and non-blocking.</li>
47+
* <li>Headers observed in Early Hints are advisory. Clients typically act only
48+
* on safe headers such as {@code Link: ...; rel=preload} or
49+
* {@code rel=preconnect}.</li>
50+
* <li>Multiple {@code 103} responses may be received; ordering follows the
51+
* network order prior to the final response.</li>
52+
* </ul>
53+
*
54+
* <h2>Example</h2>
55+
* <pre>{@code
56+
* HttpClients.custom()
57+
* .setEarlyHintsListener((hints, context) -> {
58+
* for (Header h : hints.getHeaders("Link")) {
59+
* // inspect preload links, metrics, etc.
60+
* }
61+
* })
62+
* .build();
63+
* }</pre>
64+
*
65+
* @since 5.6
66+
*/
67+
@FunctionalInterface
68+
public interface EarlyHintsListener {
69+
70+
/**
71+
* Called for each received {@code 103 Early Hints} informational response.
72+
*
73+
* @param hints the {@code 103} response object as received on the wire
74+
* @param context the current execution context (never {@code null})
75+
* @throws HttpException to signal an HTTP-layer error while handling hints
76+
* @throws IOException to signal an I/O error while handling hints
77+
*/
78+
void onEarlyHints(HttpResponse hints, HttpContext context) throws HttpException, IOException;
79+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
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.http.impl.async;
28+
29+
import org.apache.hc.client5.http.EarlyHintsListener;
30+
import org.apache.hc.client5.http.async.AsyncExecCallback;
31+
import org.apache.hc.client5.http.async.AsyncExecChain;
32+
import org.apache.hc.client5.http.async.AsyncExecChainHandler;
33+
import org.apache.hc.core5.http.EntityDetails;
34+
import org.apache.hc.core5.http.HttpException;
35+
import org.apache.hc.core5.http.HttpRequest;
36+
import org.apache.hc.core5.http.HttpResponse;
37+
import org.apache.hc.core5.http.HttpStatus;
38+
import org.apache.hc.core5.http.nio.AsyncEntityProducer;
39+
import org.apache.hc.core5.http.protocol.HttpContext;
40+
41+
/**
42+
* Execution chain handler that delivers {@code 103 Early Hints} (RFC&nbsp;8297)
43+
* informational responses to a user-provided {@link org.apache.hc.client5.http.EarlyHintsListener}
44+
* without affecting the final (non-1xx) response processing.
45+
*
46+
* <p>This handler wraps the downstream {@link org.apache.hc.client5.http.async.AsyncExecCallback}
47+
* and forwards any informational response with code
48+
* {@link org.apache.hc.core5.http.HttpStatus#SC_EARLY_HINTS 103} to the listener.
49+
* All other responses (including the final response) are delegated unchanged.</p>
50+
*
51+
*
52+
* <p>For security and interoperability, applications typically act only on
53+
* headers considered safe in Early Hints (for example, {@code Link} with
54+
* {@code rel=preload} or {@code rel=preconnect}).</p>
55+
*
56+
* @see org.apache.hc.client5.http.EarlyHintsListener
57+
* @see org.apache.hc.core5.http.HttpStatus#SC_EARLY_HINTS
58+
* @see org.apache.hc.core5.http.nio.ResponseChannel#sendInformation(HttpResponse, HttpContext)
59+
* @since 5.6
60+
*/
61+
public final class EarlyHintsAsyncExec implements AsyncExecChainHandler {
62+
private final EarlyHintsListener listener;
63+
64+
public EarlyHintsAsyncExec(final EarlyHintsListener listener) {
65+
this.listener = listener;
66+
}
67+
68+
@Override
69+
public void execute(final HttpRequest request,
70+
final AsyncEntityProducer entityProducer,
71+
final AsyncExecChain.Scope scope,
72+
final AsyncExecChain chain,
73+
final AsyncExecCallback callback) throws HttpException, java.io.IOException {
74+
75+
if (listener == null) {
76+
chain.proceed(request, entityProducer, scope, callback);
77+
return;
78+
}
79+
80+
chain.proceed(request, entityProducer, scope, new AsyncExecCallback() {
81+
@Override
82+
public void handleInformationResponse(final HttpResponse response)
83+
throws HttpException, java.io.IOException {
84+
if (response.getCode() == HttpStatus.SC_EARLY_HINTS) {
85+
listener.onEarlyHints(response, scope.clientContext);
86+
}
87+
callback.handleInformationResponse(response);
88+
}
89+
90+
@Override
91+
public org.apache.hc.core5.http.nio.AsyncDataConsumer handleResponse(
92+
final HttpResponse response, final EntityDetails entityDetails)
93+
throws HttpException, java.io.IOException {
94+
return callback.handleResponse(response, entityDetails);
95+
}
96+
97+
@Override
98+
public void completed() {
99+
callback.completed();
100+
}
101+
102+
@Override
103+
public void failed(final Exception cause) {
104+
callback.failed(cause);
105+
}
106+
});
107+
}
108+
}

httpclient5/src/main/java/org/apache/hc/client5/http/impl/async/HttpAsyncClientBuilder.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242

4343
import org.apache.hc.client5.http.AuthenticationStrategy;
4444
import org.apache.hc.client5.http.ConnectionKeepAliveStrategy;
45+
import org.apache.hc.client5.http.EarlyHintsListener;
4546
import org.apache.hc.client5.http.HttpRequestRetryStrategy;
4647
import org.apache.hc.client5.http.SchemePortResolver;
4748
import org.apache.hc.client5.http.UserTokenHandler;
@@ -262,6 +263,8 @@ private ExecInterceptorEntry(
262263

263264
private ProxySelector proxySelector;
264265

266+
private EarlyHintsListener earlyHintsListener;
267+
265268
/**
266269
* Maps {@code Content-Encoding} tokens to decoder factories in insertion order.
267270
*/
@@ -889,6 +892,22 @@ public HttpAsyncClientBuilder disableContentCompression() {
889892
return this;
890893
}
891894

895+
/**
896+
* Registers a global {@link org.apache.hc.client5.http.EarlyHintsListener}
897+
* that will be notified when the client receives {@code 103 Early Hints}
898+
* informational responses for any request executed by the built client.
899+
*
900+
* @param listener the listener to receive {@code 103 Early Hints} events,
901+
* or {@code null} to remove the listener
902+
* @return this builder
903+
* @since 5.6
904+
*/
905+
public final HttpAsyncClientBuilder setEarlyHintsListener(final EarlyHintsListener listener) {
906+
this.earlyHintsListener = listener;
907+
return this;
908+
}
909+
910+
892911
/**
893912
* Request exec chain customization and extension.
894913
* <p>
@@ -1026,6 +1045,11 @@ public CloseableHttpAsyncClient build() {
10261045
authCachingDisabled),
10271046
ChainElement.CONNECT.name());
10281047

1048+
if (earlyHintsListener != null) {
1049+
addExecInterceptorBefore(ChainElement.PROTOCOL.name(), "early-hints",
1050+
new EarlyHintsAsyncExec(earlyHintsListener));
1051+
}
1052+
10291053
execChainDefinition.addFirst(
10301054
new AsyncProtocolExec(
10311055
targetAuthStrategyCopy,

0 commit comments

Comments
 (0)