From 889c0879b4a0ad329c7c07a2bbddc2a34cfb9808 Mon Sep 17 00:00:00 2001 From: Abhishek Agrawal <81427947+AgraVator@users.noreply.github.com> Date: Thu, 15 Oct 2026 18:35:46 +0530 Subject: [PATCH] netty: Include pseudo-headers in GrpcHttp2RequestHeaders.iterator() (#13118) fixes b/556157371 --- .../io/grpc/netty/GrpcHttp2HeadersUtils.java | 28 +++++++- .../grpc/netty/GrpcHttp2HeadersUtilsTest.java | 36 ++++++++++ .../io/grpc/netty/NettyServerHandlerTest.java | 66 +++++++++++++++++++ 3 files changed, 129 insertions(+), 1 deletion(-) diff --git a/netty/src/main/java/io/grpc/netty/GrpcHttp2HeadersUtils.java b/netty/src/main/java/io/grpc/netty/GrpcHttp2HeadersUtils.java index 96c4310ae3d..8fe1e645bb7 100644 --- a/netty/src/main/java/io/grpc/netty/GrpcHttp2HeadersUtils.java +++ b/netty/src/main/java/io/grpc/netty/GrpcHttp2HeadersUtils.java @@ -306,13 +306,18 @@ protected static void appendNameAndValue(StringBuilder builder, CharSequence nam private List> namesAndValuesToImmutableList() { ArrayList> list = new ArrayList<>(values.length); + addNamesAndValuesToList(list); + return Collections.unmodifiableList(list); + } + + protected final void addNamesAndValuesToList( + List> list) { for (int i = 0; i < namesAndValuesIdx; i += 2) { String name = new String(namesAndValues[i], US_ASCII); // If binary headers, the value is base64 encoded. AsciiString value = values[i / 2]; list.add(new AbstractMap.SimpleImmutableEntry(name, value)); } - return Collections.unmodifiableList(list); } protected final String namesAndValuesToString() { @@ -494,6 +499,27 @@ public int size() { return size; } + @Override + public Iterator> iterator() { + List> list = new ArrayList<>(size()); + addIfPresent(list, PATH_HEADER, path); + addIfPresent(list, AUTHORITY_HEADER, authority); + addIfPresent(list, METHOD_HEADER, method); + addIfPresent(list, SCHEME_HEADER, scheme); + addIfPresent(list, TE_HEADER, te); + addNamesAndValuesToList(list); + return Collections.unmodifiableList(list).iterator(); + } + + private static void addIfPresent( + List> list, AsciiString name, AsciiString value) { + if (value != null) { + list.add( + new AbstractMap.SimpleImmutableEntry( + name.toString(), value)); + } + } + @Override public String toString() { StringBuilder builder = new StringBuilder(getClass().getSimpleName()).append('['); diff --git a/netty/src/test/java/io/grpc/netty/GrpcHttp2HeadersUtilsTest.java b/netty/src/test/java/io/grpc/netty/GrpcHttp2HeadersUtilsTest.java index 653b44a9d2d..9f467045cb2 100644 --- a/netty/src/test/java/io/grpc/netty/GrpcHttp2HeadersUtilsTest.java +++ b/netty/src/test/java/io/grpc/netty/GrpcHttp2HeadersUtilsTest.java @@ -328,6 +328,42 @@ public void headerIteartor_nonEmpty() { assertThat(it.hasNext()).isFalse(); } + @Test + public void headerIterator_withPseudoHeadersAndTe() { + Http2Headers http2Headers = new GrpcHttp2RequestHeaders(2); + http2Headers.add(AsciiString.of(":path"), AsciiString.of("mypath")); + http2Headers.add(AsciiString.of(":authority"), AsciiString.of("myauthority")); + http2Headers.add(AsciiString.of(":method"), AsciiString.of("mymethod")); + http2Headers.add(AsciiString.of(":scheme"), AsciiString.of("myscheme")); + http2Headers.add(AsciiString.of("te"), AsciiString.of("trailers")); + http2Headers.add(AsciiString.of("custom"), AsciiString.of("val")); + Iterator> it = http2Headers.iterator(); + + assertNextEntry(it, ":path", AsciiString.of("mypath")); + assertNextEntry(it, ":authority", AsciiString.of("myauthority")); + assertNextEntry(it, ":method", AsciiString.of("mymethod")); + assertNextEntry(it, ":scheme", AsciiString.of("myscheme")); + assertNextEntry(it, "te", AsciiString.of("trailers")); + assertNextEntry(it, "custom", AsciiString.of("val")); + assertThat(it.hasNext()).isFalse(); + } + + @Test + public void headerIterator_nullPseudoHeadersSkipped() { + Http2Headers http2Headers = new GrpcHttp2RequestHeaders(2); + http2Headers.add(AsciiString.of(":path"), AsciiString.of("mypath")); + http2Headers.add(AsciiString.of(":authority"), AsciiString.of("myauthority")); + http2Headers.remove(AsciiString.of(":authority")); + http2Headers.add(AsciiString.of(":scheme"), AsciiString.of("myscheme")); + http2Headers.add(AsciiString.of("custom"), AsciiString.of("val")); + Iterator> it = http2Headers.iterator(); + + assertNextEntry(it, ":path", AsciiString.of("mypath")); + assertNextEntry(it, ":scheme", AsciiString.of("myscheme")); + assertNextEntry(it, "custom", AsciiString.of("val")); + assertThat(it.hasNext()).isFalse(); + } + private static void assertNextEntry( Iterator> it, CharSequence key, CharSequence value) { Map.Entry entry = it.next(); diff --git a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java index 84a1a48b37f..848c8d59f43 100644 --- a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java @@ -80,6 +80,7 @@ import io.netty.channel.ChannelHandlerContext; import io.netty.channel.ChannelPromise; import io.netty.handler.codec.http.HttpHeaderNames; +import io.netty.handler.codec.http2.DefaultHttp2FrameWriter; import io.netty.handler.codec.http2.DefaultHttp2Headers; import io.netty.handler.codec.http2.Http2CodecUtil; import io.netty.handler.codec.http2.Http2Error; @@ -764,6 +765,71 @@ public void headersWithOnlyHostBecomesAuthority() throws Exception { Truth.assertThat(metadataCaptor.getValue().get(hostKey)).isNull(); } + @Test + public void trailersWithPseudoHeadersShouldFail() throws Exception { + manualSetUp(); + createStream(); + + Http2Headers trailers = new DefaultHttp2Headers() + .method(HTTP_METHOD) + .set(CONTENT_TYPE_HEADER, CONTENT_TYPE_GRPC) + .set(TE_HEADER, TE_TRAILERS) + .path(new AsciiString("/smuggled/method")); + channelRead(trailersFrame(STREAM_ID, trailers)); + + verifyWrite() + .writeRstStream( + eq(ctx()), + eq(STREAM_ID), + eq(Http2Error.PROTOCOL_ERROR.code()), + any(ChannelPromise.class)); + verify(transportListener, times(1)) + .streamCreated(any(ServerStream.class), anyString(), any(Metadata.class)); + ArgumentCaptor captor = ArgumentCaptor.forClass(Status.class); + verify(streamListener).closed(captor.capture()); + assertEquals(Code.INTERNAL, captor.getValue().getCode()); + verify(streamListener, never()).halfClosed(); + } + + @Test + public void trailersWithPseudoHeadersAfterRejectedHeadersShouldFail() throws Exception { + manualSetUp(); + Http2Headers badHeaders = new DefaultHttp2Headers() + .method(HTTP_METHOD) + .path(new AsciiString("/foo/bar")); + channelRead(headersFrame(STREAM_ID, badHeaders)); + verifyWrite() + .writeHeaders( + eq(ctx()), + eq(STREAM_ID), + any(Http2Headers.class), + eq(0), + eq(false), + any(ChannelPromise.class)); + + Http2Headers trailers = new DefaultHttp2Headers() + .method(HTTP_METHOD) + .set(CONTENT_TYPE_HEADER, CONTENT_TYPE_GRPC) + .set(TE_HEADER, TE_TRAILERS) + .path(new AsciiString("/smuggled/method")); + channelRead(trailersFrame(STREAM_ID, trailers)); + + verifyWrite() + .writeRstStream( + eq(ctx()), + eq(STREAM_ID), + eq(Http2Error.PROTOCOL_ERROR.code()), + any(ChannelPromise.class)); + verify(transportListener, never()) + .streamCreated(any(ServerStream.class), anyString(), any(Metadata.class)); + } + + private ByteBuf trailersFrame(int streamId, Http2Headers headers) { + ChannelHandlerContext ctx = newMockContext(); + new DefaultHttp2FrameWriter().writeHeaders(ctx, streamId, headers, 0, true, newPromise()); + return captureWrite(ctx); + } + @Test public void keepAliveManagerOnDataReceived_headersRead() throws Exception { manualSetUp();