32
32
import java .nio .ByteBuffer ;
33
33
import java .nio .channels .SelectionKey ;
34
34
import java .nio .charset .CharacterCodingException ;
35
+ import java .nio .charset .StandardCharsets ;
35
36
import java .util .Deque ;
36
37
import java .util .Iterator ;
37
38
import java .util .List ;
@@ -137,6 +138,10 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED }
137
138
private EndpointDetails endpointDetails ;
138
139
private boolean goAwayReceived ;
139
140
141
+ // RFC 9218 gating state
142
+ private volatile boolean serverSettingsSeen ;
143
+ private volatile boolean remoteNoH2Priorities ;
144
+
140
145
AbstractH2StreamMultiplexer (
141
146
final ProtocolIOSession ioSession ,
142
147
final FrameFactory frameFactory ,
@@ -173,6 +178,9 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED }
173
178
174
179
this .lowMark = H2Config .INIT .getInitialWindowSize () / 2 ;
175
180
this .streamListener = streamListener ;
181
+
182
+ this .serverSettingsSeen = false ;
183
+ this .remoteNoH2Priorities = false ;
176
184
}
177
185
178
186
@ Override
@@ -188,7 +196,6 @@ public String getId() {
188
196
189
197
abstract H2StreamHandler createRemotelyInitiatedStream (
190
198
H2StreamChannel channel ,
191
-
192
199
HttpProcessor httpProcessor ,
193
200
BasicHttpConnectionMetrics connMetrics ,
194
201
HandlerFactory <AsyncPushConsumer > pushHandlerFactory ) throws IOException ;
@@ -261,6 +268,11 @@ private void commitFrame(final RawFrame frame) throws IOException {
261
268
}
262
269
}
263
270
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
+
264
276
private void commitHeaders (
265
277
final int streamId , final List <? extends Header > headers , final boolean endStream ) throws IOException {
266
278
if (streamListener != null ) {
@@ -418,8 +430,9 @@ public final void onConnect() throws HttpException, IOException {
418
430
new H2Setting (H2Param .MAX_CONCURRENT_STREAMS , localConfig .getMaxConcurrentStreams ()),
419
431
new H2Setting (H2Param .INITIAL_WINDOW_SIZE , localConfig .getInitialWindowSize ()),
420
432
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 ));
423
436
commitFrame (settingsFrame );
424
437
localSettingState = SettingsHandshake .TRANSMITTED ;
425
438
maximizeWindow (0 , connInputWindow );
@@ -547,10 +560,10 @@ public final void onTimeout(final Timeout timeout) throws HttpException, IOExcep
547
560
final RawFrame goAway ;
548
561
if (localSettingState != SettingsHandshake .ACKED ) {
549
562
goAway = frameFactory .createGoAway (processedRemoteStreamId , H2Error .SETTINGS_TIMEOUT ,
550
- "Setting timeout (" + timeout + ")" );
563
+ "Setting timeout (" + timeout + ")" );
551
564
} else {
552
565
goAway = frameFactory .createGoAway (processedRemoteStreamId , H2Error .NO_ERROR ,
553
- "Timeout due to inactivity (" + timeout + ")" );
566
+ "Timeout due to inactivity (" + timeout + ")" );
554
567
}
555
568
commitFrame (goAway );
556
569
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
918
931
if ((payload .remaining () % 6 ) != 0 ) {
919
932
throw new H2ConnectionException (H2Error .FRAME_SIZE_ERROR , "Invalid SETTINGS payload" );
920
933
}
921
- consumeSettingsFrame (payload );
922
- remoteSettingState = SettingsHandshake .TRANSMITTED ;
934
+ consumeSettingsFrame (payload ); // inside: set remoteNoH2Priorities if NO_RFC7540=1 seen
923
935
}
924
- // Send ACK
936
+ serverSettingsSeen = true ;
937
+ remoteSettingState = SettingsHandshake .TRANSMITTED ;
925
938
final RawFrame response = frameFactory .createSettingsAck ();
926
939
commitFrame (response );
927
940
remoteSettingState = SettingsHandshake .ACKED ;
@@ -1021,9 +1034,21 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
1021
1034
}
1022
1035
ioSession .setEvent (SelectionKey .OP_WRITE );
1023
1036
break ;
1037
+ case PRIORITY_UPDATE : {
1038
+ onPriorityUpdateFrame (frame );
1039
+ }
1040
+ break ;
1041
+ default :
1042
+ break ;
1024
1043
}
1025
1044
}
1026
1045
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
+
1027
1052
private void consumeDataFrame (final RawFrame frame , final H2Stream stream ) throws HttpException , IOException {
1028
1053
if (stream .isRemoteClosed ()) {
1029
1054
throw new H2StreamResetException (H2Error .STREAM_CLOSED , "Stream already closed" );
@@ -1093,7 +1118,6 @@ private void consumeHeaderFrame(final RawFrame frame, final H2Stream stream) thr
1093
1118
}
1094
1119
final ByteBuffer payload = frame .getPayloadContent ();
1095
1120
if (frame .isFlagSet (FrameFlag .PRIORITY )) {
1096
- // Priority not supported
1097
1121
payload .getInt ();
1098
1122
payload .get ();
1099
1123
}
@@ -1193,9 +1217,21 @@ private void consumeSettingsFrame(final ByteBuffer payload) throws IOException {
1193
1217
throw new H2ConnectionException (H2Error .PROTOCOL_ERROR , ex .getMessage ());
1194
1218
}
1195
1219
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 ;
1196
1229
}
1197
1230
}
1198
1231
}
1232
+ if (!serverSettingsSeen ) {
1233
+ serverSettingsSeen = true ;
1234
+ }
1199
1235
applyRemoteSettings (configBuilder .build ());
1200
1236
}
1201
1237
@@ -1336,6 +1372,27 @@ void appendState(final StringBuilder buf) {
1336
1372
.append (", processedRemoteStreamId=" ).append (processedRemoteStreamId );
1337
1373
}
1338
1374
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
+
1339
1396
private static class Continuation {
1340
1397
1341
1398
final int streamId ;
@@ -1418,6 +1475,12 @@ public void submit(final List<Header> headers, final boolean endStream) throws I
1418
1475
if (localEndStream ) {
1419
1476
return ;
1420
1477
}
1478
+ for (final Header h : headers ) {
1479
+ if ("priority" .equalsIgnoreCase (h .getName ())) {
1480
+ sendPriorityUpdateInternal (id , h .getValue ());
1481
+ break ;
1482
+ }
1483
+ }
1421
1484
idle = false ;
1422
1485
commitHeaders (id , headers , endStream );
1423
1486
if (endStream ) {
@@ -1746,4 +1809,43 @@ public String toString() {
1746
1809
1747
1810
}
1748
1811
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
+ }
0 commit comments