Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 9f700c9

Browse files
HTTP/2: RFC 9218 Priority support (#552)
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 c21d60a commit 9f700c9

20 files changed

+1943
-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;
@@ -68,6 +71,7 @@
6871
import org.apache.hc.core5.http.nio.command.StaleCheckCommand;
6972
import org.apache.hc.core5.http.protocol.HttpContext;
7073
import org.apache.hc.core5.http.protocol.HttpProcessor;
74+
import org.apache.hc.core5.http.HttpHeaders;
7175
import org.apache.hc.core5.http2.H2ConnectionException;
7276
import org.apache.hc.core5.http2.H2Error;
7377
import org.apache.hc.core5.http2.H2StreamResetException;
@@ -85,6 +89,9 @@
8589
import org.apache.hc.core5.http2.nio.AsyncPingHandler;
8690
import org.apache.hc.core5.http2.nio.command.PingCommand;
8791
import org.apache.hc.core5.http2.nio.command.PushResponseCommand;
92+
import org.apache.hc.core5.http2.priority.PriorityParamsParser;
93+
import org.apache.hc.core5.http2.priority.PriorityValue;
94+
import org.apache.hc.core5.http2.priority.PriorityFormatter;
8895
import org.apache.hc.core5.io.CloseMode;
8996
import org.apache.hc.core5.reactor.Command;
9097
import org.apache.hc.core5.reactor.ProtocolIOSession;
@@ -96,7 +103,7 @@
96103

97104
abstract class AbstractH2StreamMultiplexer implements Identifiable, HttpConnection {
98105

99-
private static final long CONNECTION_WINDOW_LOW_MARK = 10 * 1024 * 1024; // 10 MiB
106+
private static final long CONNECTION_WINDOW_LOW_MARK = 10 * 1024 * 1024;
100107

101108
enum ConnectionHandshake { READY, ACTIVE, GRACEFUL_SHUTDOWN, SHUTDOWN }
102109
enum SettingsHandshake { READY, TRANSMITTED, ACKED }
@@ -135,6 +142,9 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED }
135142
private EndpointDetails endpointDetails;
136143
private boolean goAwayReceived;
137144

145+
private final Map<Integer, PriorityValue> priorities = new ConcurrentHashMap<>();
146+
private volatile boolean peerNoRfc7540Priorities;
147+
138148
AbstractH2StreamMultiplexer(
139149
final ProtocolIOSession ioSession,
140150
final FrameFactory frameFactory,
@@ -902,15 +912,13 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
902912
consumeSettingsFrame(payload);
903913
remoteSettingState = SettingsHandshake.TRANSMITTED;
904914
}
905-
// Send ACK
906915
final RawFrame response = frameFactory.createSettingsAck();
907916
commitFrame(response);
908917
remoteSettingState = SettingsHandshake.ACKED;
909918
}
910919
}
911920
break;
912921
case PRIORITY:
913-
// Stream priority not supported
914922
break;
915923
case PUSH_PROMISE: {
916924
acceptPushFrame();
@@ -995,6 +1003,29 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
9951003
}
9961004
ioSession.setEvent(SelectionKey.OP_WRITE);
9971005
break;
1006+
case PRIORITY_UPDATE: {
1007+
if (streamId != 0) {
1008+
throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "PRIORITY_UPDATE must be on stream 0");
1009+
}
1010+
final ByteBuffer payload = frame.getPayload();
1011+
if (payload == null || payload.remaining() < 4) {
1012+
throw new H2ConnectionException(H2Error.FRAME_SIZE_ERROR, "Invalid PRIORITY_UPDATE payload");
1013+
}
1014+
final int prioritizedId = payload.getInt() & 0x7fffffff;
1015+
final int len = payload.remaining();
1016+
final String field;
1017+
if (len > 0) {
1018+
final byte[] b = new byte[len];
1019+
payload.get(b);
1020+
field = new String(b, StandardCharsets.US_ASCII);
1021+
} else {
1022+
field = "";
1023+
}
1024+
final PriorityValue pv = PriorityParamsParser.parse(field).toValueWithDefaults();
1025+
priorities.put(prioritizedId, pv);
1026+
requestSessionOutput();
1027+
}
1028+
break;
9981029
}
9991030
}
10001031

@@ -1059,7 +1090,6 @@ private void consumeHeaderFrame(final RawFrame frame, final H2Stream stream) thr
10591090
}
10601091
final ByteBuffer payload = frame.getPayloadContent();
10611092
if (frame.isFlagSet(FrameFlag.PRIORITY)) {
1062-
// Priority not supported
10631093
payload.getInt();
10641094
payload.get();
10651095
}
@@ -1068,6 +1098,7 @@ private void consumeHeaderFrame(final RawFrame frame, final H2Stream stream) thr
10681098
if (streamListener != null) {
10691099
streamListener.onHeaderInput(this, streamId, headers);
10701100
}
1101+
recordPriorityFromHeaders(streamId, headers);
10711102
stream.consumeHeader(headers, frame.isFlagSet(FrameFlag.END_STREAM));
10721103
} else {
10731104
continuation.copyPayload(payload);
@@ -1086,6 +1117,7 @@ private void consumeContinuationFrame(final RawFrame frame, final H2Stream strea
10861117
if (streamListener != null) {
10871118
streamListener.onHeaderInput(this, streamId, headers);
10881119
}
1120+
recordPriorityFromHeaders(streamId, headers);
10891121
if (continuation.type == FrameType.PUSH_PROMISE.getValue()) {
10901122
stream.consumePromise(headers);
10911123
} else {
@@ -1142,6 +1174,9 @@ private void consumeSettingsFrame(final ByteBuffer payload) throws IOException {
11421174
throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, ex.getMessage());
11431175
}
11441176
break;
1177+
case SETTINGS_NO_RFC7540_PRIORITIES:
1178+
peerNoRfc7540Priorities = value == 1;
1179+
break;
11451180
}
11461181
}
11471182
}
@@ -1334,6 +1369,38 @@ H2Stream createStream(final H2StreamChannel channel, final H2StreamHandler strea
13341369
return streams.createActive(channel, streamHandler);
13351370
}
13361371

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

13391406
private final int id;
@@ -1381,6 +1448,25 @@ public void submit(final List<Header> headers, final boolean endStream) throws I
13811448
return;
13821449
}
13831450
ensureNotClosed();
1451+
if (peerNoRfc7540Priorities && streams.isSameSide(id)) {
1452+
for (final Header h : headers) {
1453+
if (HttpHeaders.PRIORITY.equalsIgnoreCase(h.getName())) {
1454+
final byte[] ascii = h.getValue() != null
1455+
? h.getValue().getBytes(StandardCharsets.US_ASCII)
1456+
: new byte[0];
1457+
final ByteArrayBuffer b = new ByteArrayBuffer(4 + ascii.length);
1458+
b.append((byte) (id >> 24));
1459+
b.append((byte) (id >> 16));
1460+
b.append((byte) (id >> 8));
1461+
b.append((byte) id);
1462+
b.append(ascii, 0, ascii.length);
1463+
final ByteBuffer pl = ByteBuffer.wrap(b.array(), 0, b.length());
1464+
final RawFrame priUpd = new RawFrame(FrameType.PRIORITY_UPDATE.getValue(), 0, 0, pl);
1465+
commitFrameInternal(priUpd);
1466+
break;
1467+
}
1468+
}
1469+
}
13841470
commitHeaders(id, headers, endStream);
13851471
if (endStream) {
13861472
localClosed = true;
@@ -1518,4 +1604,4 @@ public String toString() {
15181604

15191605
}
15201606

1521-
}
1607+
}

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)