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());
+    }
+
 }