From 1aa2b55cad736b548813f9b28ca7e278a61fff8e Mon Sep 17 00:00:00 2001 From: Arturo Bernal Date: Wed, 30 Sep 2026 17:48:50 +0200 Subject: [PATCH] HTTP/2: Correct pushed response message handling Handle informational responses and trailers in pushed responses. Validate END_STREAM and Content-Length at message completion. --- .../impl/nio/ClientPushH2StreamHandler.java | 17 ++++ .../nio/TestClientPushH2StreamHandler.java | 96 +++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientPushH2StreamHandler.java b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientPushH2StreamHandler.java index 280be311f..157d84289 100644 --- a/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientPushH2StreamHandler.java +++ b/httpcore5-h2/src/main/java/org/apache/hc/core5/http2/impl/nio/ClientPushH2StreamHandler.java @@ -36,6 +36,7 @@ import org.apache.hc.core5.http.HttpException; import org.apache.hc.core5.http.HttpRequest; import org.apache.hc.core5.http.HttpResponse; +import org.apache.hc.core5.http.HttpStatus; import org.apache.hc.core5.http.HttpVersion; import org.apache.hc.core5.http.ProtocolException; import org.apache.hc.core5.http.impl.BasicHttpConnectionMetrics; @@ -140,6 +141,14 @@ public void consumeHeader(final List
headers, final boolean endStream) t Asserts.notNull(exchangeHandler, "Exchange handler"); final HttpResponse response = DefaultH2ResponseConverter.INSTANCE.convert(headers); + final int status = response.getCode(); + + if (status < HttpStatus.SC_SUCCESS) { + if (endStream) { + throw new ProtocolException("Informational response must not set END_STREAM"); + } + return; + } if (MessageSupport.canResponseHaveBody(response)) { declaredContentLen = MessageSupport.getContentLength(response); @@ -161,6 +170,14 @@ public void consumeHeader(final List
headers, final boolean endStream) t } else { responseState = MessageState.BODY; } + } else if (responseState == MessageState.BODY) { + if (!endStream) { + throw new ProtocolException("Trailer headers must set END_STREAM"); + } + TrailersValidationSupport.verify(headers); + validateContentLength(); + responseState = MessageState.COMPLETE; + exchangeHandler.streamEnd(headers); } else { throw new ProtocolException("Unexpected message headers"); } diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientPushH2StreamHandler.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientPushH2StreamHandler.java index 8fe51fc24..6b7df25a6 100644 --- a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientPushH2StreamHandler.java +++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientPushH2StreamHandler.java @@ -145,4 +145,100 @@ void contentLengthInvalidNoBody() throws Exception { handler.consumeHeader(responseHeaders, true)); } + + @Test + void informationalPushResponseBeforeFinalResponseAccepted() throws Exception { + Mockito.when(pushHandlerFactory.create(Mockito.any(), Mockito.any())).thenReturn(pushConsumer); + handler.consumePromise(Arrays.asList( + new BasicHeader(":method", "GET"), + new BasicHeader(":scheme", "https"), + new BasicHeader(":authority", "example.com"), + new BasicHeader(":path", "/"))); + + handler.consumeHeader(Arrays.asList( + new BasicHeader(":status", "103")), false); + + Mockito.verify(pushConsumer, Mockito.never()).consumePromise( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + + handler.consumeHeader(Arrays.asList( + new BasicHeader(":status", "200")), true); + + Mockito.verify(pushConsumer).consumePromise( + Mockito.any(), Mockito.any(), Mockito.any(), Mockito.any()); + Mockito.verify(pushConsumer).streamEnd(null); + } + + @Test + void informationalPushResponseWithEndStreamRejected() throws Exception { + Mockito.when(pushHandlerFactory.create(Mockito.any(), Mockito.any())).thenReturn(pushConsumer); + handler.consumePromise(Arrays.asList( + new BasicHeader(":method", "GET"), + new BasicHeader(":scheme", "https"), + new BasicHeader(":authority", "example.com"), + new BasicHeader(":path", "/"))); + + Assertions.assertThrows(ProtocolException.class, () -> + handler.consumeHeader(Arrays.asList( + new BasicHeader(":status", "103")), true)); + } + + @Test + void pushResponseTrailersAccepted() throws Exception { + Mockito.when(pushHandlerFactory.create(Mockito.any(), Mockito.any())).thenReturn(pushConsumer); + handler.consumePromise(Arrays.asList( + new BasicHeader(":method", "GET"), + new BasicHeader(":scheme", "https"), + new BasicHeader(":authority", "example.com"), + new BasicHeader(":path", "/"))); + + handler.consumeHeader(Arrays.asList( + new BasicHeader(":status", "200"), + new BasicHeader("content-length", "2")), false); + handler.consumeData(ByteBuffer.wrap(new byte[] { 0, 1 }), false); + + final List
trailers = Arrays.asList( + new BasicHeader("x-checksum", "abc123")); + handler.consumeHeader(trailers, true); + + Mockito.verify(pushConsumer).streamEnd(trailers); + } + + @Test + void pushResponseTrailersWithoutEndStreamRejected() throws Exception { + Mockito.when(pushHandlerFactory.create(Mockito.any(), Mockito.any())).thenReturn(pushConsumer); + handler.consumePromise(Arrays.asList( + new BasicHeader(":method", "GET"), + new BasicHeader(":scheme", "https"), + new BasicHeader(":authority", "example.com"), + new BasicHeader(":path", "/"))); + + handler.consumeHeader(Arrays.asList( + new BasicHeader(":status", "200")), false); + + Assertions.assertThrows(ProtocolException.class, () -> + handler.consumeHeader(Arrays.asList( + new BasicHeader("x-checksum", "abc123")), false)); + } + + @Test + void pushResponseContentLengthMismatchWithTrailersRejected() throws Exception { + Mockito.when(pushHandlerFactory.create(Mockito.any(), Mockito.any())).thenReturn(pushConsumer); + handler.consumePromise(Arrays.asList( + new BasicHeader(":method", "GET"), + new BasicHeader(":scheme", "https"), + new BasicHeader(":authority", "example.com"), + new BasicHeader(":path", "/"))); + + handler.consumeHeader(Arrays.asList( + new BasicHeader(":status", "200"), + new BasicHeader("content-length", "3")), false); + handler.consumeData(ByteBuffer.wrap(new byte[] { 0, 1 }), false); + + Assertions.assertThrows(ProtocolException.class, () -> + handler.consumeHeader(Arrays.asList( + new BasicHeader("x-checksum", "abc123")), true)); + } + + }