Skip to content

Commit 8d69aea

Browse files
petedmarshcodex
andcommitted
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. Co-authored-by: Codex <noreply@openai.com>
1 parent c9ee3f8 commit 8d69aea

17 files changed

Lines changed: 419 additions & 8 deletions
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2026 The gRPC Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package io.grpc.netty;
18+
19+
import io.netty.handler.codec.http2.DefaultHttp2HeadersEncoder;
20+
import io.netty.handler.codec.http2.Http2Exception;
21+
import io.netty.handler.codec.http2.Http2HeadersEncoder;
22+
23+
/** HTTP/2 headers encoder with gRPC's HPACK configuration. */
24+
final class GrpcHttp2HeadersEncoder extends DefaultHttp2HeadersEncoder {
25+
private static final int DEFAULT_DYNAMIC_TABLE_ARRAY_SIZE_HINT = 16;
26+
private static final int MIN_DYNAMIC_TABLE_ARRAY_SIZE_HINT = 2;
27+
28+
private final boolean disableDynamicTable;
29+
30+
GrpcHttp2HeadersEncoder(
31+
Http2HeadersEncoder.SensitivityDetector sensitivityDetector,
32+
boolean disableDynamicTable) {
33+
super(
34+
sensitivityDetector,
35+
false,
36+
disableDynamicTable
37+
? MIN_DYNAMIC_TABLE_ARRAY_SIZE_HINT : DEFAULT_DYNAMIC_TABLE_ARRAY_SIZE_HINT,
38+
Integer.MAX_VALUE);
39+
this.disableDynamicTable = disableDynamicTable;
40+
if (disableDynamicTable) {
41+
try {
42+
super.maxHeaderTableSize(0);
43+
} catch (Http2Exception e) {
44+
// Zero is always a valid HPACK dynamic table size.
45+
throw new AssertionError(e);
46+
}
47+
}
48+
}
49+
50+
@Override
51+
public void maxHeaderTableSize(long max) throws Http2Exception {
52+
super.maxHeaderTableSize(disableDynamicTable ? 0 : max);
53+
}
54+
}

‎netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ public final class NettyChannelBuilder extends ForwardingChannelBuilder2<NettyCh
110110
private boolean autoFlowControl = DEFAULT_AUTO_FLOW_CONTROL;
111111
private int flowControlWindow = DEFAULT_FLOW_CONTROL_WINDOW;
112112
private final Set<AsciiString> neverIndexedMetadataKeys = new HashSet<>();
113+
private boolean disableHpackDynamicTable;
113114
private int maxHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
114115
private int softLimitHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
115116
private int maxInboundMessageSize = GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE;
@@ -476,6 +477,21 @@ public NettyChannelBuilder neverIndexMetadataKeys(
476477
return this;
477478
}
478479

480+
/**
481+
* Disables use of the HPACK dynamic table for HTTP/2 header compression.
482+
*
483+
* <p>HPACK itself remains enabled, as required by HTTP/2. Static table references may still be
484+
* used. Disabling the dynamic table reduces per-connection memory usage, but can increase the
485+
* size of header blocks. The inbound dynamic table is disabled after the peer acknowledges the
486+
* corresponding HTTP/2 setting, and requires a peer that correctly implements that setting. By
487+
* default, the dynamic table is enabled.
488+
*/
489+
@CanIgnoreReturnValue
490+
public NettyChannelBuilder disableHpackDynamicTable() {
491+
disableHpackDynamicTable = true;
492+
return this;
493+
}
494+
479495
/**
480496
* Sets the maximum size of header list allowed to be received. This is cumulative size of the
481497
* headers with some overhead, as defined for
@@ -669,6 +685,7 @@ ClientTransportFactory buildTransportFactory() {
669685
autoFlowControl,
670686
flowControlWindow,
671687
neverIndexedMetadataKeys,
688+
disableHpackDynamicTable,
672689
maxInboundMessageSize,
673690
maxHeaderListSize,
674691
softLimitHeaderListSize,
@@ -813,6 +830,7 @@ private static final class NettyTransportFactory implements ClientTransportFacto
813830
private final boolean autoFlowControl;
814831
private final int flowControlWindow;
815832
private final Set<AsciiString> neverIndexedMetadataKeys;
833+
private final boolean disableHpackDynamicTable;
816834
private final int maxMessageSize;
817835
private final int maxHeaderListSize;
818836
private final int softLimitHeaderListSize;
@@ -835,6 +853,7 @@ private static final class NettyTransportFactory implements ClientTransportFacto
835853
boolean autoFlowControl,
836854
int flowControlWindow,
837855
Set<AsciiString> neverIndexedMetadataKeys,
856+
boolean disableHpackDynamicTable,
838857
int maxMessageSize,
839858
int maxHeaderListSize,
840859
int softLimitHeaderListSize,
@@ -854,6 +873,7 @@ private static final class NettyTransportFactory implements ClientTransportFacto
854873
this.flowControlWindow = flowControlWindow;
855874
this.neverIndexedMetadataKeys = Collections.unmodifiableSet(
856875
new HashSet<>(checkNotNull(neverIndexedMetadataKeys, "neverIndexedMetadataKeys")));
876+
this.disableHpackDynamicTable = disableHpackDynamicTable;
857877
this.maxMessageSize = maxMessageSize;
858878
this.maxHeaderListSize = maxHeaderListSize;
859879
this.softLimitHeaderListSize = softLimitHeaderListSize;
@@ -904,6 +924,7 @@ public void run() {
904924
autoFlowControl,
905925
flowControlWindow,
906926
neverIndexedMetadataKeys,
927+
disableHpackDynamicTable,
907928
maxMessageSize,
908929
maxHeaderListSize,
909930
softLimitHeaderListSize,
@@ -944,6 +965,7 @@ public SwapChannelCredentialsResult swapChannelCredentials(ChannelCredentials ch
944965
autoFlowControl,
945966
flowControlWindow,
946967
neverIndexedMetadataKeys,
968+
disableHpackDynamicTable,
947969
maxMessageSize,
948970
maxHeaderListSize,
949971
softLimitHeaderListSize,

‎netty/src/main/java/io/grpc/netty/NettyClientHandler.java‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
import io.netty.handler.codec.http2.DefaultHttp2ConnectionEncoder;
6060
import io.netty.handler.codec.http2.DefaultHttp2FrameReader;
6161
import io.netty.handler.codec.http2.DefaultHttp2FrameWriter;
62-
import io.netty.handler.codec.http2.DefaultHttp2HeadersEncoder;
6362
import io.netty.handler.codec.http2.DefaultHttp2LocalFlowController;
6463
import io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController;
6564
import io.netty.handler.codec.http2.Http2CodecUtil;
@@ -162,6 +161,7 @@ static NettyClientHandler newHandler(
162161
boolean autoFlowControl,
163162
int flowControlWindow,
164163
Set<AsciiString> neverIndexedMetadataKeys,
164+
boolean disableHpackDynamicTable,
165165
int maxHeaderListSize,
166166
int softLimitHeaderListSize,
167167
Supplier<Stopwatch> stopwatchFactory,
@@ -175,8 +175,8 @@ static NettyClientHandler newHandler(
175175
Preconditions.checkArgument(maxHeaderListSize > 0, "maxHeaderListSize must be positive");
176176
Http2HeadersDecoder headersDecoder = new GrpcHttp2ClientHeadersDecoder(maxHeaderListSize);
177177
Http2FrameReader frameReader = new DefaultHttp2FrameReader(headersDecoder);
178-
Http2HeadersEncoder encoder = new DefaultHttp2HeadersEncoder(
179-
sensitivityDetector(neverIndexedMetadataKeys), false, 16, Integer.MAX_VALUE);
178+
Http2HeadersEncoder encoder = new GrpcHttp2HeadersEncoder(
179+
sensitivityDetector(neverIndexedMetadataKeys), disableHpackDynamicTable);
180180
Http2FrameWriter frameWriter = new DefaultHttp2FrameWriter(encoder);
181181
Http2Connection connection = new DefaultHttp2Connection(false);
182182
UniformStreamByteDistributor dist = new UniformStreamByteDistributor(connection);
@@ -193,6 +193,7 @@ static NettyClientHandler newHandler(
193193
keepAliveManager,
194194
autoFlowControl,
195195
flowControlWindow,
196+
disableHpackDynamicTable,
196197
maxHeaderListSize,
197198
softLimitHeaderListSize,
198199
stopwatchFactory,
@@ -214,6 +215,7 @@ static NettyClientHandler newHandler(
214215
KeepAliveManager keepAliveManager,
215216
boolean autoFlowControl,
216217
int flowControlWindow,
218+
boolean disableHpackDynamicTable,
217219
int maxHeaderListSize,
218220
int softLimitHeaderListSize,
219221
Supplier<Stopwatch> stopwatchFactory,
@@ -261,6 +263,9 @@ static NettyClientHandler newHandler(
261263
settings.initialWindowSize(flowControlWindow);
262264
settings.maxConcurrentStreams(0);
263265
settings.maxHeaderListSize(maxHeaderListSize);
266+
if (disableHpackDynamicTable) {
267+
settings.headerTableSize(0);
268+
}
264269

265270
return new NettyClientHandler(
266271
decoder,

‎netty/src/main/java/io/grpc/netty/NettyClientTransport.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ class NettyClientTransport implements ConnectionClientTransport,
8787
private final boolean autoFlowControl;
8888
private final int flowControlWindow;
8989
private final Set<AsciiString> neverIndexedMetadataKeys;
90+
private final boolean disableHpackDynamicTable;
9091
private final int maxMessageSize;
9192
private final int maxHeaderListSize;
9293
private final int softLimitHeaderListSize;
@@ -123,6 +124,7 @@ class NettyClientTransport implements ConnectionClientTransport,
123124
boolean autoFlowControl,
124125
int flowControlWindow,
125126
Set<AsciiString> neverIndexedMetadataKeys,
127+
boolean disableHpackDynamicTable,
126128
int maxMessageSize,
127129
int maxHeaderListSize,
128130
int softLimitHeaderListSize,
@@ -150,6 +152,7 @@ class NettyClientTransport implements ConnectionClientTransport,
150152
this.flowControlWindow = flowControlWindow;
151153
this.neverIndexedMetadataKeys =
152154
Preconditions.checkNotNull(neverIndexedMetadataKeys, "neverIndexedMetadataKeys");
155+
this.disableHpackDynamicTable = disableHpackDynamicTable;
153156
this.maxMessageSize = maxMessageSize;
154157
this.maxHeaderListSize = maxHeaderListSize;
155158
this.softLimitHeaderListSize = softLimitHeaderListSize;
@@ -253,6 +256,7 @@ public Runnable start(Listener transportListener) {
253256
autoFlowControl,
254257
flowControlWindow,
255258
neverIndexedMetadataKeys,
259+
disableHpackDynamicTable,
256260
maxHeaderListSize,
257261
softLimitHeaderListSize,
258262
GrpcUtil.STOPWATCH_SUPPLIER,

‎netty/src/main/java/io/grpc/netty/NettyServer.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ class NettyServer implements InternalServer, InternalWithLogId {
9595
private final boolean autoFlowControl;
9696
private final int flowControlWindow;
9797
private final Set<AsciiString> neverIndexedMetadataKeys;
98+
private final boolean disableHpackDynamicTable;
9899
private final int maxMessageSize;
99100
private final int maxHeaderListSize;
100101
private final int softLimitHeaderListSize;
@@ -134,6 +135,7 @@ class NettyServer implements InternalServer, InternalWithLogId {
134135
boolean autoFlowControl,
135136
int flowControlWindow,
136137
Set<AsciiString> neverIndexedMetadataKeys,
138+
boolean disableHpackDynamicTable,
137139
int maxMessageSize,
138140
int maxHeaderListSize,
139141
int softLimitHeaderListSize,
@@ -167,6 +169,7 @@ class NettyServer implements InternalServer, InternalWithLogId {
167169
this.flowControlWindow = flowControlWindow;
168170
this.neverIndexedMetadataKeys = Collections.unmodifiableSet(
169171
new HashSet<>(checkNotNull(neverIndexedMetadataKeys, "neverIndexedMetadataKeys")));
172+
this.disableHpackDynamicTable = disableHpackDynamicTable;
170173
this.maxMessageSize = maxMessageSize;
171174
this.maxHeaderListSize = maxHeaderListSize;
172175
this.softLimitHeaderListSize = softLimitHeaderListSize;
@@ -273,6 +276,7 @@ public void initChannel(Channel ch) {
273276
autoFlowControl,
274277
flowControlWindow,
275278
neverIndexedMetadataKeys,
279+
disableHpackDynamicTable,
276280
maxMessageSize,
277281
maxHeaderListSize,
278282
softLimitHeaderListSize,

‎netty/src/main/java/io/grpc/netty/NettyServerBuilder.java‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ public final class NettyServerBuilder extends ForwardingServerBuilder<NettyServe
111111
private boolean autoFlowControl = true;
112112
private int flowControlWindow = DEFAULT_FLOW_CONTROL_WINDOW;
113113
private final Set<AsciiString> neverIndexedMetadataKeys = new HashSet<>();
114+
private boolean disableHpackDynamicTable;
114115
private int maxMessageSize = DEFAULT_MAX_MESSAGE_SIZE;
115116
private int maxHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
116117
private int softLimitHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
@@ -483,6 +484,21 @@ public NettyServerBuilder neverIndexMetadataKeys(
483484
return this;
484485
}
485486

487+
/**
488+
* Disables use of the HPACK dynamic table for HTTP/2 header compression.
489+
*
490+
* <p>HPACK itself remains enabled, as required by HTTP/2. Static table references may still be
491+
* used. Disabling the dynamic table reduces per-connection memory usage, but can increase the
492+
* size of header blocks. The inbound dynamic table is disabled after the peer acknowledges the
493+
* corresponding HTTP/2 setting, and requires a peer that correctly implements that setting. By
494+
* default, the dynamic table is enabled.
495+
*/
496+
@CanIgnoreReturnValue
497+
public NettyServerBuilder disableHpackDynamicTable() {
498+
disableHpackDynamicTable = true;
499+
return this;
500+
}
501+
486502
/**
487503
* Sets the maximum message size allowed to be received on the server. If not called,
488504
* defaults to 4 MiB. The default provides protection to services who haven't considered the
@@ -773,6 +789,7 @@ NettyServer buildTransportServers(
773789
autoFlowControl,
774790
flowControlWindow,
775791
neverIndexedMetadataKeys,
792+
disableHpackDynamicTable,
776793
maxMessageSize,
777794
maxHeaderListSize,
778795
softLimitHeaderListSize,

‎netty/src/main/java/io/grpc/netty/NettyServerHandler.java‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@
7070
import io.netty.handler.codec.http2.DefaultHttp2FrameReader;
7171
import io.netty.handler.codec.http2.DefaultHttp2FrameWriter;
7272
import io.netty.handler.codec.http2.DefaultHttp2Headers;
73-
import io.netty.handler.codec.http2.DefaultHttp2HeadersEncoder;
7473
import io.netty.handler.codec.http2.DefaultHttp2LocalFlowController;
7574
import io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController;
7675
import io.netty.handler.codec.http2.EmptyHttp2Headers;
@@ -166,6 +165,7 @@ static NettyServerHandler newHandler(
166165
boolean autoFlowControl,
167166
int flowControlWindow,
168167
Set<AsciiString> neverIndexedMetadataKeys,
168+
boolean disableHpackDynamicTable,
169169
int maxHeaderListSize,
170170
int softLimitHeaderListSize,
171171
int maxMessageSize,
@@ -186,11 +186,9 @@ static NettyServerHandler newHandler(
186186
Http2HeadersDecoder headersDecoder = new GrpcHttp2ServerHeadersDecoder(maxHeaderListSize);
187187
Http2FrameReader frameReader = new Http2InboundFrameLogger(
188188
new DefaultHttp2FrameReader(headersDecoder), frameLogger);
189-
Http2HeadersEncoder encoder = new DefaultHttp2HeadersEncoder(
189+
Http2HeadersEncoder encoder = new GrpcHttp2HeadersEncoder(
190190
NettyClientHandler.sensitivityDetector(neverIndexedMetadataKeys),
191-
false,
192-
16,
193-
Integer.MAX_VALUE);
191+
disableHpackDynamicTable);
194192
Http2FrameWriter frameWriter =
195193
new Http2OutboundFrameLogger(new DefaultHttp2FrameWriter(encoder), frameLogger);
196194
return newHandler(
@@ -203,6 +201,7 @@ static NettyServerHandler newHandler(
203201
maxStreams,
204202
autoFlowControl,
205203
flowControlWindow,
204+
disableHpackDynamicTable,
206205
maxHeaderListSize,
207206
softLimitHeaderListSize,
208207
maxMessageSize,
@@ -230,6 +229,7 @@ static NettyServerHandler newHandler(
230229
int maxStreams,
231230
boolean autoFlowControl,
232231
int flowControlWindow,
232+
boolean disableHpackDynamicTable,
233233
int maxHeaderListSize,
234234
int softLimitHeaderListSize,
235235
int maxMessageSize,
@@ -287,6 +287,9 @@ static NettyServerHandler newHandler(
287287
settings.initialWindowSize(flowControlWindow);
288288
settings.maxConcurrentStreams(maxStreams);
289289
settings.maxHeaderListSize(maxHeaderListSize);
290+
if (disableHpackDynamicTable) {
291+
settings.headerTableSize(0);
292+
}
290293

291294
return new NettyServerHandler(
292295
channelUnused,

‎netty/src/main/java/io/grpc/netty/NettyServerTransport.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class NettyServerTransport implements ServerTransport {
7272
private final boolean autoFlowControl;
7373
private final int flowControlWindow;
7474
private final Set<AsciiString> neverIndexedMetadataKeys;
75+
private final boolean disableHpackDynamicTable;
7576
private final int maxMessageSize;
7677
private final int maxHeaderListSize;
7778
private final int softLimitHeaderListSize;
@@ -99,6 +100,7 @@ class NettyServerTransport implements ServerTransport {
99100
boolean autoFlowControl,
100101
int flowControlWindow,
101102
Set<AsciiString> neverIndexedMetadataKeys,
103+
boolean disableHpackDynamicTable,
102104
int maxMessageSize,
103105
int maxHeaderListSize,
104106
int softLimitHeaderListSize,
@@ -124,6 +126,7 @@ class NettyServerTransport implements ServerTransport {
124126
this.flowControlWindow = flowControlWindow;
125127
this.neverIndexedMetadataKeys =
126128
Preconditions.checkNotNull(neverIndexedMetadataKeys, "neverIndexedMetadataKeys");
129+
this.disableHpackDynamicTable = disableHpackDynamicTable;
127130
this.maxMessageSize = maxMessageSize;
128131
this.maxHeaderListSize = maxHeaderListSize;
129132
this.softLimitHeaderListSize = softLimitHeaderListSize;
@@ -288,6 +291,7 @@ private NettyServerHandler createHandler(
288291
autoFlowControl,
289292
flowControlWindow,
290293
neverIndexedMetadataKeys,
294+
disableHpackDynamicTable,
291295
maxHeaderListSize,
292296
softLimitHeaderListSize,
293297
maxMessageSize,

0 commit comments

Comments
 (0)