Skip to content

Commit 258be30

Browse files
committed
add interceptor
1 parent f1e1ad0 commit 258be30

File tree

5 files changed

+479
-1
lines changed

5 files changed

+479
-1
lines changed

httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/H2Processors.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.apache.hc.core5.http2.protocol.H2RequestConformance;
3939
import org.apache.hc.core5.http2.protocol.H2RequestConnControl;
4040
import org.apache.hc.core5.http2.protocol.H2RequestContent;
41+
import org.apache.hc.core5.http2.protocol.H2RequestPriority;
4142
import org.apache.hc.core5.http2.protocol.H2RequestTargetHost;
4243
import org.apache.hc.core5.http2.protocol.H2RequestValidateHost;
4344
import org.apache.hc.core5.http2.protocol.H2ResponseConformance;
@@ -86,6 +87,7 @@ public static HttpProcessorBuilder customClient(final String agentInfo) {
8687
H2RequestTargetHost.INSTANCE,
8788
H2RequestContent.INSTANCE,
8889
H2RequestConnControl.INSTANCE,
90+
H2RequestPriority.INSTANCE,
8991
new RequestUserAgent(!TextUtils.isBlank(agentInfo) ? agentInfo :
9092
VersionInfo.getSoftwareInfo(SOFTWARE, "org.apache.hc.core5", HttpProcessors.class)),
9193
RequestExpectContinue.INSTANCE);

httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientH2StreamMultiplexer.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
import org.apache.hc.core5.http2.config.H2Config;
4444
import org.apache.hc.core5.http2.frame.DefaultFrameFactory;
4545
import org.apache.hc.core5.http2.frame.FrameFactory;
46+
import org.apache.hc.core5.http2.frame.RawFrame;
4647
import org.apache.hc.core5.http2.frame.StreamIdGenerator;
4748
import org.apache.hc.core5.reactor.ProtocolIOSession;
4849

@@ -51,6 +52,8 @@
5152
* client side HTTP/2 messaging protocol with full support for
5253
* multiplexed message transmission.
5354
*
55+
* Enforces RFC 9218 §7.1: clients MUST treat inbound PRIORITY_UPDATE as a connection error.
56+
*
5457
* @since 5.0
5558
*/
5659
@Internal
@@ -94,6 +97,7 @@ void acceptHeaderFrame() throws H2ConnectionException {
9497

9598
@Override
9699
void acceptPushFrame() throws H2ConnectionException {
100+
// Allowed; server may send push streams if enabled.
97101
}
98102

99103
@Override
@@ -135,6 +139,17 @@ H2StreamHandler createRemotelyInitiatedStream(
135139
context);
136140
}
137141

142+
/**
143+
* RFC 9218 §7.1: clients MUST treat inbound PRIORITY_UPDATE as a connection error.
144+
* @since 5.4
145+
*/
146+
@Override
147+
protected void onPriorityUpdateFrame(final RawFrame frame) throws H2ConnectionException {
148+
throw new H2ConnectionException(
149+
H2Error.PROTOCOL_ERROR,
150+
"Inbound PRIORITY_UPDATE is not permitted on client connections");
151+
}
152+
138153
@Override
139154
public String toString() {
140155
final StringBuilder buf = new StringBuilder();
@@ -145,4 +160,3 @@ public String toString() {
145160
}
146161

147162
}
148-
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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.core5.http2.protocol;
28+
29+
import java.io.IOException;
30+
31+
import org.apache.hc.core5.annotation.Contract;
32+
import org.apache.hc.core5.annotation.ThreadingBehavior;
33+
import org.apache.hc.core5.http.EntityDetails;
34+
import org.apache.hc.core5.http.Header;
35+
import org.apache.hc.core5.http.HttpException;
36+
import org.apache.hc.core5.http.HttpHeaders;
37+
import org.apache.hc.core5.http.HttpRequest;
38+
import org.apache.hc.core5.http.HttpRequestInterceptor;
39+
import org.apache.hc.core5.http.HttpVersion;
40+
import org.apache.hc.core5.http.ProtocolVersion;
41+
import org.apache.hc.core5.http.priority.PriorityFormatter;
42+
import org.apache.hc.core5.http.priority.PriorityValue;
43+
import org.apache.hc.core5.http.protocol.HttpContext;
44+
import org.apache.hc.core5.http.protocol.HttpCoreContext;
45+
import org.apache.hc.core5.util.Args;
46+
47+
/**
48+
* Emits RFC 9218 {@code Priority} request header for HTTP/2+.
49+
* <p>
50+
* The priority value is taken from the request context attribute
51+
* {@link #ATTR_HTTP2_PRIORITY_VALUE}. If the formatted value equals
52+
* RFC defaults (u=3, i=false) the header is omitted.
53+
* <p>
54+
* If {@code overwrite} is {@code false} (default), an existing {@code Priority}
55+
* header set by the caller is preserved.
56+
*
57+
* @since 5.4
58+
*/
59+
@Contract(threading = ThreadingBehavior.IMMUTABLE)
60+
public final class H2RequestPriority implements HttpRequestInterceptor {
61+
62+
/**
63+
* Context attribute to carry a {@link PriorityValue}.
64+
*/
65+
public static final String ATTR_HTTP2_PRIORITY_VALUE = "http2.priority.value";
66+
67+
/**
68+
* Singleton with {@code overwrite=false}.
69+
*/
70+
public static final H2RequestPriority INSTANCE = new H2RequestPriority(false);
71+
72+
private final boolean overwrite;
73+
74+
public H2RequestPriority() {
75+
this(false);
76+
}
77+
78+
public H2RequestPriority(final boolean overwrite) {
79+
this.overwrite = overwrite;
80+
}
81+
82+
@Override
83+
public void process(final HttpRequest request, final EntityDetails entity,
84+
final HttpContext context) throws HttpException, IOException {
85+
86+
Args.notNull(request, "HTTP request");
87+
Args.notNull(context, "HTTP context");
88+
89+
final ProtocolVersion ver = HttpCoreContext.cast(context).getProtocolVersion();
90+
if (ver == null || ver.compareToVersion(HttpVersion.HTTP_2) < 0) {
91+
return; // only for HTTP/2+
92+
}
93+
94+
final Header existing = request.getFirstHeader(HttpHeaders.PRIORITY);
95+
if (existing != null && !overwrite) {
96+
return; // respect caller-set header
97+
}
98+
99+
final PriorityValue pv = HttpCoreContext.cast(context)
100+
.getAttribute(ATTR_HTTP2_PRIORITY_VALUE, PriorityValue.class);
101+
if (pv == null) {
102+
return;
103+
}
104+
105+
final String value = PriorityFormatter.format(pv);
106+
if (value == null) {
107+
// defaults (u=3, i=false) -> omit header
108+
if (overwrite && existing != null) {
109+
request.removeHeaders(HttpHeaders.PRIORITY);
110+
}
111+
return;
112+
}
113+
114+
if (overwrite && existing != null) {
115+
request.removeHeaders(HttpHeaders.PRIORITY);
116+
}
117+
request.addHeader(HttpHeaders.PRIORITY, value);
118+
}
119+
}
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
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.core5.http2.examples;
28+
29+
import java.io.BufferedReader;
30+
import java.io.InputStreamReader;
31+
import java.nio.charset.Charset;
32+
import java.nio.charset.StandardCharsets;
33+
import java.util.List;
34+
import java.util.concurrent.Future;
35+
36+
import org.apache.hc.core5.annotation.Experimental;
37+
import org.apache.hc.core5.http.ClassicHttpRequest;
38+
import org.apache.hc.core5.http.ClassicHttpResponse;
39+
import org.apache.hc.core5.http.ContentType;
40+
import org.apache.hc.core5.http.Header;
41+
import org.apache.hc.core5.http.HttpConnection;
42+
import org.apache.hc.core5.http.HttpEntity;
43+
import org.apache.hc.core5.http.HttpHost;
44+
import org.apache.hc.core5.http.impl.bootstrap.HttpAsyncRequester;
45+
import org.apache.hc.core5.http.io.support.ClassicRequestBuilder;
46+
import org.apache.hc.core5.http.nio.AsyncClientEndpoint;
47+
import org.apache.hc.core5.http.nio.support.classic.ClassicToAsyncRequestProducer;
48+
import org.apache.hc.core5.http.nio.support.classic.ClassicToAsyncResponseConsumer;
49+
import org.apache.hc.core5.http.priority.PriorityValue;
50+
import org.apache.hc.core5.http.protocol.HttpCoreContext;
51+
import org.apache.hc.core5.http2.HttpVersionPolicy;
52+
import org.apache.hc.core5.http2.config.H2Config;
53+
import org.apache.hc.core5.http2.frame.RawFrame;
54+
import org.apache.hc.core5.http2.impl.H2Processors;
55+
import org.apache.hc.core5.http2.impl.nio.H2StreamListener;
56+
import org.apache.hc.core5.http2.protocol.H2RequestPriority;
57+
import org.apache.hc.core5.io.CloseMode;
58+
import org.apache.hc.core5.util.Timeout;
59+
60+
/**
61+
* Example: HTTP/2 request that sets RFC 9218 Priority via context and emits the "Priority" header.
62+
* <p>
63+
* Requires H2Processors to include H2RequestPriority (client chain) and an HTTP/2 connection.
64+
*/
65+
@Experimental
66+
public class ClassicH2PriorityExample {
67+
68+
public static void main(final String[] args) throws Exception {
69+
70+
// Force HTTP/2 and disable push for a cleaner demo
71+
final H2Config h2Config = H2Config.custom()
72+
.setPushEnabled(false)
73+
.build();
74+
75+
// Ensure the client processor chain has H2RequestPriority inside (see H2Processors.customClient)
76+
final HttpAsyncRequester requester = org.apache.hc.core5.http2.impl.nio.bootstrap.H2RequesterBootstrap.bootstrap()
77+
.setH2Config(h2Config)
78+
.setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_2)
79+
.setHttpProcessor(H2Processors.client()) // includes H2RequestPriority
80+
.setStreamListener(new H2StreamListener() {
81+
@Override
82+
public void onHeaderInput(final HttpConnection connection, final int streamId, final List<? extends Header> headers) {
83+
for (final Header h : headers) {
84+
System.out.println(connection.getRemoteAddress() + " (" + streamId + ") << " + h);
85+
}
86+
}
87+
88+
@Override
89+
public void onHeaderOutput(final HttpConnection connection, final int streamId, final List<? extends Header> headers) {
90+
for (final Header h : headers) {
91+
System.out.println(connection.getRemoteAddress() + " (" + streamId + ") >> " + h);
92+
}
93+
}
94+
95+
@Override
96+
public void onFrameInput(final HttpConnection c, final int id, final RawFrame f) {
97+
}
98+
99+
@Override
100+
public void onFrameOutput(final HttpConnection c, final int id, final RawFrame f) {
101+
}
102+
103+
@Override
104+
public void onInputFlowControl(final HttpConnection c, final int id, final int d, final int s) {
105+
}
106+
107+
@Override
108+
public void onOutputFlowControl(final HttpConnection c, final int id, final int d, final int s) {
109+
}
110+
})
111+
.create();
112+
113+
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
114+
System.out.println("HTTP requester shutting down");
115+
requester.close(CloseMode.GRACEFUL);
116+
}));
117+
requester.start();
118+
119+
final HttpHost target = new HttpHost("nghttp2.org");
120+
final Future<AsyncClientEndpoint> future = requester.connect(target, Timeout.ofSeconds(30));
121+
final AsyncClientEndpoint clientEndpoint = future.get();
122+
123+
// ---- Request 1: Explicit non-default priority -> header MUST be emitted
124+
executeWithPriority(clientEndpoint, target, "/httpbin/headers", PriorityValue.of(0, true));
125+
126+
// ---- Request 2: RFC defaults -> header MUST be omitted by the interceptor
127+
executeWithPriority(clientEndpoint, target, "/httpbin/user-agent", PriorityValue.defaults());
128+
129+
System.out.println("Shutting down I/O reactor");
130+
requester.initiateShutdown();
131+
}
132+
133+
private static void executeWithPriority(
134+
final AsyncClientEndpoint endpoint,
135+
final HttpHost target,
136+
final String path,
137+
final PriorityValue priorityValue) throws Exception {
138+
139+
final ClassicHttpRequest request = ClassicRequestBuilder.get()
140+
.setHttpHost(target)
141+
.setPath(path)
142+
.build();
143+
144+
// Place the PriorityValue into the context so H2RequestPriority can format the header
145+
final HttpCoreContext ctx = HttpCoreContext.create();
146+
ctx.setAttribute(H2RequestPriority.ATTR_HTTP2_PRIORITY_VALUE, priorityValue);
147+
148+
final ClassicToAsyncRequestProducer requestProducer = new ClassicToAsyncRequestProducer(request, Timeout.ofMinutes(1));
149+
final ClassicToAsyncResponseConsumer responseConsumer = new ClassicToAsyncResponseConsumer(Timeout.ofMinutes(1));
150+
151+
endpoint.execute(requestProducer, responseConsumer, ctx, null);
152+
153+
requestProducer.blockWaiting().execute();
154+
try (ClassicHttpResponse response = responseConsumer.blockWaiting()) {
155+
System.out.println(path + " -> " + response.getCode());
156+
final HttpEntity entity = response.getEntity();
157+
if (entity != null) {
158+
final ContentType ct = ContentType.parse(entity.getContentType());
159+
final Charset cs = ContentType.getCharset(ct, StandardCharsets.UTF_8);
160+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(entity.getContent(), cs))) {
161+
String line;
162+
while ((line = reader.readLine()) != null) {
163+
System.out.println(line);
164+
}
165+
}
166+
}
167+
}
168+
}
169+
}

0 commit comments

Comments
 (0)