Skip to content

Commit d6fba2c

Browse files
committed
add experimental RFC 9218 Priority header support.
Wire H2RequestPriority via H2AsyncClientBuilder.enablePriorityHeader(); omit on defaults, allow overwrite.
1 parent 26d51f6 commit d6fba2c

File tree

5 files changed

+427
-4
lines changed

5 files changed

+427
-4
lines changed

httpclient5/src/main/java/org/apache/hc/client5/http/config/RequestConfig.java

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@
3232
import java.util.concurrent.TimeUnit;
3333

3434
import org.apache.hc.core5.annotation.Contract;
35+
import org.apache.hc.core5.annotation.Experimental;
3536
import org.apache.hc.core5.annotation.ThreadingBehavior;
3637
import org.apache.hc.core5.http.HttpHost;
38+
import org.apache.hc.core5.http2.priority.PriorityValue;
3739
import org.apache.hc.core5.util.Args;
3840
import org.apache.hc.core5.util.TimeValue;
3941
import org.apache.hc.core5.util.Timeout;
@@ -69,13 +71,18 @@ public class RequestConfig implements Cloneable {
6971

7072
private final ExpectContinueTrigger expectContinueTrigger;
7173

74+
/**
75+
* HTTP/2 Priority header value to emit when using H2+. Null means “don’t emit”.
76+
*/
77+
private final PriorityValue h2Priority;
78+
7279
/**
7380
* Intended for CDI compatibility
7481
*/
7582
protected RequestConfig() {
7683
this(false, null, null, false, false, 0, false, null, null,
7784
DEFAULT_CONNECTION_REQUEST_TIMEOUT, null, null, DEFAULT_CONN_KEEP_ALIVE, false, false, false, null,
78-
ExpectContinueTrigger.ALWAYS);
85+
ExpectContinueTrigger.ALWAYS, null);
7986
}
8087

8188
RequestConfig(
@@ -96,7 +103,8 @@ protected RequestConfig() {
96103
final boolean hardCancellationEnabled,
97104
final boolean protocolUpgradeEnabled,
98105
final Path unixDomainSocket,
99-
final ExpectContinueTrigger expectContinueTrigger) {
106+
final ExpectContinueTrigger expectContinueTrigger,
107+
final PriorityValue h2Priority) {
100108
super();
101109
this.expectContinueEnabled = expectContinueEnabled;
102110
this.proxy = proxy;
@@ -116,6 +124,7 @@ protected RequestConfig() {
116124
this.protocolUpgradeEnabled = protocolUpgradeEnabled;
117125
this.unixDomainSocket = unixDomainSocket;
118126
this.expectContinueTrigger = expectContinueTrigger;
127+
this.h2Priority = h2Priority;
119128
}
120129

121130
/**
@@ -244,6 +253,15 @@ public ExpectContinueTrigger getExpectContinueTrigger() {
244253
return expectContinueTrigger;
245254
}
246255

256+
/**
257+
* Returns the HTTP/2+ priority preference for this request or {@code null} if unset.
258+
* @since 5.6
259+
*/
260+
@Experimental
261+
public PriorityValue getH2Priority() {
262+
return h2Priority;
263+
}
264+
247265
@Override
248266
protected RequestConfig clone() throws CloneNotSupportedException {
249267
return (RequestConfig) super.clone();
@@ -296,7 +314,8 @@ public static RequestConfig.Builder copy(final RequestConfig config) {
296314
.setContentCompressionEnabled(config.isContentCompressionEnabled())
297315
.setHardCancellationEnabled(config.isHardCancellationEnabled())
298316
.setProtocolUpgradeEnabled(config.isProtocolUpgradeEnabled())
299-
.setUnixDomainSocket(config.getUnixDomainSocket());
317+
.setUnixDomainSocket(config.getUnixDomainSocket())
318+
.setH2Priority(config.getH2Priority());
300319
}
301320

302321
public static class Builder {
@@ -319,6 +338,7 @@ public static class Builder {
319338
private boolean protocolUpgradeEnabled;
320339
private Path unixDomainSocket;
321340
private ExpectContinueTrigger expectContinueTrigger;
341+
private PriorityValue h2Priority;
322342

323343
Builder() {
324344
super();
@@ -691,6 +711,16 @@ public Builder setExpectContinueTrigger(final ExpectContinueTrigger trigger) {
691711
return this;
692712
}
693713

714+
/**
715+
* Sets HTTP/2+ request priority. If {@code null}, the header is not emitted.
716+
* @since 5.6
717+
*/
718+
@Experimental
719+
public Builder setH2Priority(final PriorityValue priority) {
720+
this.h2Priority = priority;
721+
return this;
722+
}
723+
694724
public RequestConfig build() {
695725
return new RequestConfig(
696726
expectContinueEnabled,
@@ -710,7 +740,8 @@ public RequestConfig build() {
710740
hardCancellationEnabled,
711741
protocolUpgradeEnabled,
712742
unixDomainSocket,
713-
expectContinueTrigger);
743+
expectContinueTrigger,
744+
h2Priority);
714745
}
715746

716747
}

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,15 @@
6464
import org.apache.hc.client5.http.impl.auth.SystemDefaultCredentialsProvider;
6565
import org.apache.hc.client5.http.impl.nio.MultihomeConnectionInitiator;
6666
import org.apache.hc.client5.http.impl.routing.DefaultRoutePlanner;
67+
import org.apache.hc.client5.http.protocol.H2RequestPriority;
6768
import org.apache.hc.client5.http.protocol.RedirectStrategy;
6869
import org.apache.hc.client5.http.protocol.RequestAddCookies;
6970
import org.apache.hc.client5.http.protocol.RequestDefaultHeaders;
7071
import org.apache.hc.client5.http.protocol.RequestExpectContinue;
7172
import org.apache.hc.client5.http.protocol.ResponseProcessCookies;
7273
import org.apache.hc.client5.http.routing.HttpRoutePlanner;
7374
import org.apache.hc.client5.http.ssl.DefaultClientTlsStrategy;
75+
import org.apache.hc.core5.annotation.Experimental;
7476
import org.apache.hc.core5.annotation.Internal;
7577
import org.apache.hc.core5.concurrent.DefaultThreadFactory;
7678
import org.apache.hc.core5.function.Callback;
@@ -216,6 +218,8 @@ private ExecInterceptorEntry(
216218

217219
private Decorator<IOSession> ioSessionDecorator;
218220

221+
private boolean priorityHeaderDisabled;
222+
219223
public static H2AsyncClientBuilder create() {
220224
return new H2AsyncClientBuilder();
221225
}
@@ -312,6 +316,16 @@ public final H2AsyncClientBuilder setIoSessionDecorator(final Decorator<IOSessio
312316
return this;
313317
}
314318

319+
/**
320+
* Disable installing the HTTP/2 Priority header interceptor by default.
321+
* @since 5.6
322+
*/
323+
@Experimental
324+
public final H2AsyncClientBuilder disablePriorityHeader() {
325+
this.priorityHeaderDisabled = true;
326+
return this;
327+
}
328+
315329
/**
316330
* Adds this protocol interceptor to the head of the protocol processing list.
317331
*
@@ -699,6 +713,7 @@ public final H2AsyncClientBuilder evictIdleConnections(final TimeValue maxIdleTi
699713
return this;
700714
}
701715

716+
702717
/**
703718
* Request exec chain customization and extension.
704719
* <p>
@@ -762,6 +777,11 @@ public CloseableHttpAsyncClient build() {
762777
}
763778
}
764779
}
780+
781+
if (!priorityHeaderDisabled) {
782+
b.addLast(H2RequestPriority.INSTANCE);
783+
}
784+
765785
b.addAll(
766786
new H2RequestTargetHost(),
767787
new RequestDefaultHeaders(defaultHeaders),
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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+
28+
package org.apache.hc.client5.http.protocol;
29+
30+
import java.io.IOException;
31+
32+
import org.apache.hc.client5.http.config.RequestConfig;
33+
import org.apache.hc.core5.annotation.Contract;
34+
import org.apache.hc.core5.annotation.Experimental;
35+
import org.apache.hc.core5.annotation.ThreadingBehavior;
36+
import org.apache.hc.core5.http.EntityDetails;
37+
import org.apache.hc.core5.http.Header;
38+
import org.apache.hc.core5.http.HttpException;
39+
import org.apache.hc.core5.http.HttpHeaders;
40+
import org.apache.hc.core5.http.HttpRequest;
41+
import org.apache.hc.core5.http.HttpRequestInterceptor;
42+
import org.apache.hc.core5.http.HttpVersion;
43+
import org.apache.hc.core5.http.ProtocolVersion;
44+
import org.apache.hc.core5.http.protocol.HttpContext;
45+
import org.apache.hc.core5.http2.priority.PriorityFormatter;
46+
import org.apache.hc.core5.http2.priority.PriorityValue;
47+
import org.apache.hc.core5.util.Args;
48+
49+
/**
50+
* Adds the {@code Priority} request header to HTTP/2+ requests when a per-request
51+
* priority is configured.
52+
* <p>
53+
* The priority is taken from {@link RequestConfig#getH2Priority()}. If a {@code Priority}
54+
* header is already present on the request, it is left unchanged. If formatting the
55+
* configured value yields an empty string (e.g., because it encodes protocol defaults),
56+
* the header is not added.
57+
*
58+
* @since 5.6
59+
*/
60+
@Experimental
61+
@Contract(threading = ThreadingBehavior.IMMUTABLE)
62+
public final class H2RequestPriority implements HttpRequestInterceptor {
63+
64+
/**
65+
* Singleton instance.
66+
*/
67+
public static final H2RequestPriority INSTANCE = new H2RequestPriority();
68+
69+
@Override
70+
public void process(
71+
final HttpRequest request,
72+
final EntityDetails entity,
73+
final HttpContext context) throws HttpException, IOException {
74+
75+
Args.notNull(request, "HTTP request");
76+
Args.notNull(context, "HTTP context");
77+
78+
final HttpClientContext httpClientContext = HttpClientContext.cast(context);
79+
80+
final ProtocolVersion pv = httpClientContext.getProtocolVersion();
81+
if (pv.compareToVersion(HttpVersion.HTTP_2) < 0) {
82+
return; // only for HTTP/2+
83+
}
84+
85+
final Header existing = request.getFirstHeader(HttpHeaders.PRIORITY);
86+
if (existing != null) {
87+
return;
88+
}
89+
90+
final RequestConfig requestConfig = httpClientContext.getRequestConfigOrDefault();
91+
if (requestConfig == null) {
92+
return;
93+
}
94+
95+
final PriorityValue pri = requestConfig.getH2Priority();
96+
if (pri == null || PriorityValue.defaults().equals(pri)) {
97+
return;
98+
}
99+
100+
final Header header = PriorityFormatter.formatHeader(pri);
101+
if (header.getValue() == null || header.getValue().isEmpty()) {
102+
return;
103+
}
104+
105+
request.addHeader(header);
106+
}
107+
}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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.examples;
28+
29+
import java.util.concurrent.Future;
30+
31+
import org.apache.hc.client5.http.async.methods.SimpleHttpRequest;
32+
import org.apache.hc.client5.http.async.methods.SimpleHttpResponse;
33+
import org.apache.hc.client5.http.async.methods.SimpleRequestBuilder;
34+
import org.apache.hc.client5.http.config.RequestConfig;
35+
import org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient;
36+
import org.apache.hc.client5.http.impl.async.H2AsyncClientBuilder;
37+
import org.apache.hc.client5.http.protocol.HttpClientContext;
38+
import org.apache.hc.core5.annotation.Experimental;
39+
import org.apache.hc.core5.http2.config.H2Config;
40+
import org.apache.hc.core5.http2.priority.PriorityValue;
41+
42+
/**
43+
* Demonstrates sending the HTTP/2 {@code Priority} request header using per-request configuration.
44+
*
45+
* <p><strong>How it works</strong>:
46+
* <ul>
47+
* <li>Call {@code enablePriorityHeader()} on the H2 client builder to register
48+
* {@link org.apache.hc.client5.http.protocol.H2RequestPriority}.</li>
49+
* <li>For each request, set a priority on the {@link RequestConfig} via
50+
* {@link RequestConfig.Builder#setH2Priority(PriorityValue)} and attach it to the
51+
* {@link HttpClientContext} passed to {@code execute}.</li>
52+
* </ul>
53+
*
54+
* <p><strong>Notes</strong>:
55+
* <ul>
56+
* <li>If a {@code Priority} header is already present on the request, it is preserved.</li>
57+
* <li>If the configured value encodes protocol defaults, the header is omitted.</li>
58+
* <li>Applies to HTTP/2+ only; HTTP/1.1 requests are unaffected.</li>
59+
* </ul>
60+
*
61+
* @since 5.6
62+
*/
63+
@Experimental
64+
public class AsyncClientH2Priority {
65+
66+
public static void main(final String[] args) throws Exception {
67+
try (CloseableHttpAsyncClient client = H2AsyncClientBuilder.create()
68+
.setH2Config(H2Config.custom()
69+
.setPushEnabled(false)
70+
.build())
71+
.build()) {
72+
73+
client.start();
74+
75+
// --- Request 1: non-default priority -> header sent (e.g., "u=0, i")
76+
final HttpClientContext ctx1 = HttpClientContext.create();
77+
ctx1.setRequestConfig(RequestConfig.custom()
78+
.setH2Priority(PriorityValue.of(0, true))
79+
.build());
80+
81+
final SimpleHttpRequest req1 = SimpleRequestBuilder.get("https://nghttp2.org/httpbin/headers").build();
82+
final Future<SimpleHttpResponse> f1 = client.execute(req1, ctx1, null);
83+
final SimpleHttpResponse r1 = f1.get();
84+
System.out.println("[/httpbin/headers] -> " + r1.getCode());
85+
System.out.println("Negotiated protocol (req1): " + ctx1.getProtocolVersion());
86+
System.out.println(r1.getBodyText());
87+
88+
// --- Request 2: defaults -> header omitted
89+
final HttpClientContext ctx2 = HttpClientContext.create();
90+
ctx2.setRequestConfig(RequestConfig.custom()
91+
.setH2Priority(PriorityValue.defaults())
92+
.build());
93+
94+
final SimpleHttpRequest req2 = SimpleRequestBuilder.get("https://nghttp2.org/httpbin/user-agent").build();
95+
final SimpleHttpResponse r2 = client.execute(req2, ctx2, null).get();
96+
System.out.println("[/httpbin/user-agent] -> " + r2.getCode());
97+
System.out.println("Negotiated protocol (req2): " + ctx2.getProtocolVersion());
98+
System.out.println(r2.getBodyText());
99+
100+
// --- Request 3: user-provided header -> preserved (no overwrite)
101+
final HttpClientContext ctx3 = HttpClientContext.create();
102+
ctx3.setRequestConfig(RequestConfig.custom()
103+
.setH2Priority(PriorityValue.of(5, false))
104+
.build());
105+
final SimpleHttpRequest req3 = SimpleRequestBuilder.get("https://nghttp2.org/httpbin/headers").build();
106+
req3.addHeader("Priority", "u=2");
107+
final SimpleHttpResponse r3 = client.execute(req3, ctx3, null).get();
108+
System.out.println("[/httpbin/headers with user header] -> " + r3.getCode());
109+
System.out.println("Negotiated protocol (req3): " + ctx3.getProtocolVersion());
110+
System.out.println(r3.getBodyText());
111+
}
112+
}
113+
}

0 commit comments

Comments
 (0)