You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@hc.apache.org by ab...@apache.org on 2026/09/30 04:49:43 UTC
(httpcomponents-core) branch 5.4.x updated: Reject H2 DATA on reserved streams
This is an automated email from the ASF dual-hosted git repository.
arturobernalg pushed a commit to branch 5.4.x
in repository https://gitbox.apache.org/repos/asf/httpcomponents-core.git
The following commit(s) were added to refs/heads/5.4.x by this push:
new 2613f31cd Reject H2 DATA on reserved streams
2613f31cd is described below
commit 2613f31cdd49a2ac8fccd5abc3374e3892672c39
Author: Arturo Bernal <ab...@apache.org>
AuthorDate: Tue Sep 29 13:46:06 2026 +0200
Reject H2 DATA on reserved streams
Report a connection PROTOCOL_ERROR when DATA is received
on a reserved stream.
(cherry picked from commit 814622c13b37c4eb735265ddafffb2793e3587e9)
---
.../impl/nio/AbstractH2StreamMultiplexer.java | 3 +
.../impl/nio/TestAbstractH2StreamMultiplexer.java | 67 ++++++++++++++++++++++
2 files changed, 70 insertions(+)
diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java
index bb87344af..1cc4b41fd 100644
--- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java
+++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/AbstractH2StreamMultiplexer.java
@@ -1030,6 +1030,9 @@ private void consumeFrame(final RawFrame frame) throws HttpException, IOExceptio
}
private void consumeDataFrame(final RawFrame frame, final H2Stream stream) throws HttpException, IOException {
+ if (stream.isReserved()) {
+ throw new H2ConnectionException(H2Error.PROTOCOL_ERROR, "Unexpected DATA frame on reserved stream");
+ }
if (stream.isRemoteClosed()) {
throw new H2StreamResetException(H2Error.STREAM_CLOSED, "Stream already closed");
}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java
index ec8b6d433..74c31b4c2 100644
--- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestAbstractH2StreamMultiplexer.java
@@ -60,6 +60,7 @@
import org.apache.hc.core5.http2.config.H2Setting;
import org.apache.hc.core5.http2.frame.DefaultFrameFactory;
import org.apache.hc.core5.http2.frame.FrameConsts;
+import org.apache.hc.core5.http2.frame.FrameFlag;
import org.apache.hc.core5.http2.frame.FrameFactory;
import org.apache.hc.core5.http2.frame.FrameType;
import org.apache.hc.core5.http2.frame.RawFrame;
@@ -1204,5 +1205,71 @@ private static int getMaxListSize(final HPackDecoder hPackDecoder) throws Except
return field.getInt(hPackDecoder);
}
+ @Test
+ void testDataOnReservedRemoteStreamIsConnectionProtocolError() throws Exception {
+ final H2Config h2Config = H2Config.custom()
+ .setPushEnabled(true)
+ .build();
+
+ final AbstractH2StreamMultiplexer mux = new H2StreamMultiplexerImpl(
+ protocolIOSession,
+ FRAME_FACTORY,
+ StreamIdGenerator.ODD,
+ httpProcessor,
+ CharCodingConfig.DEFAULT,
+ h2Config,
+ h2StreamListener,
+ () -> streamHandler);
+
+ // Existing client-initiated stream 1.
+ final H2StreamChannel stream1 = mux.createChannel(1);
+ mux.createStream(stream1, streamHandler);
+
+ // Receive PUSH_PROMISE for stream 2 -> stream 2 becomes reserved (remote).
+ final ByteArrayBuffer headerBuf = new ByteArrayBuffer(256);
+ final HPackEncoder encoder = new HPackEncoder(
+ H2Config.INIT.getHeaderTableSize(),
+ CharCodingSupport.createEncoder(CharCodingConfig.DEFAULT));
+
+ final List<Header> headers = Arrays.asList(
+ new BasicHeader(":method", "GET"),
+ new BasicHeader(":scheme", "https"),
+ new BasicHeader(":authority", "example.test"),
+ new BasicHeader(":path", "/pushed"));
+
+ encoder.encodeHeaders(headerBuf, headers, h2Config.isCompressionEnabled());
+
+ final ByteBuffer pushPayload = ByteBuffer.allocate(4 + headerBuf.length());
+ pushPayload.putInt(2);
+ pushPayload.put(headerBuf.array(), 0, headerBuf.length());
+ pushPayload.flip();
+
+ final RawFrame pushPromise = new RawFrame(
+ FrameType.PUSH_PROMISE.getValue(),
+ FrameFlag.END_HEADERS.getValue(),
+ 1,
+ pushPayload);
+
+ mux.onInput(ByteBuffer.wrap(encodeFrame(pushPromise)));
+
+ // DATA is illegal while stream 2 is still reserved (remote).
+ final RawFrame data = FRAME_FACTORY.createData(
+ 2,
+ ByteBuffer.wrap(new byte[] {1, 2, 3}),
+ false);
+
+ final H2ConnectionException ex = Assertions.assertThrows(
+ H2ConnectionException.class,
+ () -> mux.onInput(ByteBuffer.wrap(encodeFrame(data))));
+
+ Assertions.assertEquals(
+ H2Error.PROTOCOL_ERROR,
+ H2Error.getByCode(ex.getCode()));
+
+ Mockito.verify(streamHandler, Mockito.never()).consumeData(
+ ArgumentMatchers.any(ByteBuffer.class),
+ ArgumentMatchers.anyBoolean());
+ }
+
}