Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public enum H2Param {
MAX_CONCURRENT_STREAMS(0x3),
INITIAL_WINDOW_SIZE(0x4),
MAX_FRAME_SIZE(0x5),
MAX_HEADER_LIST_SIZE(0x6);
MAX_HEADER_LIST_SIZE(0x6),
SETTINGS_NO_RFC7540_PRIORITIES (0x9);

int code;

Expand All @@ -50,25 +51,32 @@ public int getCode() {
return code;
}

private static final H2Param[] LOOKUP_TABLE = new H2Param[6];
private static final H2Param[] LOOKUP_TABLE;
static {
for (final H2Param param: H2Param.values()) {
LOOKUP_TABLE[param.code - 1] = param;
int max = 0;
for (final H2Param p : H2Param.values()) {
if (p.code > max) {
max = p.code;
}
}
LOOKUP_TABLE = new H2Param[max + 1];
for (final H2Param p : H2Param.values()) {
LOOKUP_TABLE[p.code] = p;
}
}

public static H2Param valueOf(final int code) {
if (code < 1 || code > LOOKUP_TABLE.length) {
if (code < 0 || code >= LOOKUP_TABLE.length) {
return null;
}
return LOOKUP_TABLE[code - 1];
return LOOKUP_TABLE[code];
}

public static String toString(final int code) {
if (code < 1 || code > LOOKUP_TABLE.length) {
if (code < 0 || code >= LOOKUP_TABLE.length || LOOKUP_TABLE[code] == null) {
return Integer.toString(code);
}
return LOOKUP_TABLE[code - 1].name();
return LOOKUP_TABLE[code].name();
}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,9 @@ public RawFrame createWindowUpdate(final int streamId, final int increment) {
return new RawFrame(FrameType.WINDOW_UPDATE.getValue(), 0, streamId, payload);
}

public RawFrame createPriorityUpdate(final ByteBuffer payload) {
// type 0x10, flags 0, streamId 0 (connection control stream)
return new RawFrame(FrameType.PRIORITY_UPDATE.getValue(), 0, 0, payload);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ public enum FrameType {
PING(0x06),
GOAWAY(0x07),
WINDOW_UPDATE(0x08),
CONTINUATION(0x09);
CONTINUATION(0x09),
PRIORITY_UPDATE(0x10); // 16

int value;
final int value;

FrameType(final int value) {
this.value = value;
Expand All @@ -54,25 +55,37 @@ public int getValue() {
return value;
}

private static final FrameType[] LOOKUP_TABLE = new FrameType[10];
private static final FrameType[] LOOKUP_TABLE;
static {
for (final FrameType frameType: FrameType.values()) {
LOOKUP_TABLE[frameType.value] = frameType;
int max = -1;
for (final FrameType t : FrameType.values()) {
if (t.value > max) {
max = t.value;
}
}
LOOKUP_TABLE = new FrameType[max + 1];
for (final FrameType t : FrameType.values()) {
LOOKUP_TABLE[t.value] = t;
}
}

public static FrameType valueOf(final int value) {
if (value < 0 || value >= LOOKUP_TABLE.length) {
return null;
}
return LOOKUP_TABLE[value];
return LOOKUP_TABLE[value]; // may be null for gaps (e.g., 0x0A..0x0F)
}

public static String toString(final int value) {
if (value < 0 || value >= LOOKUP_TABLE.length) {
return Integer.toString(value);
}
return LOOKUP_TABLE[value].name();
final FrameType t = LOOKUP_TABLE[value];
return t != null ? t.name() : Integer.toString(value);
}

}
/** Convenience: compare this enum to a raw frame type byte. */
public boolean same(final int rawType) {
return this.value == rawType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,13 @@
import java.nio.BufferOverflowException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.charset.StandardCharsets;
import java.util.Deque;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedDeque;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicInteger;
Expand Down Expand Up @@ -66,6 +69,7 @@
import org.apache.hc.core5.http.nio.command.ShutdownCommand;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.http.protocol.HttpProcessor;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http2.H2ConnectionException;
import org.apache.hc.core5.http2.H2Error;
import org.apache.hc.core5.http2.H2StreamResetException;
Expand All @@ -83,6 +87,9 @@
import org.apache.hc.core5.http2.nio.AsyncPingHandler;
import org.apache.hc.core5.http2.nio.command.PingCommand;
import org.apache.hc.core5.http2.nio.command.PushResponseCommand;
import org.apache.hc.core5.http2.priority.PriorityParamsParser;
import org.apache.hc.core5.http2.priority.PriorityValue;
import org.apache.hc.core5.http2.priority.PriorityFormatter;
import org.apache.hc.core5.io.CloseMode;
import org.apache.hc.core5.reactor.Command;
import org.apache.hc.core5.reactor.ProtocolIOSession;
Expand All @@ -94,7 +101,7 @@

abstract class AbstractH2StreamMultiplexer implements Identifiable, HttpConnection {

private static final long CONNECTION_WINDOW_LOW_MARK = 10 * 1024 * 1024; // 10 MiB
private static final long CONNECTION_WINDOW_LOW_MARK = 10 * 1024 * 1024;

enum ConnectionHandshake { READY, ACTIVE, GRACEFUL_SHUTDOWN, SHUTDOWN }
enum SettingsHandshake { READY, TRANSMITTED, ACKED }
Expand Down Expand Up @@ -133,6 +140,9 @@ enum SettingsHandshake { READY, TRANSMITTED, ACKED }
private EndpointDetails endpointDetails;
private boolean goAwayReceived;

private final Map<Integer, PriorityValue> priorities = new ConcurrentHashMap<>();
private volatile boolean peerNoRfc7540Priorities;

AbstractH2StreamMultiplexer(
final ProtocolIOSession ioSession,
final FrameFactory frameFactory,
Expand Down Expand Up @@ -892,15 +902,13 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
consumeSettingsFrame(payload);
remoteSettingState = SettingsHandshake.TRANSMITTED;
}
// Send ACK
final RawFrame response = frameFactory.createSettingsAck();
commitFrame(response);
remoteSettingState = SettingsHandshake.ACKED;
}
}
break;
case PRIORITY:
// Stream priority not supported
break;
case PUSH_PROMISE: {
acceptPushFrame();
Expand Down Expand Up @@ -985,6 +993,29 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
}
ioSession.setEvent(SelectionKey.OP_WRITE);
break;
case PRIORITY_UPDATE: {
if (streamId != 0) {
throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "PRIORITY_UPDATE must be on stream 0");
}
final ByteBuffer payload = frame.getPayload();
if (payload == null || payload.remaining() < 4) {
throw new H2ConnectionException(H2Error.FRAME_SIZE_ERROR, "Invalid PRIORITY_UPDATE payload");
}
final int prioritizedId = payload.getInt() & 0x7fffffff;
final int len = payload.remaining();
final String field;
if (len > 0) {
final byte[] b = new byte[len];
payload.get(b);
field = new String(b, StandardCharsets.US_ASCII);
} else {
field = "";
}
final PriorityValue pv = PriorityParamsParser.parse(field).toValueWithDefaults();
priorities.put(prioritizedId, pv);
requestSessionOutput();
}
break;
}
}

Expand Down Expand Up @@ -1049,7 +1080,6 @@ private void consumeHeaderFrame(final RawFrame frame, final H2Stream stream) thr
}
final ByteBuffer payload = frame.getPayloadContent();
if (frame.isFlagSet(FrameFlag.PRIORITY)) {
// Priority not supported
payload.getInt();
payload.get();
}
Expand All @@ -1058,6 +1088,7 @@ private void consumeHeaderFrame(final RawFrame frame, final H2Stream stream) thr
if (streamListener != null) {
streamListener.onHeaderInput(this, streamId, headers);
}
recordPriorityFromHeaders(streamId, headers);
stream.consumeHeader(headers, frame.isFlagSet(FrameFlag.END_STREAM));
} else {
continuation.copyPayload(payload);
Expand All @@ -1076,6 +1107,7 @@ private void consumeContinuationFrame(final RawFrame frame, final H2Stream strea
if (streamListener != null) {
streamListener.onHeaderInput(this, streamId, headers);
}
recordPriorityFromHeaders(streamId, headers);
if (continuation.type == FrameType.PUSH_PROMISE.getValue()) {
stream.consumePromise(headers);
} else {
Expand Down Expand Up @@ -1132,6 +1164,9 @@ private void consumeSettingsFrame(final ByteBuffer payload) throws IOException {
throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, ex.getMessage());
}
break;
case SETTINGS_NO_RFC7540_PRIORITIES:
peerNoRfc7540Priorities = value == 1;
break;
}
}
}
Expand Down Expand Up @@ -1324,6 +1359,38 @@ H2Stream createStream(final H2StreamChannel channel, final H2StreamHandler strea
return streams.createActive(channel, streamHandler);
}

public final void sendPriorityUpdate(final int prioritizedStreamId, final PriorityValue value) throws IOException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arturobernalg By the way this method does not seem to be used anywhere. Is this intended?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ok2c Yes, that was intentional. The logic was inlined into the stream submit path, so the standalone helper became redundant. I'm going to removed it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arturobernalg Please also remove H2RequestPriority interceptor for now. It is not being used anywhere in core and I presume your intention was to use it in client. In this case the interceptor should be in client.

Copy link
Member

@ok2c ok2c Oct 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arturobernalg Also PriorityFormatter is not being used anywhere in core. If they are not used in core they should not be in core. Re-submit them in client. Believe me you do not want to depend on a new release of core to be able to fix a bug in client.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@arturobernalg Please let me know if you can live with #567. The functionality should be moved to client and we should put more work into the API design

if (value == null) {
return;
}
final String field = PriorityFormatter.format(value);
if (field == null) {
return;
}
final byte[] ascii = field.getBytes(StandardCharsets.US_ASCII);
final ByteArrayBuffer buf = new ByteArrayBuffer(4 + ascii.length);
buf.append((byte) (prioritizedStreamId >> 24));
buf.append((byte) (prioritizedStreamId >> 16));
buf.append((byte) (prioritizedStreamId >> 8));
buf.append((byte) prioritizedStreamId);
buf.append(ascii, 0, ascii.length);
final RawFrame frame = frameFactory.createPriorityUpdate(ByteBuffer.wrap(buf.array(), 0, buf.length()));
commitFrame(frame);
}

private void recordPriorityFromHeaders(final int streamId, final List<? extends Header> headers) {
if (headers == null || headers.isEmpty()) {
return;
}
for (final Header h : headers) {
if (HttpHeaders.PRIORITY.equalsIgnoreCase(h.getName())) {
final PriorityValue pv = PriorityParamsParser.parse(h.getValue()).toValueWithDefaults();
priorities.put(streamId, pv);
break;
}
}
}

class H2StreamChannelImpl implements H2StreamChannel {

private final int id;
Expand Down Expand Up @@ -1371,6 +1438,25 @@ public void submit(final List<Header> headers, final boolean endStream) throws I
return;
}
ensureNotClosed();
if (peerNoRfc7540Priorities && streams.isSameSide(id)) {
for (final Header h : headers) {
if (HttpHeaders.PRIORITY.equalsIgnoreCase(h.getName())) {
final byte[] ascii = h.getValue() != null
? h.getValue().getBytes(StandardCharsets.US_ASCII)
: new byte[0];
final ByteArrayBuffer b = new ByteArrayBuffer(4 + ascii.length);
b.append((byte) (id >> 24));
b.append((byte) (id >> 16));
b.append((byte) (id >> 8));
b.append((byte) id);
b.append(ascii, 0, ascii.length);
final ByteBuffer pl = ByteBuffer.wrap(b.array(), 0, b.length());
final RawFrame priUpd = new RawFrame(FrameType.PRIORITY_UPDATE.getValue(), 0, 0, pl);
commitFrameInternal(priUpd);
break;
}
}
}
commitHeaders(id, headers, endStream);
if (endStream) {
localClosed = true;
Expand Down Expand Up @@ -1508,4 +1594,4 @@ public String toString() {

}

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,8 @@ H2Setting[] generateSettings(final H2Config localConfig) {
new H2Setting(H2Param.MAX_CONCURRENT_STREAMS, localConfig.getMaxConcurrentStreams()),
new H2Setting(H2Param.INITIAL_WINDOW_SIZE, localConfig.getInitialWindowSize()),
new H2Setting(H2Param.MAX_FRAME_SIZE, localConfig.getMaxFrameSize()),
new H2Setting(H2Param.MAX_HEADER_LIST_SIZE, localConfig.getMaxHeaderListSize())
new H2Setting(H2Param.MAX_HEADER_LIST_SIZE, localConfig.getMaxHeaderListSize()),
new H2Setting(H2Param.SETTINGS_NO_RFC7540_PRIORITIES, 1)
};
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.core5.http2.priority;


import java.util.ArrayList;
import java.util.List;

import org.apache.hc.core5.annotation.Internal;

/**
* Formats PriorityValue as RFC 9218 Structured Fields Dictionary.
* Only emits non-defaults: u when != 3, i when true.
* Returns null when both are defaults (callers should omit the header then).
*/
@Internal
public final class PriorityFormatter {

private PriorityFormatter() {
}

public static String format(final PriorityValue value) {
if (value == null) {
return null;
}
final List<String> parts = new ArrayList<>(2);
if (value.getUrgency() != PriorityValue.DEFAULT_URGENCY) {
parts.add("u=" + value.getUrgency());
}
if (value.isIncremental()) {
// In SF Dictionary, boolean true can be represented by key without value (per RFC 8941).
parts.add("i");
}
if (parts.isEmpty()) {
return null; // omit header when all defaults
}
return String.join(", ", parts);
}
}
Loading