From eec3174dc8e885f2a0e7e9b1456875ee986eddbb Mon Sep 17 00:00:00 2001 From: Peter Marsh Date: Thu, 6 Aug 2026 16:48:52 +0200 Subject: [PATCH 1/2] netty: Add option to disable HPACK dynamic table Add client and server builder controls that disable HPACK dynamic table use in both directions. Advertise a zero header table size and keep the encoder table pinned at zero when the peer changes its setting. Add encoder, handler, builder, transport, and interoperability coverage. AI assistance: OpenAI Codex (GPT-5) was used to review these HPACK changes and strengthen the tests. --- .../grpc/netty/GrpcHttp2HeadersEncoder.java | 52 +++++++ .../io/grpc/netty/NettyChannelBuilder.java | 22 +++ .../io/grpc/netty/NettyClientHandler.java | 10 +- .../io/grpc/netty/NettyClientTransport.java | 4 + .../main/java/io/grpc/netty/NettyServer.java | 4 + .../io/grpc/netty/NettyServerBuilder.java | 17 +++ .../io/grpc/netty/NettyServerHandler.java | 10 +- .../io/grpc/netty/NettyServerTransport.java | 4 + .../netty/GrpcHttp2HeadersEncoderTest.java | 89 ++++++++++++ .../netty/HpackDynamicTableInteropTest.java | 128 ++++++++++++++++++ .../grpc/netty/NettyChannelBuilderTest.java | 7 + .../io/grpc/netty/NettyClientHandlerTest.java | 29 ++++ .../grpc/netty/NettyClientTransportTest.java | 4 + .../io/grpc/netty/NettyHandlerTestBase.java | 6 + .../io/grpc/netty/NettyServerBuilderTest.java | 5 + .../io/grpc/netty/NettyServerHandlerTest.java | 20 +++ .../java/io/grpc/netty/NettyServerTest.java | 7 + 17 files changed, 412 insertions(+), 6 deletions(-) create mode 100644 netty/src/main/java/io/grpc/netty/GrpcHttp2HeadersEncoder.java create mode 100644 netty/src/test/java/io/grpc/netty/GrpcHttp2HeadersEncoderTest.java create mode 100644 netty/src/test/java/io/grpc/netty/HpackDynamicTableInteropTest.java diff --git a/netty/src/main/java/io/grpc/netty/GrpcHttp2HeadersEncoder.java b/netty/src/main/java/io/grpc/netty/GrpcHttp2HeadersEncoder.java new file mode 100644 index 00000000000..ce747dbb8f2 --- /dev/null +++ b/netty/src/main/java/io/grpc/netty/GrpcHttp2HeadersEncoder.java @@ -0,0 +1,52 @@ +/* + * Copyright 2026 The gRPC Authors + * + * Licensed 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. + */ + +package io.grpc.netty; + +import io.netty.handler.codec.http2.DefaultHttp2HeadersEncoder; +import io.netty.handler.codec.http2.Http2Exception; +import io.netty.handler.codec.http2.Http2HeadersEncoder; + +/** HTTP/2 headers encoder with gRPC's HPACK configuration. */ +final class GrpcHttp2HeadersEncoder extends DefaultHttp2HeadersEncoder { + private static final int DEFAULT_DYNAMIC_TABLE_ARRAY_SIZE_HINT = 16; + private static final int MIN_DYNAMIC_TABLE_ARRAY_SIZE_HINT = 2; + + private final boolean disableDynamicTable; + + GrpcHttp2HeadersEncoder(boolean disableDynamicTable) { + super( + Http2HeadersEncoder.NEVER_SENSITIVE, + false, + disableDynamicTable + ? MIN_DYNAMIC_TABLE_ARRAY_SIZE_HINT : DEFAULT_DYNAMIC_TABLE_ARRAY_SIZE_HINT, + Integer.MAX_VALUE); + this.disableDynamicTable = disableDynamicTable; + if (disableDynamicTable) { + try { + super.maxHeaderTableSize(0); + } catch (Http2Exception e) { + // Zero is always a valid HPACK dynamic table size. + throw new AssertionError(e); + } + } + } + + @Override + public void maxHeaderTableSize(long max) throws Http2Exception { + super.maxHeaderTableSize(disableDynamicTable ? 0 : max); + } +} diff --git a/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java b/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java index 8ad67f8f14e..5932b5f4f75 100644 --- a/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java +++ b/netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java @@ -105,6 +105,7 @@ public final class NettyChannelBuilder extends ForwardingChannelBuilder2 eventLoopGroupPool = DEFAULT_EVENT_LOOP_GROUP_POOL; private boolean autoFlowControl = DEFAULT_AUTO_FLOW_CONTROL; private int flowControlWindow = DEFAULT_FLOW_CONTROL_WINDOW; + private boolean disableHpackDynamicTable; private int maxHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE; private int softLimitHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE; private int maxInboundMessageSize = GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE; @@ -434,6 +435,21 @@ public NettyChannelBuilder flowControlWindow(int flowControlWindow) { return this; } + /** + * Disables use of the HPACK dynamic table for HTTP/2 header compression. + * + *

HPACK itself remains enabled, as required by HTTP/2. Static table references may still be + * used. Disabling the dynamic table reduces per-connection memory usage, but can increase the + * size of header blocks. The inbound dynamic table is disabled after the peer acknowledges the + * corresponding HTTP/2 setting, and requires a peer that correctly implements that setting. By + * default, the dynamic table is enabled. + */ + @CanIgnoreReturnValue + public NettyChannelBuilder disableHpackDynamicTable() { + disableHpackDynamicTable = true; + return this; + } + /** * Sets the maximum size of header list allowed to be received. This is cumulative size of the * headers with some overhead, as defined for @@ -626,6 +642,7 @@ ClientTransportFactory buildTransportFactory() { eventLoopGroupPool, autoFlowControl, flowControlWindow, + disableHpackDynamicTable, maxInboundMessageSize, maxHeaderListSize, softLimitHeaderListSize, @@ -769,6 +786,7 @@ private static final class NettyTransportFactory implements ClientTransportFacto private final EventLoopGroup group; private final boolean autoFlowControl; private final int flowControlWindow; + private final boolean disableHpackDynamicTable; private final int maxMessageSize; private final int maxHeaderListSize; private final int softLimitHeaderListSize; @@ -790,6 +808,7 @@ private static final class NettyTransportFactory implements ClientTransportFacto ObjectPool groupPool, boolean autoFlowControl, int flowControlWindow, + boolean disableHpackDynamicTable, int maxMessageSize, int maxHeaderListSize, int softLimitHeaderListSize, @@ -807,6 +826,7 @@ private static final class NettyTransportFactory implements ClientTransportFacto this.group = groupPool.getObject(); this.autoFlowControl = autoFlowControl; this.flowControlWindow = flowControlWindow; + this.disableHpackDynamicTable = disableHpackDynamicTable; this.maxMessageSize = maxMessageSize; this.maxHeaderListSize = maxHeaderListSize; this.softLimitHeaderListSize = softLimitHeaderListSize; @@ -856,6 +876,7 @@ public void run() { localNegotiator, autoFlowControl, flowControlWindow, + disableHpackDynamicTable, maxMessageSize, maxHeaderListSize, softLimitHeaderListSize, @@ -895,6 +916,7 @@ public SwapChannelCredentialsResult swapChannelCredentials(ChannelCredentials ch groupPool, autoFlowControl, flowControlWindow, + disableHpackDynamicTable, maxMessageSize, maxHeaderListSize, softLimitHeaderListSize, diff --git a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java index 14a1d7535ad..6ab9d2953fb 100644 --- a/netty/src/main/java/io/grpc/netty/NettyClientHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyClientHandler.java @@ -59,7 +59,6 @@ import io.netty.handler.codec.http2.DefaultHttp2ConnectionEncoder; import io.netty.handler.codec.http2.DefaultHttp2FrameReader; import io.netty.handler.codec.http2.DefaultHttp2FrameWriter; -import io.netty.handler.codec.http2.DefaultHttp2HeadersEncoder; import io.netty.handler.codec.http2.DefaultHttp2LocalFlowController; import io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController; import io.netty.handler.codec.http2.Http2CodecUtil; @@ -158,6 +157,7 @@ static NettyClientHandler newHandler( @Nullable KeepAliveManager keepAliveManager, boolean autoFlowControl, int flowControlWindow, + boolean disableHpackDynamicTable, int maxHeaderListSize, int softLimitHeaderListSize, Supplier stopwatchFactory, @@ -171,8 +171,7 @@ static NettyClientHandler newHandler( Preconditions.checkArgument(maxHeaderListSize > 0, "maxHeaderListSize must be positive"); Http2HeadersDecoder headersDecoder = new GrpcHttp2ClientHeadersDecoder(maxHeaderListSize); Http2FrameReader frameReader = new DefaultHttp2FrameReader(headersDecoder); - Http2HeadersEncoder encoder = new DefaultHttp2HeadersEncoder( - Http2HeadersEncoder.NEVER_SENSITIVE, false, 16, Integer.MAX_VALUE); + Http2HeadersEncoder encoder = new GrpcHttp2HeadersEncoder(disableHpackDynamicTable); Http2FrameWriter frameWriter = new DefaultHttp2FrameWriter(encoder); Http2Connection connection = new DefaultHttp2Connection(false); UniformStreamByteDistributor dist = new UniformStreamByteDistributor(connection); @@ -189,6 +188,7 @@ static NettyClientHandler newHandler( keepAliveManager, autoFlowControl, flowControlWindow, + disableHpackDynamicTable, maxHeaderListSize, softLimitHeaderListSize, stopwatchFactory, @@ -210,6 +210,7 @@ static NettyClientHandler newHandler( KeepAliveManager keepAliveManager, boolean autoFlowControl, int flowControlWindow, + boolean disableHpackDynamicTable, int maxHeaderListSize, int softLimitHeaderListSize, Supplier stopwatchFactory, @@ -257,6 +258,9 @@ static NettyClientHandler newHandler( settings.initialWindowSize(flowControlWindow); settings.maxConcurrentStreams(0); settings.maxHeaderListSize(maxHeaderListSize); + if (disableHpackDynamicTable) { + settings.headerTableSize(0); + } return new NettyClientHandler( decoder, diff --git a/netty/src/main/java/io/grpc/netty/NettyClientTransport.java b/netty/src/main/java/io/grpc/netty/NettyClientTransport.java index 6585df42df3..1fec68bcca7 100644 --- a/netty/src/main/java/io/grpc/netty/NettyClientTransport.java +++ b/netty/src/main/java/io/grpc/netty/NettyClientTransport.java @@ -85,6 +85,7 @@ class NettyClientTransport implements ConnectionClientTransport, private final AsciiString userAgent; private final boolean autoFlowControl; private final int flowControlWindow; + private final boolean disableHpackDynamicTable; private final int maxMessageSize; private final int maxHeaderListSize; private final int softLimitHeaderListSize; @@ -120,6 +121,7 @@ class NettyClientTransport implements ConnectionClientTransport, ProtocolNegotiator negotiator, boolean autoFlowControl, int flowControlWindow, + boolean disableHpackDynamicTable, int maxMessageSize, int maxHeaderListSize, int softLimitHeaderListSize, @@ -145,6 +147,7 @@ class NettyClientTransport implements ConnectionClientTransport, this.channelOptions = Preconditions.checkNotNull(channelOptions, "channelOptions"); this.autoFlowControl = autoFlowControl; this.flowControlWindow = flowControlWindow; + this.disableHpackDynamicTable = disableHpackDynamicTable; this.maxMessageSize = maxMessageSize; this.maxHeaderListSize = maxHeaderListSize; this.softLimitHeaderListSize = softLimitHeaderListSize; @@ -247,6 +250,7 @@ public Runnable start(Listener transportListener) { keepAliveManager, autoFlowControl, flowControlWindow, + disableHpackDynamicTable, maxHeaderListSize, softLimitHeaderListSize, GrpcUtil.STOPWATCH_SUPPLIER, diff --git a/netty/src/main/java/io/grpc/netty/NettyServer.java b/netty/src/main/java/io/grpc/netty/NettyServer.java index 2bb6b2c5921..baf3cd4809c 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServer.java +++ b/netty/src/main/java/io/grpc/netty/NettyServer.java @@ -91,6 +91,7 @@ class NettyServer implements InternalServer, InternalWithLogId { private final ChannelGroup channelGroup; private final boolean autoFlowControl; private final int flowControlWindow; + private final boolean disableHpackDynamicTable; private final int maxMessageSize; private final int maxHeaderListSize; private final int softLimitHeaderListSize; @@ -129,6 +130,7 @@ class NettyServer implements InternalServer, InternalWithLogId { int maxStreamsPerConnection, boolean autoFlowControl, int flowControlWindow, + boolean disableHpackDynamicTable, int maxMessageSize, int maxHeaderListSize, int softLimitHeaderListSize, @@ -160,6 +162,7 @@ class NettyServer implements InternalServer, InternalWithLogId { this.maxStreamsPerConnection = maxStreamsPerConnection; this.autoFlowControl = autoFlowControl; this.flowControlWindow = flowControlWindow; + this.disableHpackDynamicTable = disableHpackDynamicTable; this.maxMessageSize = maxMessageSize; this.maxHeaderListSize = maxHeaderListSize; this.softLimitHeaderListSize = softLimitHeaderListSize; @@ -265,6 +268,7 @@ public void initChannel(Channel ch) { maxStreamsPerConnection, autoFlowControl, flowControlWindow, + disableHpackDynamicTable, maxMessageSize, maxHeaderListSize, softLimitHeaderListSize, diff --git a/netty/src/main/java/io/grpc/netty/NettyServerBuilder.java b/netty/src/main/java/io/grpc/netty/NettyServerBuilder.java index 4ef14b0e933..843e9cee14f 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServerBuilder.java +++ b/netty/src/main/java/io/grpc/netty/NettyServerBuilder.java @@ -105,6 +105,7 @@ public final class NettyServerBuilder extends ForwardingServerBuilderHPACK itself remains enabled, as required by HTTP/2. Static table references may still be + * used. Disabling the dynamic table reduces per-connection memory usage, but can increase the + * size of header blocks. The inbound dynamic table is disabled after the peer acknowledges the + * corresponding HTTP/2 setting, and requires a peer that correctly implements that setting. By + * default, the dynamic table is enabled. + */ + @CanIgnoreReturnValue + public NettyServerBuilder disableHpackDynamicTable() { + disableHpackDynamicTable = true; + return this; + } + /** * Sets the maximum message size allowed to be received on the server. If not called, * defaults to 4 MiB. The default provides protection to services who haven't considered the @@ -729,6 +745,7 @@ NettyServer buildTransportServers( maxConcurrentCallsPerConnection, autoFlowControl, flowControlWindow, + disableHpackDynamicTable, maxMessageSize, maxHeaderListSize, softLimitHeaderListSize, diff --git a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java index 58166f50f7e..d082d0c69b2 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServerHandler.java +++ b/netty/src/main/java/io/grpc/netty/NettyServerHandler.java @@ -70,7 +70,6 @@ import io.netty.handler.codec.http2.DefaultHttp2FrameReader; import io.netty.handler.codec.http2.DefaultHttp2FrameWriter; import io.netty.handler.codec.http2.DefaultHttp2Headers; -import io.netty.handler.codec.http2.DefaultHttp2HeadersEncoder; import io.netty.handler.codec.http2.DefaultHttp2LocalFlowController; import io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController; import io.netty.handler.codec.http2.EmptyHttp2Headers; @@ -164,6 +163,7 @@ static NettyServerHandler newHandler( int maxStreams, boolean autoFlowControl, int flowControlWindow, + boolean disableHpackDynamicTable, int maxHeaderListSize, int softLimitHeaderListSize, int maxMessageSize, @@ -184,8 +184,7 @@ static NettyServerHandler newHandler( Http2HeadersDecoder headersDecoder = new GrpcHttp2ServerHeadersDecoder(maxHeaderListSize); Http2FrameReader frameReader = new Http2InboundFrameLogger( new DefaultHttp2FrameReader(headersDecoder), frameLogger); - Http2HeadersEncoder encoder = new DefaultHttp2HeadersEncoder( - Http2HeadersEncoder.NEVER_SENSITIVE, false, 16, Integer.MAX_VALUE); + Http2HeadersEncoder encoder = new GrpcHttp2HeadersEncoder(disableHpackDynamicTable); Http2FrameWriter frameWriter = new Http2OutboundFrameLogger(new DefaultHttp2FrameWriter(encoder), frameLogger); return newHandler( @@ -198,6 +197,7 @@ static NettyServerHandler newHandler( maxStreams, autoFlowControl, flowControlWindow, + disableHpackDynamicTable, maxHeaderListSize, softLimitHeaderListSize, maxMessageSize, @@ -225,6 +225,7 @@ static NettyServerHandler newHandler( int maxStreams, boolean autoFlowControl, int flowControlWindow, + boolean disableHpackDynamicTable, int maxHeaderListSize, int softLimitHeaderListSize, int maxMessageSize, @@ -282,6 +283,9 @@ static NettyServerHandler newHandler( settings.initialWindowSize(flowControlWindow); settings.maxConcurrentStreams(maxStreams); settings.maxHeaderListSize(maxHeaderListSize); + if (disableHpackDynamicTable) { + settings.headerTableSize(0); + } return new NettyServerHandler( channelUnused, diff --git a/netty/src/main/java/io/grpc/netty/NettyServerTransport.java b/netty/src/main/java/io/grpc/netty/NettyServerTransport.java index c0e52b75876..9829c3958b8 100644 --- a/netty/src/main/java/io/grpc/netty/NettyServerTransport.java +++ b/netty/src/main/java/io/grpc/netty/NettyServerTransport.java @@ -69,6 +69,7 @@ class NettyServerTransport implements ServerTransport { private boolean terminated; private final boolean autoFlowControl; private final int flowControlWindow; + private final boolean disableHpackDynamicTable; private final int maxMessageSize; private final int maxHeaderListSize; private final int softLimitHeaderListSize; @@ -95,6 +96,7 @@ class NettyServerTransport implements ServerTransport { int maxStreams, boolean autoFlowControl, int flowControlWindow, + boolean disableHpackDynamicTable, int maxMessageSize, int maxHeaderListSize, int softLimitHeaderListSize, @@ -118,6 +120,7 @@ class NettyServerTransport implements ServerTransport { this.maxStreams = maxStreams; this.autoFlowControl = autoFlowControl; this.flowControlWindow = flowControlWindow; + this.disableHpackDynamicTable = disableHpackDynamicTable; this.maxMessageSize = maxMessageSize; this.maxHeaderListSize = maxHeaderListSize; this.softLimitHeaderListSize = softLimitHeaderListSize; @@ -281,6 +284,7 @@ private NettyServerHandler createHandler( maxStreams, autoFlowControl, flowControlWindow, + disableHpackDynamicTable, maxHeaderListSize, softLimitHeaderListSize, maxMessageSize, diff --git a/netty/src/test/java/io/grpc/netty/GrpcHttp2HeadersEncoderTest.java b/netty/src/test/java/io/grpc/netty/GrpcHttp2HeadersEncoderTest.java new file mode 100644 index 00000000000..f23767cc2d8 --- /dev/null +++ b/netty/src/test/java/io/grpc/netty/GrpcHttp2HeadersEncoderTest.java @@ -0,0 +1,89 @@ +/* + * Copyright 2026 The gRPC Authors + * + * Licensed 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. + */ + +package io.grpc.netty; + +import static com.google.common.truth.Truth.assertThat; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.handler.codec.http2.DefaultHttp2Headers; +import io.netty.handler.codec.http2.DefaultHttp2HeadersDecoder; +import io.netty.handler.codec.http2.Http2Headers; +import io.netty.util.AsciiString; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public class GrpcHttp2HeadersEncoderTest { + private static final AsciiString CUSTOM_NAME = AsciiString.cached("custom-key"); + private static final AsciiString CUSTOM_VALUE = AsciiString.cached("custom-value"); + + @Test + public void dynamicTableEnabledByDefault() throws Exception { + GrpcHttp2HeadersEncoder encoder = new GrpcHttp2HeadersEncoder(false); + ByteBuf first = Unpooled.buffer(); + ByteBuf second = Unpooled.buffer(); + try { + Http2Headers headers = new DefaultHttp2Headers().add(CUSTOM_NAME, CUSTOM_VALUE); + + encoder.encodeHeaders(1, headers, first); + encoder.encodeHeaders(3, headers, second); + + assertThat(first.getUnsignedByte(first.readerIndex()) & 0xC0).isEqualTo(0x40); + assertThat(second.getUnsignedByte(second.readerIndex()) & 0x80).isEqualTo(0x80); + } finally { + first.release(); + second.release(); + encoder.close(); + } + } + + @Test + public void dynamicTableDisabledPermanently_staticTableStillUsed() throws Exception { + GrpcHttp2HeadersEncoder encoder = new GrpcHttp2HeadersEncoder(true); + DefaultHttp2HeadersDecoder decoder = new DefaultHttp2HeadersDecoder(); + ByteBuf first = Unpooled.buffer(); + ByteBuf second = Unpooled.buffer(); + ByteBuf staticHeader = Unpooled.buffer(); + try { + assertThat(encoder.maxHeaderTableSize()).isEqualTo(0); + encoder.maxHeaderTableSize(4096); + assertThat(encoder.maxHeaderTableSize()).isEqualTo(0); + + Http2Headers headers = new DefaultHttp2Headers().add(CUSTOM_NAME, CUSTOM_VALUE); + encoder.encodeHeaders(1, headers, first); + Http2Headers firstDecoded = decoder.decodeHeaders(1, first); + assertThat(firstDecoded.get(CUSTOM_NAME).toString()).isEqualTo(CUSTOM_VALUE.toString()); + assertThat(decoder.configuration().maxHeaderTableSize()).isEqualTo(0); + + encoder.encodeHeaders(3, headers, second); + assertThat(second.getUnsignedByte(second.readerIndex()) & 0x80).isEqualTo(0); + Http2Headers secondDecoded = decoder.decodeHeaders(3, second); + assertThat(secondDecoded.get(CUSTOM_NAME).toString()).isEqualTo(CUSTOM_VALUE.toString()); + + encoder.encodeHeaders(5, new DefaultHttp2Headers().method(AsciiString.cached("GET")), + staticHeader); + assertThat(staticHeader.getUnsignedByte(staticHeader.readerIndex())).isEqualTo(0x82); + } finally { + first.release(); + second.release(); + staticHeader.release(); + encoder.close(); + } + } +} diff --git a/netty/src/test/java/io/grpc/netty/HpackDynamicTableInteropTest.java b/netty/src/test/java/io/grpc/netty/HpackDynamicTableInteropTest.java new file mode 100644 index 00000000000..db516283892 --- /dev/null +++ b/netty/src/test/java/io/grpc/netty/HpackDynamicTableInteropTest.java @@ -0,0 +1,128 @@ +/* + * Copyright 2026 The gRPC Authors + * + * Licensed 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. + */ + +package io.grpc.netty; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.Server; +import io.grpc.ServerInterceptors; +import io.grpc.stub.MetadataUtils; +import io.grpc.stub.StreamObserver; +import io.grpc.testing.protobuf.SimpleRequest; +import io.grpc.testing.protobuf.SimpleResponse; +import io.grpc.testing.protobuf.SimpleServiceGrpc; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.After; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class HpackDynamicTableInteropTest { + private static final int RPC_COUNT = 10; + private static final Metadata.Key REQUEST_METADATA_KEY = + Metadata.Key.of("x-hpack-request", Metadata.ASCII_STRING_MARSHALLER); + private static final Metadata.Key RESPONSE_METADATA_KEY = + Metadata.Key.of("x-hpack-response", Metadata.ASCII_STRING_MARSHALLER); + private static final String REQUEST_METADATA_VALUE = "repeated-request-metadata-value"; + private static final String RESPONSE_METADATA_VALUE = "repeated-response-metadata-value"; + + @Parameters(name = "clientDisabled={0}, serverDisabled={1}") + public static Iterable data() { + return Arrays.asList(new Object[][] { + {false, false}, {false, true}, {true, false}, {true, true} + }); + } + + @Parameter(0) + public boolean clientDisabled; + + @Parameter(1) + public boolean serverDisabled; + + private Server server; + private ManagedChannel channel; + + @After + public void tearDown() throws Exception { + if (channel != null) { + channel.shutdownNow(); + channel.awaitTermination(5, TimeUnit.SECONDS); + } + if (server != null) { + server.shutdownNow(); + server.awaitTermination(5, TimeUnit.SECONDS); + } + } + + @Test + public void unaryRpcInteroperates() throws Exception { + Metadata responseMetadata = new Metadata(); + responseMetadata.put(RESPONSE_METADATA_KEY, RESPONSE_METADATA_VALUE); + NettyServerBuilder serverBuilder = NettyServerBuilder.forPort(0) + .addService( + ServerInterceptors.intercept( + new SimpleServiceImpl(), + MetadataUtils.newAttachMetadataServerInterceptor(responseMetadata))); + if (serverDisabled) { + serverBuilder.disableHpackDynamicTable(); + } + server = serverBuilder.build().start(); + + NettyChannelBuilder channelBuilder = NettyChannelBuilder + .forAddress("localhost", server.getPort()) + .usePlaintext(); + if (clientDisabled) { + channelBuilder.disableHpackDynamicTable(); + } + channel = channelBuilder.build(); + + Metadata requestMetadata = new Metadata(); + requestMetadata.put(REQUEST_METADATA_KEY, REQUEST_METADATA_VALUE); + AtomicReference headersCapture = new AtomicReference<>(); + AtomicReference trailersCapture = new AtomicReference<>(); + SimpleServiceGrpc.SimpleServiceBlockingStub stub = + SimpleServiceGrpc.newBlockingStub(channel) + .withInterceptors( + MetadataUtils.newAttachHeadersInterceptor(requestMetadata), + MetadataUtils.newCaptureMetadataInterceptor(headersCapture, trailersCapture)); + + for (int i = 0; i < RPC_COUNT; i++) { + SimpleResponse response = + stub.withDeadlineAfter(10, TimeUnit.SECONDS) + .unaryRpc(SimpleRequest.getDefaultInstance()); + assertEquals(SimpleResponse.getDefaultInstance(), response); + assertNotNull(headersCapture.get()); + assertEquals(RESPONSE_METADATA_VALUE, headersCapture.get().get(RESPONSE_METADATA_KEY)); + } + } + + private static final class SimpleServiceImpl extends SimpleServiceGrpc.SimpleServiceImplBase { + @Override + public void unaryRpc(SimpleRequest request, StreamObserver responseObserver) { + responseObserver.onNext(SimpleResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + } +} diff --git a/netty/src/test/java/io/grpc/netty/NettyChannelBuilderTest.java b/netty/src/test/java/io/grpc/netty/NettyChannelBuilderTest.java index 95d54d13b82..067b1481505 100644 --- a/netty/src/test/java/io/grpc/netty/NettyChannelBuilderTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyChannelBuilderTest.java @@ -49,6 +49,13 @@ public class NettyChannelBuilderTest { private final SslContext noSslContext = null; + @Test + public void disableHpackDynamicTableIsFluent() { + NettyChannelBuilder builder = NettyChannelBuilder.forTarget("foo"); + + assertThat(builder.disableHpackDynamicTable()).isSameInstanceAs(builder); + } + private void shutdown(ManagedChannel mc) throws Exception { mc.shutdownNow(); assertTrue(mc.awaitTermination(1, TimeUnit.SECONDS)); diff --git a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java index 9f6be9a2f3e..2661c9efca7 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientHandlerTest.java @@ -128,6 +128,7 @@ public class NettyClientHandlerTest extends NettyHandlerTestBase() { @Override @@ -228,6 +231,31 @@ public Void answer(InvocationOnMock invocation) throws Throwable { channel().releaseOutbound(); } + @Test + public void shouldAdvertiseZeroHpackDynamicTable() throws Exception { + ArgumentCaptor captor = ArgumentCaptor.forClass(Http2Settings.class); + verifyWrite().writeSettings( + any(ChannelHandlerContext.class), captor.capture(), any(ChannelPromise.class)); + + assertThat(captor.getValue().headerTableSize()).isEqualTo(0); + assertThat(frameReader().configuration().headersConfiguration().maxHeaderTableSize()) + .isEqualTo(4096); + + channelRead(serializeSettingsAck()); + + assertThat(frameReader().configuration().headersConfiguration().maxHeaderTableSize()) + .isEqualTo(0); + } + + @Test + public void shouldNotAdvertiseHpackDynamicTableSizeByDefault() { + ArgumentCaptor captor = ArgumentCaptor.forClass(Http2Settings.class); + verifyWrite().writeSettings( + any(ChannelHandlerContext.class), captor.capture(), any(ChannelPromise.class)); + + assertThat(captor.getValue().headerTableSize()).isNull(); + } + @Test @SuppressWarnings("InlineMeInliner") public void sendLargerThanSoftLimitHeaderMayFail() throws Exception { @@ -1158,6 +1186,7 @@ public Stopwatch get() { mockKeepAliveManager, false, flowControlWindow, + disableHpackDynamicTable, maxHeaderListSize, softLimitHeaderListSize, stopwatchSupplier, diff --git a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java index ef8d2e5efda..935b0580530 100644 --- a/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyClientTransportTest.java @@ -237,6 +237,7 @@ public void setSoLingerChannelOption() throws IOException, GeneralSecurityExcept newNegotiator(), false, DEFAULT_WINDOW_SIZE, + false, DEFAULT_MAX_MESSAGE_SIZE, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, @@ -513,6 +514,7 @@ public void failingToConstructChannelShouldFailGracefully() throws Exception { newNegotiator(), false, DEFAULT_WINDOW_SIZE, + false, DEFAULT_MAX_MESSAGE_SIZE, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE, @@ -1147,6 +1149,7 @@ private NettyClientTransport newTransport(ProtocolNegotiator negotiator, int max negotiator, false, DEFAULT_WINDOW_SIZE, + false, maxMsgSize, maxHeaderListSize, maxHeaderListSize, @@ -1196,6 +1199,7 @@ private void startServer(int maxStreamsPerConnection, int maxHeaderListSize, maxStreamsPerConnection, false, DEFAULT_WINDOW_SIZE, + false, DEFAULT_MAX_MESSAGE_SIZE, maxHeaderListSize, maxHeaderListSize, diff --git a/netty/src/test/java/io/grpc/netty/NettyHandlerTestBase.java b/netty/src/test/java/io/grpc/netty/NettyHandlerTestBase.java index c971294fbb6..66082f91334 100644 --- a/netty/src/test/java/io/grpc/netty/NettyHandlerTestBase.java +++ b/netty/src/test/java/io/grpc/netty/NettyHandlerTestBase.java @@ -322,6 +322,12 @@ protected final ByteBuf serializeSettings(Http2Settings settings) { return captureWrite(ctx); } + protected final ByteBuf serializeSettingsAck() { + ChannelHandlerContext ctx = newMockContext(); + new DefaultHttp2FrameWriter().writeSettingsAck(ctx, newPromise()); + return captureWrite(ctx); + } + protected final ByteBuf windowUpdate(int streamId, int delta) { ChannelHandlerContext ctx = newMockContext(); new DefaultHttp2FrameWriter().writeWindowUpdate(ctx, streamId, delta, newPromise()); diff --git a/netty/src/test/java/io/grpc/netty/NettyServerBuilderTest.java b/netty/src/test/java/io/grpc/netty/NettyServerBuilderTest.java index f3b73a515b5..f8ceca0f757 100644 --- a/netty/src/test/java/io/grpc/netty/NettyServerBuilderTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyServerBuilderTest.java @@ -40,6 +40,11 @@ public class NettyServerBuilderTest { private NettyServerBuilder builder = NettyServerBuilder.forPort(8080); + @Test + public void disableHpackDynamicTableIsFluent() { + assertThat(builder.disableHpackDynamicTable()).isSameInstanceAs(builder); + } + @Test public void addMultipleListenAddresses() { builder.addListenAddress(new InetSocketAddress(8081)); diff --git a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java index 84a1a48b37f..a4dcb3b55da 100644 --- a/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyServerHandlerTest.java @@ -138,6 +138,7 @@ public class NettyServerHandlerTest extends NettyHandlerTestBase captor = ArgumentCaptor.forClass(Http2Settings.class); + verifyWrite().writeSettings( + any(ChannelHandlerContext.class), captor.capture(), any(ChannelPromise.class)); + assertEquals(0, captor.getValue().headerTableSize().longValue()); + assertEquals(4096, + frameReader().configuration().headersConfiguration().maxHeaderTableSize()); + + channelRead(serializeSettingsAck()); + + assertEquals(0, frameReader().configuration().headersConfiguration().maxHeaderTableSize()); } @Test @@ -1425,6 +1444,7 @@ protected NettyServerHandler newHandler() { maxConcurrentStreams, autoFlowControl, flowControlWindow, + disableHpackDynamicTable, maxHeaderListSize, softLimitHeaderListSize, DEFAULT_MAX_MESSAGE_SIZE, diff --git a/netty/src/test/java/io/grpc/netty/NettyServerTest.java b/netty/src/test/java/io/grpc/netty/NettyServerTest.java index e81008d029e..72a974a29b8 100644 --- a/netty/src/test/java/io/grpc/netty/NettyServerTest.java +++ b/netty/src/test/java/io/grpc/netty/NettyServerTest.java @@ -149,6 +149,7 @@ class NoHandlerProtocolNegotiator implements ProtocolNegotiator { 1, // ignore false, // ignore 1, // ignore + false, // disableHpackDynamicTable 1, // ignore 1, // ignore 1, // ignore @@ -206,6 +207,7 @@ public void multiPortStartStopGet() throws Exception { 1, // ignore false, // ignore 1, // ignore + false, // disableHpackDynamicTable 1, // ignore 1, // ignore 1, // ignore @@ -286,6 +288,7 @@ public void multiPortConnections() throws Exception { 1, // ignore false, // ignore 1, // ignore + false, // disableHpackDynamicTable 1, // ignore 1, // ignore 1, // ignore @@ -354,6 +357,7 @@ public void getPort_notStarted() { 1, // ignore false, // ignore 1, // ignore + false, // disableHpackDynamicTable 1, // ignore 1, // ignore 1, // ignore @@ -435,6 +439,7 @@ class TestProtocolNegotiator implements ProtocolNegotiator { 1, // ignore false, // ignore 1, // ignore + false, // disableHpackDynamicTable 1, // ignore 1, // ignore 1, // ignore @@ -489,6 +494,7 @@ public void channelzListenSocket() throws Exception { 1, // ignore false, // ignore 1, // ignore + false, // disableHpackDynamicTable 1, // ignore 1, // ignore 1, // ignore @@ -637,6 +643,7 @@ private NettyServer getServer(List addr, EventLoopGroup ev) { 1, // ignore false, // ignore 1, // ignore + false, // disableHpackDynamicTable 1, // ignore 1, // ignore 1, // ignore From 9f1747080be4168a11af40abdcdb2b0441b3a180 Mon Sep 17 00:00:00 2001 From: Peter Marsh Date: Thu, 6 Aug 2026 16:49:29 +0200 Subject: [PATCH 2/2] okhttp: Honor peer HPACK table-size settings Apply SETTINGS_HEADER_TABLE_SIZE to the outbound HPACK writer before acknowledging it, so the next header block emits the required dynamic table size update. Stop applying the peer encoder setting to the inbound decoder. Add framed unit coverage and bidirectional OkHttp-Netty regression tests. The tests verify that repeated calls remain on one transport. AI assistance: OpenAI Codex (GPT-5) was used to implement and test this grpc-okhttp compatibility fix. --- .../HpackDynamicTableInteropTest.java | 149 ++++++++++++++++++ .../io/grpc/okhttp/internal/framed/Hpack.java | 9 +- .../io/grpc/okhttp/internal/framed/Http2.java | 7 +- .../okhttp/internal/framed/Http2Test.java | 45 ++++++ 4 files changed, 203 insertions(+), 7 deletions(-) create mode 100644 interop-testing/src/test/java/io/grpc/testing/integration/HpackDynamicTableInteropTest.java diff --git a/interop-testing/src/test/java/io/grpc/testing/integration/HpackDynamicTableInteropTest.java b/interop-testing/src/test/java/io/grpc/testing/integration/HpackDynamicTableInteropTest.java new file mode 100644 index 00000000000..b9c8983ccbc --- /dev/null +++ b/interop-testing/src/test/java/io/grpc/testing/integration/HpackDynamicTableInteropTest.java @@ -0,0 +1,149 @@ +/* + * Copyright 2026 The gRPC Authors + * + * Licensed 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. + */ + +package io.grpc.testing.integration; + +import static com.google.common.truth.Truth.assertThat; + +import io.grpc.Attributes; +import io.grpc.InsecureServerCredentials; +import io.grpc.ManagedChannel; +import io.grpc.Metadata; +import io.grpc.Server; +import io.grpc.ServerBuilder; +import io.grpc.ServerCall; +import io.grpc.ServerCallHandler; +import io.grpc.ServerInterceptor; +import io.grpc.ServerInterceptors; +import io.grpc.ServerTransportFilter; +import io.grpc.netty.NettyChannelBuilder; +import io.grpc.netty.NettyServerBuilder; +import io.grpc.okhttp.OkHttpChannelBuilder; +import io.grpc.okhttp.OkHttpServerBuilder; +import io.grpc.stub.MetadataUtils; +import io.grpc.stub.StreamObserver; +import io.grpc.testing.GrpcCleanupRule; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +/** Interoperability tests for disabling the HPACK dynamic table. */ +@RunWith(JUnit4.class) +public final class HpackDynamicTableInteropTest { + private static final int CALL_COUNT = 3; + private static final String REQUEST_METADATA_VALUE = "repeated-request-metadata-value"; + private static final String RESPONSE_METADATA_VALUE = "repeated-response-metadata-value"; + private static final Metadata.Key REQUEST_METADATA_KEY = + Metadata.Key.of("hpack-request-metadata", Metadata.ASCII_STRING_MARSHALLER); + private static final Metadata.Key RESPONSE_METADATA_KEY = + Metadata.Key.of("hpack-response-metadata", Metadata.ASCII_STRING_MARSHALLER); + private static final EmptyProtos.Empty EMPTY = EmptyProtos.Empty.getDefaultInstance(); + + @Rule public final GrpcCleanupRule grpcCleanup = new GrpcCleanupRule(); + + private final AtomicInteger serverTransportCount = new AtomicInteger(); + private final AtomicInteger requestsWithExpectedMetadata = new AtomicInteger(); + + @Test + public void defaultOkHttpClient_interoperatesWithDisabledNettyServer() throws Exception { + Server server = startServer( + NettyServerBuilder.forPort(0, InsecureServerCredentials.create()) + .disableHpackDynamicTable()); + ManagedChannel channel = grpcCleanup.register( + OkHttpChannelBuilder.forAddress("localhost", server.getPort()) + .usePlaintext() + .build()); + + makeRepeatedCalls(channel); + } + + @Test + public void disabledNettyClient_interoperatesWithDefaultOkHttpServer() throws Exception { + Server server = startServer( + OkHttpServerBuilder.forPort(0, InsecureServerCredentials.create())); + ManagedChannel channel = grpcCleanup.register( + NettyChannelBuilder.forAddress("localhost", server.getPort()) + .usePlaintext() + .disableHpackDynamicTable() + .build()); + + makeRepeatedCalls(channel); + } + + private Server startServer(ServerBuilder serverBuilder) throws Exception { + Metadata responseMetadata = new Metadata(); + responseMetadata.put(RESPONSE_METADATA_KEY, RESPONSE_METADATA_VALUE); + + Server server = serverBuilder + .addTransportFilter(new ServerTransportFilter() { + @Override + public Attributes transportReady(Attributes transportAttrs) { + serverTransportCount.incrementAndGet(); + return transportAttrs; + } + }) + .addService(ServerInterceptors.intercept( + new TestService(), + new ServerInterceptor() { + @Override + public ServerCall.Listener interceptCall( + ServerCall call, + Metadata headers, + ServerCallHandler next) { + if (REQUEST_METADATA_VALUE.equals(headers.get(REQUEST_METADATA_KEY))) { + requestsWithExpectedMetadata.incrementAndGet(); + } + return next.startCall(call, headers); + } + }, + MetadataUtils.newAttachMetadataServerInterceptor(responseMetadata))) + .build(); + return grpcCleanup.register(server).start(); + } + + private void makeRepeatedCalls(ManagedChannel channel) { + Metadata requestMetadata = new Metadata(); + requestMetadata.put(REQUEST_METADATA_KEY, REQUEST_METADATA_VALUE); + AtomicReference responseHeaders = new AtomicReference<>(); + AtomicReference responseTrailers = new AtomicReference<>(); + TestServiceGrpc.TestServiceBlockingStub stub = TestServiceGrpc.newBlockingStub(channel) + .withInterceptors( + MetadataUtils.newAttachHeadersInterceptor(requestMetadata), + MetadataUtils.newCaptureMetadataInterceptor(responseHeaders, responseTrailers)); + + for (int i = 0; i < CALL_COUNT; i++) { + assertThat(stub.withDeadlineAfter(10, TimeUnit.SECONDS).emptyCall(EMPTY)).isEqualTo(EMPTY); + assertThat(responseHeaders.get()).isNotNull(); + assertThat(responseHeaders.get().get(RESPONSE_METADATA_KEY)) + .isEqualTo(RESPONSE_METADATA_VALUE); + } + assertThat(requestsWithExpectedMetadata.get()).isEqualTo(CALL_COUNT); + assertThat(serverTransportCount.get()).isEqualTo(1); + } + + private static final class TestService extends TestServiceGrpc.TestServiceImplBase { + @Override + public void emptyCall( + EmptyProtos.Empty request, StreamObserver responseObserver) { + responseObserver.onNext(EMPTY); + responseObserver.onCompleted(); + } + } +} diff --git a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/Hpack.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/Hpack.java index 3155d6d533a..c275a27d08c 100644 --- a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/Hpack.java +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/Hpack.java @@ -155,10 +155,11 @@ int maxDynamicTableByteCount() { } /** - * Called by the reader when the peer sent {@link Settings#HEADER_TABLE_SIZE}. - * While this establishes the maximum dynamic table size, the - * {@link #maxDynamicTableByteCount} set during processing may limit the - * table size to a smaller amount. + * Updates the limit for header blocks received from the peer. This corresponds to a + * {@link Settings#HEADER_TABLE_SIZE} advertised by the local endpoint, not one received from + * the peer. While this establishes the maximum dynamic table size, the + * {@link #maxDynamicTableByteCount} set during processing may limit the table size to a smaller + * amount. *

Evicts entries or clears the table as needed. */ void headerTableSizeSetting(int headerTableSizeSetting) { diff --git a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/Http2.java b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/Http2.java index 0eb49b9f076..e2a5e0ab9ef 100644 --- a/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/Http2.java +++ b/okhttp/third_party/okhttp/main/java/io/grpc/okhttp/internal/framed/Http2.java @@ -312,9 +312,6 @@ private void readSettings(Handler handler, int length, byte flags, int streamId) settings.set(id, 0, value); } handler.settings(false, settings); - if (settings.getHeaderTableSize() >= 0) { - hpackReader.headerTableSizeSetting(settings.getHeaderTableSize()); - } } private void readPushPromise(Handler handler, int length, byte flags, int streamId) @@ -397,6 +394,10 @@ static final class Writer implements io.grpc.okhttp.internal.framed.FrameWriter @Override public synchronized void ackSettings(io.grpc.okhttp.internal.framed.Settings peerSettings) throws IOException { if (closed) throw new IOException("closed"); this.maxFrameSize = peerSettings.getMaxFrameSize(maxFrameSize); + int headerTableSize = peerSettings.getHeaderTableSize(); + if (headerTableSize >= 0) { + hpackWriter.resizeHeaderTable(headerTableSize); + } int length = 0; byte type = TYPE_SETTINGS; byte flags = FLAG_ACK; diff --git a/okhttp/third_party/okhttp/test/java/io/grpc/okhttp/internal/framed/Http2Test.java b/okhttp/third_party/okhttp/test/java/io/grpc/okhttp/internal/framed/Http2Test.java index 5631a18515d..132452254ea 100644 --- a/okhttp/third_party/okhttp/test/java/io/grpc/okhttp/internal/framed/Http2Test.java +++ b/okhttp/third_party/okhttp/test/java/io/grpc/okhttp/internal/framed/Http2Test.java @@ -20,6 +20,7 @@ import static io.grpc.okhttp.internal.framed.Http2.FLAG_PADDED; import static io.grpc.okhttp.internal.framed.Http2.TYPE_DATA; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; @@ -75,6 +76,50 @@ public void dataFramePadding() throws IOException { assertEquals(2037 - 125, bufferIn.size()); } + @Test + public void ackSettingsHeaderTableSizeZeroUpdatesWriter() throws IOException { + Buffer sink = new Buffer(); + Http2.Writer writer = new Http2.Writer(sink, true); + Settings settings = new Settings().set(Settings.HEADER_TABLE_SIZE, 0, 0); + + writer.ackSettings(settings); + assertEquals(9, sink.size()); + sink.skip(9); // SETTINGS ACK frame. + + writer.headers(false, 3, Arrays.asList(new Header("custom-key", "custom-value"))); + sink.skip(9); // HEADERS frame header. + + assertEquals(0x20, sink.readByte() & 0xff); // Dynamic table size update to zero. + } + + @Test + public void ackSettingsWithoutHeaderTableSizeDoesNotUpdateWriter() throws IOException { + Buffer sink = new Buffer(); + Http2.Writer writer = new Http2.Writer(sink, true); + + writer.ackSettings(new Settings()); + assertEquals(9, sink.size()); + sink.skip(9); // SETTINGS ACK frame. + + writer.headers(false, 3, Arrays.asList(new Header("custom-key", "custom-value"))); + sink.skip(9); // HEADERS frame header. + + assertEquals(0x40, sink.readByte() & 0xff); // Literal with incremental indexing. + } + + @Test + public void peerHeaderTableSizeDoesNotChangeInboundDecoder() throws IOException { + Buffer frames = new Buffer(); + Http2.Writer peerWriter = new Http2.Writer(frames, false); + peerWriter.settings(new Settings().set(Settings.HEADER_TABLE_SIZE, 0, 0)); + Http2.Reader reader = new Http2.Reader(frames, 4096, true); + + assertTrue(reader.nextFrame(mockHandler)); + + // The peer's setting limits our encoder; it does not limit decoding the peer's headers. + assertEquals(4096, reader.hpackReader.maxDynamicTableByteCount()); + } + private Buffer createData(int flag, int length, int paddingLength) throws IOException { Buffer sink = new Buffer(); writeLength(sink, length);