Skip to content

Commit 1c039ff

Browse files
committed
HTTP/2: implement RFC 9218 core pieces.
Advertise SETTINGS_NO_RFC7540_PRIORITIES=1 and honor peer’s setting when emitting PRIORITY_UPDATE. Add parsing for PRIORITY_UPDATE (empty PFV allowed) and expose onPriorityUpdateFrame hook. Track serverSettingsSeen/remoteNoH2Priorities; reject invalid SETTINGS and gate priority updates.
1 parent 49ae683 commit 1c039ff

File tree

21 files changed

+2189
-30
lines changed

21 files changed

+2189
-30
lines changed

httpcore5-h2/src/main/java/org/apache/hc/core5/http2/config/H2Param.java

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ public enum H2Param {
3838
MAX_CONCURRENT_STREAMS(0x3),
3939
INITIAL_WINDOW_SIZE(0x4),
4040
MAX_FRAME_SIZE(0x5),
41-
MAX_HEADER_LIST_SIZE(0x6);
41+
MAX_HEADER_LIST_SIZE(0x6),
42+
SETTINGS_NO_RFC7540_PRIORITIES (0x9);
4243

4344
int code;
4445

@@ -50,25 +51,32 @@ public int getCode() {
5051
return code;
5152
}
5253

53-
private static final H2Param[] LOOKUP_TABLE = new H2Param[6];
54+
private static final H2Param[] LOOKUP_TABLE;
5455
static {
55-
for (final H2Param param: H2Param.values()) {
56-
LOOKUP_TABLE[param.code - 1] = param;
56+
int max = 0;
57+
for (final H2Param p : H2Param.values()) {
58+
if (p.code > max) {
59+
max = p.code;
60+
}
61+
}
62+
LOOKUP_TABLE = new H2Param[max + 1];
63+
for (final H2Param p : H2Param.values()) {
64+
LOOKUP_TABLE[p.code] = p;
5765
}
5866
}
5967

6068
public static H2Param valueOf(final int code) {
61-
if (code < 1 || code > LOOKUP_TABLE.length) {
69+
if (code < 0 || code >= LOOKUP_TABLE.length) {
6270
return null;
6371
}
64-
return LOOKUP_TABLE[code - 1];
72+
return LOOKUP_TABLE[code];
6573
}
6674

6775
public static String toString(final int code) {
68-
if (code < 1 || code > LOOKUP_TABLE.length) {
76+
if (code < 0 || code >= LOOKUP_TABLE.length || LOOKUP_TABLE[code] == null) {
6977
return Integer.toString(code);
7078
}
71-
return LOOKUP_TABLE[code - 1].name();
79+
return LOOKUP_TABLE[code].name();
7280
}
7381

7482
}

httpcore5-h2/src/main/java/org/apache/hc/core5/http2/frame/FrameType.java

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ public enum FrameType {
4242
PING(0x06),
4343
GOAWAY(0x07),
4444
WINDOW_UPDATE(0x08),
45-
CONTINUATION(0x09);
45+
CONTINUATION(0x09),
46+
PRIORITY_UPDATE(0x10); // 16
4647

47-
int value;
48+
final int value;
4849

4950
FrameType(final int value) {
5051
this.value = value;
@@ -54,25 +55,38 @@ public int getValue() {
5455
return value;
5556
}
5657

57-
private static final FrameType[] LOOKUP_TABLE = new FrameType[10];
58+
private static final FrameType[] LOOKUP_TABLE;
5859
static {
59-
for (final FrameType frameType: FrameType.values()) {
60-
LOOKUP_TABLE[frameType.value] = frameType;
60+
int max = -1;
61+
for (final FrameType t : FrameType.values()) {
62+
if (t.value > max) {
63+
max = t.value;
64+
}
65+
}
66+
LOOKUP_TABLE = new FrameType[max + 1];
67+
for (final FrameType t : FrameType.values()) {
68+
LOOKUP_TABLE[t.value] = t;
6169
}
6270
}
6371

6472
public static FrameType valueOf(final int value) {
6573
if (value < 0 || value >= LOOKUP_TABLE.length) {
6674
return null;
6775
}
68-
return LOOKUP_TABLE[value];
76+
return LOOKUP_TABLE[value]; // may be null for gaps (e.g., 0x0A..0x0F)
6977
}
7078

7179
public static String toString(final int value) {
7280
if (value < 0 || value >= LOOKUP_TABLE.length) {
7381
return Integer.toString(value);
7482
}
75-
return LOOKUP_TABLE[value].name();
83+
final FrameType t = LOOKUP_TABLE[value];
84+
return t != null ? t.name() : Integer.toString(value);
7685
}
7786

87+
/** Convenience: compare this enum to a raw frame type byte. */
88+
public boolean same(final int rawType) {
89+
return this.value == rawType;
90+
}
7891
}
92+

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/AbstractH2StreamMultiplexer.java

Lines changed: 112 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import java.nio.ByteBuffer;
3333
import java.nio.channels.SelectionKey;
3434
import java.nio.charset.CharacterCodingException;
35+
import java.nio.charset.StandardCharsets;
3536
import java.util.Deque;
3637
import java.util.Iterator;
3738
import java.util.List;
@@ -137,6 +138,10 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED }
137138
private EndpointDetails endpointDetails;
138139
private boolean goAwayReceived;
139140

141+
// RFC 9218 gating state
142+
private volatile boolean serverSettingsSeen;
143+
private volatile boolean remoteNoH2Priorities;
144+
140145
AbstractH2StreamMultiplexer(
141146
final ProtocolIOSession ioSession,
142147
final FrameFactory frameFactory,
@@ -173,6 +178,9 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED }
173178

174179
this.lowMark = H2Config.INIT.getInitialWindowSize() / 2;
175180
this.streamListener = streamListener;
181+
182+
this.serverSettingsSeen = false;
183+
this.remoteNoH2Priorities = false;
176184
}
177185

178186
@Override
@@ -188,7 +196,6 @@ public String getId() {
188196

189197
abstract H2StreamHandler createRemotelyInitiatedStream(
190198
H2StreamChannel channel,
191-
192199
HttpProcessor httpProcessor,
193200
BasicHttpConnectionMetrics connMetrics,
194201
HandlerFactory<AsyncPushConsumer> pushHandlerFactory) throws IOException;
@@ -261,6 +268,11 @@ private void commitFrame(final RawFrame frame) throws IOException {
261268
}
262269
}
263270

271+
protected final void writeExtensionFrame(final int type, final int flags, final int streamId, final ByteBuffer payload)
272+
throws IOException {
273+
commitFrame(new RawFrame(type, flags, streamId, payload));
274+
}
275+
264276
private void commitHeaders(
265277
final int streamId, final List<? extends Header> headers, final boolean endStream) throws IOException {
266278
if (streamListener != null) {
@@ -418,8 +430,9 @@ public final void onConnect() throws HttpException, IOException {
418430
new H2Setting(H2Param.MAX_CONCURRENT_STREAMS, localConfig.getMaxConcurrentStreams()),
419431
new H2Setting(H2Param.INITIAL_WINDOW_SIZE, localConfig.getInitialWindowSize()),
420432
new H2Setting(H2Param.MAX_FRAME_SIZE, localConfig.getMaxFrameSize()),
421-
new H2Setting(H2Param.MAX_HEADER_LIST_SIZE, localConfig.getMaxHeaderListSize()));
422-
433+
new H2Setting(H2Param.MAX_HEADER_LIST_SIZE, localConfig.getMaxHeaderListSize()),
434+
// RFC 9218 MUST: advertise intent to ignore RFC 7540 priorities in the first SETTINGS
435+
new H2Setting(H2Param.SETTINGS_NO_RFC7540_PRIORITIES, 1));
423436
commitFrame(settingsFrame);
424437
localSettingState = SettingsHandshake.TRANSMITTED;
425438
maximizeWindow(0, connInputWindow);
@@ -547,10 +560,10 @@ public final void onTimeout(final Timeout timeout) throws HttpException, IOExcep
547560
final RawFrame goAway;
548561
if (localSettingState != SettingsHandshake.ACKED) {
549562
goAway = frameFactory.createGoAway(processedRemoteStreamId, H2Error.SETTINGS_TIMEOUT,
550-
"Setting timeout (" + timeout + ")");
563+
"Setting timeout (" + timeout + ")");
551564
} else {
552565
goAway = frameFactory.createGoAway(processedRemoteStreamId, H2Error.NO_ERROR,
553-
"Timeout due to inactivity (" + timeout + ")");
566+
"Timeout due to inactivity (" + timeout + ")");
554567
}
555568
commitFrame(goAway);
556569
for (final Iterator<Map.Entry<Integer, H2Stream>> it = streamMap.entrySet().iterator(); it.hasNext(); ) {
@@ -918,10 +931,10 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
918931
if ((payload.remaining() % 6) != 0) {
919932
throw new H2ConnectionException(H2Error.FRAME_SIZE_ERROR, "Invalid SETTINGS payload");
920933
}
921-
consumeSettingsFrame(payload);
922-
remoteSettingState = SettingsHandshake.TRANSMITTED;
934+
consumeSettingsFrame(payload); // inside: set remoteNoH2Priorities if NO_RFC7540=1 seen
923935
}
924-
// Send ACK
936+
serverSettingsSeen = true;
937+
remoteSettingState = SettingsHandshake.TRANSMITTED;
925938
final RawFrame response = frameFactory.createSettingsAck();
926939
commitFrame(response);
927940
remoteSettingState = SettingsHandshake.ACKED;
@@ -1021,9 +1034,21 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
10211034
}
10221035
ioSession.setEvent(SelectionKey.OP_WRITE);
10231036
break;
1037+
case PRIORITY_UPDATE: {
1038+
onPriorityUpdateFrame(frame);
1039+
}
1040+
break;
1041+
default:
1042+
break;
10241043
}
10251044
}
10261045

1046+
protected void onPriorityUpdateFrame(final RawFrame frame) throws H2ConnectionException {
1047+
parsePriorityUpdatePayload(frame); // allows empty PFV
1048+
// At this layer we don't need to parse the dictionary; unknown/ext params are fine.
1049+
// Apply 'u.priorityFieldValue' to your local scheduler as needed; no errors for empty.
1050+
}
1051+
10271052
private void consumeDataFrame(final RawFrame frame, final H2Stream stream) throws HttpException, IOException {
10281053
if (stream.isRemoteClosed()) {
10291054
throw new H2StreamResetException(H2Error.STREAM_CLOSED, "Stream already closed");
@@ -1093,7 +1118,6 @@ private void consumeHeaderFrame(final RawFrame frame, final H2Stream stream) thr
10931118
}
10941119
final ByteBuffer payload = frame.getPayloadContent();
10951120
if (frame.isFlagSet(FrameFlag.PRIORITY)) {
1096-
// Priority not supported
10971121
payload.getInt();
10981122
payload.get();
10991123
}
@@ -1193,9 +1217,21 @@ private void consumeSettingsFrame(final ByteBuffer payload) throws IOException {
11931217
throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, ex.getMessage());
11941218
}
11951219
break;
1220+
case SETTINGS_NO_RFC7540_PRIORITIES:
1221+
if (value != 0 && value != 1) {
1222+
throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Invalid value for SETTINGS_NO_RFC7540_PRIORITIES");
1223+
}
1224+
if (serverSettingsSeen && remoteNoH2Priorities != (value == 1)) {
1225+
throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "SETTINGS_NO_RFC7540_PRIORITIES changed");
1226+
}
1227+
remoteNoH2Priorities = value == 1;
1228+
break;
11961229
}
11971230
}
11981231
}
1232+
if (!serverSettingsSeen) {
1233+
serverSettingsSeen = true;
1234+
}
11991235
applyRemoteSettings(configBuilder.build());
12001236
}
12011237

@@ -1336,6 +1372,27 @@ void appendState(final StringBuilder buf) {
13361372
.append(", processedRemoteStreamId=").append(processedRemoteStreamId);
13371373
}
13381374

1375+
private boolean isPriorityUpdateAllowed() {
1376+
// pre-SETTINGS: allowed; post-SETTINGS: only if peer said "1"
1377+
return !serverSettingsSeen || remoteNoH2Priorities;
1378+
}
1379+
1380+
private void sendPriorityUpdateInternal(final int prioritizedStreamId,
1381+
final String priorityFieldValue) throws IOException {
1382+
if (priorityFieldValue == null) {
1383+
return;
1384+
}
1385+
if (!isPriorityUpdateAllowed()) {
1386+
return; // suppressed per RFC 9218 §2.1.1
1387+
}
1388+
final byte[] ascii = priorityFieldValue.getBytes(StandardCharsets.US_ASCII);
1389+
final ByteBuffer payload = ByteBuffer.allocate(4 + ascii.length);
1390+
payload.putInt(prioritizedStreamId & 0x7FFFFFFF);
1391+
payload.put(ascii);
1392+
payload.flip();
1393+
writeExtensionFrame(FrameType.PRIORITY_UPDATE.getValue(), 0, 0, payload);
1394+
}
1395+
13391396
private static class Continuation {
13401397

13411398
final int streamId;
@@ -1418,6 +1475,12 @@ public void submit(final List<Header> headers, final boolean endStream) throws I
14181475
if (localEndStream) {
14191476
return;
14201477
}
1478+
for (final Header h : headers) {
1479+
if ("priority".equalsIgnoreCase(h.getName())) {
1480+
sendPriorityUpdateInternal(id, h.getValue());
1481+
break;
1482+
}
1483+
}
14211484
idle = false;
14221485
commitHeaders(id, headers, endStream);
14231486
if (endStream) {
@@ -1746,4 +1809,43 @@ public String toString() {
17461809

17471810
}
17481811

1749-
}
1812+
/**
1813+
* RFC 9218 parsing helper for PRIORITY_UPDATE.
1814+
* - Validates streamId == 0 (HTTP/2 control stream)
1815+
* - Enforces payload length >= 4 (31-bit stream ID); allows exactly 4 (empty Priority Field Value)
1816+
* - Returns ASCII Priority Field Value as-is (unknown/ext parameters preserved).
1817+
*
1818+
* Base class does NOT call this by default; a server-side subclass can call it from
1819+
* {@link #onPriorityUpdateFrame(RawFrame)} to accept client PRIORITY_UPDATE frames.
1820+
*/
1821+
protected final PrioritizedUpdate parsePriorityUpdatePayload(final RawFrame frame) throws H2ConnectionException {
1822+
if (frame.getStreamId() != 0) {
1823+
throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "PRIORITY_UPDATE must use stream 0");
1824+
}
1825+
final ByteBuffer payload = frame.getPayloadContent();
1826+
if (payload == null || payload.remaining() < 4) {
1827+
throw new H2ConnectionException(H2Error.FRAME_SIZE_ERROR, "Invalid PRIORITY_UPDATE payload");
1828+
}
1829+
final int prioritizedId = payload.getInt() & 0x7FFF_FFFF;
1830+
1831+
// Allow empty Priority Field Value (payload length exactly 4) => "all defaults"
1832+
final String fieldValue;
1833+
if (payload.hasRemaining()) {
1834+
// Preserve as-is (unknown/ext params are ignored by us but kept intact)
1835+
fieldValue = StandardCharsets.US_ASCII.decode(payload.slice()).toString();
1836+
} else {
1837+
fieldValue = "";
1838+
}
1839+
return new PrioritizedUpdate(prioritizedId, fieldValue);
1840+
}
1841+
1842+
protected static final class PrioritizedUpdate {
1843+
public final int prioritizedStreamId;
1844+
public final String priorityFieldValue; // may be empty => "all defaults"
1845+
PrioritizedUpdate(final int prioritizedStreamId, final String priorityFieldValue) {
1846+
this.prioritizedStreamId = prioritizedStreamId;
1847+
this.priorityFieldValue = priorityFieldValue != null ? priorityFieldValue : "";
1848+
}
1849+
}
1850+
1851+
}

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-

0 commit comments

Comments
 (0)