Skip to content

Commit e1d5baf

Browse files
committed
HTTP/2: RFC 9218 Priority support
Add PRIORITY_UPDATE (0x10) and SETTINGS_NO_RFC7540_PRIORITIES (0x9). Client emits before HEADERS on opt-in; server accepts and applies. Wire into multiplexer + Priority header utils + tests.
1 parent e38ef45 commit e1d5baf

File tree

20 files changed

+1917
-23
lines changed

20 files changed

+1917
-23
lines changed

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

Lines changed: 17 additions & 9 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

74-
}
82+
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,9 @@ public RawFrame createWindowUpdate(final int streamId, final int increment) {
111111
return new RawFrame(FrameType.WINDOW_UPDATE.getValue(), 0, streamId, payload);
112112
}
113113

114+
public RawFrame createPriorityUpdate(final ByteBuffer payload) {
115+
// type 0x10, flags 0, streamId 0 (connection control stream)
116+
return new RawFrame(FrameType.PRIORITY_UPDATE.getValue(), 0, 0, payload);
117+
}
118+
114119
}

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

Lines changed: 21 additions & 8 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,37 @@ 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

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

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

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,13 @@
3131
import java.nio.BufferOverflowException;
3232
import java.nio.ByteBuffer;
3333
import java.nio.channels.SelectionKey;
34+
import java.nio.charset.StandardCharsets;
3435
import java.util.Deque;
3536
import java.util.Iterator;
3637
import java.util.List;
38+
import java.util.Map;
3739
import java.util.Queue;
40+
import java.util.concurrent.ConcurrentHashMap;
3841
import java.util.concurrent.ConcurrentLinkedDeque;
3942
import java.util.concurrent.ConcurrentLinkedQueue;
4043
import java.util.concurrent.atomic.AtomicInteger;
@@ -66,6 +69,7 @@
6669
import org.apache.hc.core5.http.nio.command.ShutdownCommand;
6770
import org.apache.hc.core5.http.protocol.HttpContext;
6871
import org.apache.hc.core5.http.protocol.HttpProcessor;
72+
import org.apache.hc.core5.http.HttpHeaders;
6973
import org.apache.hc.core5.http2.H2ConnectionException;
7074
import org.apache.hc.core5.http2.H2Error;
7175
import org.apache.hc.core5.http2.H2StreamResetException;
@@ -83,6 +87,9 @@
8387
import org.apache.hc.core5.http2.nio.AsyncPingHandler;
8488
import org.apache.hc.core5.http2.nio.command.PingCommand;
8589
import org.apache.hc.core5.http2.nio.command.PushResponseCommand;
90+
import org.apache.hc.core5.http2.priority.PriorityParamsParser;
91+
import org.apache.hc.core5.http2.priority.PriorityValue;
92+
import org.apache.hc.core5.http2.priority.PriorityFormatter;
8693
import org.apache.hc.core5.io.CloseMode;
8794
import org.apache.hc.core5.reactor.Command;
8895
import org.apache.hc.core5.reactor.ProtocolIOSession;
@@ -94,7 +101,7 @@
94101

95102
abstract class AbstractH2StreamMultiplexer implements Identifiable, HttpConnection {
96103

97-
private static final long CONNECTION_WINDOW_LOW_MARK = 10 * 1024 * 1024; // 10 MiB
104+
private static final long CONNECTION_WINDOW_LOW_MARK = 10 * 1024 * 1024;
98105

99106
enum ConnectionHandshake { READY, ACTIVE, GRACEFUL_SHUTDOWN, SHUTDOWN }
100107
enum SettingsHandshake { READY, TRANSMITTED, ACKED }
@@ -133,6 +140,9 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED }
133140
private EndpointDetails endpointDetails;
134141
private boolean goAwayReceived;
135142

143+
private final Map<Integer, PriorityValue> priorities = new ConcurrentHashMap<>();
144+
private volatile boolean peerNoRfc7540Priorities;
145+
136146
AbstractH2StreamMultiplexer(
137147
final ProtocolIOSession ioSession,
138148
final FrameFactory frameFactory,
@@ -892,15 +902,13 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
892902
consumeSettingsFrame(payload);
893903
remoteSettingState = SettingsHandshake.TRANSMITTED;
894904
}
895-
// Send ACK
896905
final RawFrame response = frameFactory.createSettingsAck();
897906
commitFrame(response);
898907
remoteSettingState = SettingsHandshake.ACKED;
899908
}
900909
}
901910
break;
902911
case PRIORITY:
903-
// Stream priority not supported
904912
break;
905913
case PUSH_PROMISE: {
906914
acceptPushFrame();
@@ -985,6 +993,29 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
985993
}
986994
ioSession.setEvent(SelectionKey.OP_WRITE);
987995
break;
996+
case PRIORITY_UPDATE: {
997+
if (streamId != 0) {
998+
throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "PRIORITY_UPDATE must be on stream 0");
999+
}
1000+
final ByteBuffer payload = frame.getPayload();
1001+
if (payload == null || payload.remaining() < 4) {
1002+
throw new H2ConnectionException(H2Error.FRAME_SIZE_ERROR, "Invalid PRIORITY_UPDATE payload");
1003+
}
1004+
final int prioritizedId = payload.getInt() & 0x7fffffff;
1005+
final int len = payload.remaining();
1006+
final String field;
1007+
if (len > 0) {
1008+
final byte[] b = new byte[len];
1009+
payload.get(b);
1010+
field = new String(b, StandardCharsets.US_ASCII);
1011+
} else {
1012+
field = "";
1013+
}
1014+
final PriorityValue pv = PriorityParamsParser.parse(field).toValueWithDefaults();
1015+
priorities.put(prioritizedId, pv);
1016+
requestSessionOutput();
1017+
}
1018+
break;
9881019
}
9891020
}
9901021

@@ -1049,7 +1080,6 @@ private void consumeHeaderFrame(final RawFrame frame, final H2Stream stream) thr
10491080
}
10501081
final ByteBuffer payload = frame.getPayloadContent();
10511082
if (frame.isFlagSet(FrameFlag.PRIORITY)) {
1052-
// Priority not supported
10531083
payload.getInt();
10541084
payload.get();
10551085
}
@@ -1058,6 +1088,7 @@ private void consumeHeaderFrame(final RawFrame frame, final H2Stream stream) thr
10581088
if (streamListener != null) {
10591089
streamListener.onHeaderInput(this, streamId, headers);
10601090
}
1091+
recordPriorityFromHeaders(streamId, headers);
10611092
stream.consumeHeader(headers, frame.isFlagSet(FrameFlag.END_STREAM));
10621093
} else {
10631094
continuation.copyPayload(payload);
@@ -1076,6 +1107,7 @@ private void consumeContinuationFrame(final RawFrame frame, final H2Stream strea
10761107
if (streamListener != null) {
10771108
streamListener.onHeaderInput(this, streamId, headers);
10781109
}
1110+
recordPriorityFromHeaders(streamId, headers);
10791111
if (continuation.type == FrameType.PUSH_PROMISE.getValue()) {
10801112
stream.consumePromise(headers);
10811113
} else {
@@ -1132,6 +1164,9 @@ private void consumeSettingsFrame(final ByteBuffer payload) throws IOException {
11321164
throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, ex.getMessage());
11331165
}
11341166
break;
1167+
case SETTINGS_NO_RFC7540_PRIORITIES:
1168+
peerNoRfc7540Priorities = value == 1;
1169+
break;
11351170
}
11361171
}
11371172
}
@@ -1324,6 +1359,38 @@ H2Stream createStream(final H2StreamChannel channel, final H2StreamHandler strea
13241359
return streams.createActive(channel, streamHandler);
13251360
}
13261361

1362+
public final void sendPriorityUpdate(final int prioritizedStreamId, final PriorityValue value) throws IOException {
1363+
if (value == null) {
1364+
return;
1365+
}
1366+
final String field = PriorityFormatter.format(value);
1367+
if (field == null) {
1368+
return;
1369+
}
1370+
final byte[] ascii = field.getBytes(StandardCharsets.US_ASCII);
1371+
final ByteArrayBuffer buf = new ByteArrayBuffer(4 + ascii.length);
1372+
buf.append((byte) (prioritizedStreamId >> 24));
1373+
buf.append((byte) (prioritizedStreamId >> 16));
1374+
buf.append((byte) (prioritizedStreamId >> 8));
1375+
buf.append((byte) prioritizedStreamId);
1376+
buf.append(ascii, 0, ascii.length);
1377+
final RawFrame frame = frameFactory.createPriorityUpdate(ByteBuffer.wrap(buf.array(), 0, buf.length()));
1378+
commitFrame(frame);
1379+
}
1380+
1381+
private void recordPriorityFromHeaders(final int streamId, final List<? extends Header> headers) {
1382+
if (headers == null || headers.isEmpty()) {
1383+
return;
1384+
}
1385+
for (final Header h : headers) {
1386+
if (HttpHeaders.PRIORITY.equalsIgnoreCase(h.getName())) {
1387+
final PriorityValue pv = PriorityParamsParser.parse(h.getValue()).toValueWithDefaults();
1388+
priorities.put(streamId, pv);
1389+
break;
1390+
}
1391+
}
1392+
}
1393+
13271394
class H2StreamChannelImpl implements H2StreamChannel {
13281395

13291396
private final int id;
@@ -1371,6 +1438,25 @@ public void submit(final List<Header> headers, final boolean endStream) throws I
13711438
return;
13721439
}
13731440
ensureNotClosed();
1441+
if (peerNoRfc7540Priorities && streams.isSameSide(id)) {
1442+
for (final Header h : headers) {
1443+
if (HttpHeaders.PRIORITY.equalsIgnoreCase(h.getName())) {
1444+
final byte[] ascii = h.getValue() != null
1445+
? h.getValue().getBytes(StandardCharsets.US_ASCII)
1446+
: new byte[0];
1447+
final ByteArrayBuffer b = new ByteArrayBuffer(4 + ascii.length);
1448+
b.append((byte) (id >> 24));
1449+
b.append((byte) (id >> 16));
1450+
b.append((byte) (id >> 8));
1451+
b.append((byte) id);
1452+
b.append(ascii, 0, ascii.length);
1453+
final ByteBuffer pl = ByteBuffer.wrap(b.array(), 0, b.length());
1454+
final RawFrame priUpd = new RawFrame(FrameType.PRIORITY_UPDATE.getValue(), 0, 0, pl);
1455+
commitFrameInternal(priUpd);
1456+
break;
1457+
}
1458+
}
1459+
}
13741460
commitHeaders(id, headers, endStream);
13751461
if (endStream) {
13761462
localClosed = true;
@@ -1508,4 +1594,4 @@ public String toString() {
15081594

15091595
}
15101596

1511-
}
1597+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,8 @@ H2Setting[] generateSettings(final H2Config localConfig) {
103103
new H2Setting(H2Param.MAX_CONCURRENT_STREAMS, localConfig.getMaxConcurrentStreams()),
104104
new H2Setting(H2Param.INITIAL_WINDOW_SIZE, localConfig.getInitialWindowSize()),
105105
new H2Setting(H2Param.MAX_FRAME_SIZE, localConfig.getMaxFrameSize()),
106-
new H2Setting(H2Param.MAX_HEADER_LIST_SIZE, localConfig.getMaxHeaderListSize())
106+
new H2Setting(H2Param.MAX_HEADER_LIST_SIZE, localConfig.getMaxHeaderListSize()),
107+
new H2Setting(H2Param.SETTINGS_NO_RFC7540_PRIORITIES, 1)
107108
};
108109
}
109110

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
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.priority;
28+
29+
30+
import java.util.ArrayList;
31+
import java.util.List;
32+
33+
import org.apache.hc.core5.annotation.Internal;
34+
35+
/**
36+
* Formats PriorityValue as RFC 9218 Structured Fields Dictionary.
37+
* Only emits non-defaults: u when != 3, i when true.
38+
* Returns null when both are defaults (callers should omit the header then).
39+
*/
40+
@Internal
41+
public final class PriorityFormatter {
42+
43+
private PriorityFormatter() {
44+
}
45+
46+
public static String format(final PriorityValue value) {
47+
if (value == null) {
48+
return null;
49+
}
50+
final List<String> parts = new ArrayList<>(2);
51+
if (value.getUrgency() != PriorityValue.DEFAULT_URGENCY) {
52+
parts.add("u=" + value.getUrgency());
53+
}
54+
if (value.isIncremental()) {
55+
// In SF Dictionary, boolean true can be represented by key without value (per RFC 8941).
56+
parts.add("i");
57+
}
58+
if (parts.isEmpty()) {
59+
return null; // omit header when all defaults
60+
}
61+
return String.join(", ", parts);
62+
}
63+
}

0 commit comments

Comments
 (0)