Skip to content

Commit eec3174

Browse files
committed
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.
1 parent 6b2d978 commit eec3174

17 files changed

Lines changed: 412 additions & 6 deletions
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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(boolean disableDynamicTable) {
31+
super(
32+
Http2HeadersEncoder.NEVER_SENSITIVE,
33+
false,
34+
disableDynamicTable
35+
? MIN_DYNAMIC_TABLE_ARRAY_SIZE_HINT : DEFAULT_DYNAMIC_TABLE_ARRAY_SIZE_HINT,
36+
Integer.MAX_VALUE);
37+
this.disableDynamicTable = disableDynamicTable;
38+
if (disableDynamicTable) {
39+
try {
40+
super.maxHeaderTableSize(0);
41+
} catch (Http2Exception e) {
42+
// Zero is always a valid HPACK dynamic table size.
43+
throw new AssertionError(e);
44+
}
45+
}
46+
}
47+
48+
@Override
49+
public void maxHeaderTableSize(long max) throws Http2Exception {
50+
super.maxHeaderTableSize(disableDynamicTable ? 0 : max);
51+
}
52+
}

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ public final class NettyChannelBuilder extends ForwardingChannelBuilder2<NettyCh
105105
private ObjectPool<? extends EventLoopGroup> eventLoopGroupPool = DEFAULT_EVENT_LOOP_GROUP_POOL;
106106
private boolean autoFlowControl = DEFAULT_AUTO_FLOW_CONTROL;
107107
private int flowControlWindow = DEFAULT_FLOW_CONTROL_WINDOW;
108+
private boolean disableHpackDynamicTable;
108109
private int maxHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
109110
private int softLimitHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
110111
private int maxInboundMessageSize = GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE;
@@ -434,6 +435,21 @@ public NettyChannelBuilder flowControlWindow(int flowControlWindow) {
434435
return this;
435436
}
436437

438+
/**
439+
* Disables use of the HPACK dynamic table for HTTP/2 header compression.
440+
*
441+
* <p>HPACK itself remains enabled, as required by HTTP/2. Static table references may still be
442+
* used. Disabling the dynamic table reduces per-connection memory usage, but can increase the
443+
* size of header blocks. The inbound dynamic table is disabled after the peer acknowledges the
444+
* corresponding HTTP/2 setting, and requires a peer that correctly implements that setting. By
445+
* default, the dynamic table is enabled.
446+
*/
447+
@CanIgnoreReturnValue
448+
public NettyChannelBuilder disableHpackDynamicTable() {
449+
disableHpackDynamicTable = true;
450+
return this;
451+
}
452+
437453
/**
438454
* Sets the maximum size of header list allowed to be received. This is cumulative size of the
439455
* headers with some overhead, as defined for
@@ -626,6 +642,7 @@ ClientTransportFactory buildTransportFactory() {
626642
eventLoopGroupPool,
627643
autoFlowControl,
628644
flowControlWindow,
645+
disableHpackDynamicTable,
629646
maxInboundMessageSize,
630647
maxHeaderListSize,
631648
softLimitHeaderListSize,
@@ -769,6 +786,7 @@ private static final class NettyTransportFactory implements ClientTransportFacto
769786
private final EventLoopGroup group;
770787
private final boolean autoFlowControl;
771788
private final int flowControlWindow;
789+
private final boolean disableHpackDynamicTable;
772790
private final int maxMessageSize;
773791
private final int maxHeaderListSize;
774792
private final int softLimitHeaderListSize;
@@ -790,6 +808,7 @@ private static final class NettyTransportFactory implements ClientTransportFacto
790808
ObjectPool<? extends EventLoopGroup> groupPool,
791809
boolean autoFlowControl,
792810
int flowControlWindow,
811+
boolean disableHpackDynamicTable,
793812
int maxMessageSize,
794813
int maxHeaderListSize,
795814
int softLimitHeaderListSize,
@@ -807,6 +826,7 @@ private static final class NettyTransportFactory implements ClientTransportFacto
807826
this.group = groupPool.getObject();
808827
this.autoFlowControl = autoFlowControl;
809828
this.flowControlWindow = flowControlWindow;
829+
this.disableHpackDynamicTable = disableHpackDynamicTable;
810830
this.maxMessageSize = maxMessageSize;
811831
this.maxHeaderListSize = maxHeaderListSize;
812832
this.softLimitHeaderListSize = softLimitHeaderListSize;
@@ -856,6 +876,7 @@ public void run() {
856876
localNegotiator,
857877
autoFlowControl,
858878
flowControlWindow,
879+
disableHpackDynamicTable,
859880
maxMessageSize,
860881
maxHeaderListSize,
861882
softLimitHeaderListSize,
@@ -895,6 +916,7 @@ public SwapChannelCredentialsResult swapChannelCredentials(ChannelCredentials ch
895916
groupPool,
896917
autoFlowControl,
897918
flowControlWindow,
919+
disableHpackDynamicTable,
898920
maxMessageSize,
899921
maxHeaderListSize,
900922
softLimitHeaderListSize,

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

Lines changed: 7 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;
@@ -158,6 +157,7 @@ static NettyClientHandler newHandler(
158157
@Nullable KeepAliveManager keepAliveManager,
159158
boolean autoFlowControl,
160159
int flowControlWindow,
160+
boolean disableHpackDynamicTable,
161161
int maxHeaderListSize,
162162
int softLimitHeaderListSize,
163163
Supplier<Stopwatch> stopwatchFactory,
@@ -171,8 +171,7 @@ static NettyClientHandler newHandler(
171171
Preconditions.checkArgument(maxHeaderListSize > 0, "maxHeaderListSize must be positive");
172172
Http2HeadersDecoder headersDecoder = new GrpcHttp2ClientHeadersDecoder(maxHeaderListSize);
173173
Http2FrameReader frameReader = new DefaultHttp2FrameReader(headersDecoder);
174-
Http2HeadersEncoder encoder = new DefaultHttp2HeadersEncoder(
175-
Http2HeadersEncoder.NEVER_SENSITIVE, false, 16, Integer.MAX_VALUE);
174+
Http2HeadersEncoder encoder = new GrpcHttp2HeadersEncoder(disableHpackDynamicTable);
176175
Http2FrameWriter frameWriter = new DefaultHttp2FrameWriter(encoder);
177176
Http2Connection connection = new DefaultHttp2Connection(false);
178177
UniformStreamByteDistributor dist = new UniformStreamByteDistributor(connection);
@@ -189,6 +188,7 @@ static NettyClientHandler newHandler(
189188
keepAliveManager,
190189
autoFlowControl,
191190
flowControlWindow,
191+
disableHpackDynamicTable,
192192
maxHeaderListSize,
193193
softLimitHeaderListSize,
194194
stopwatchFactory,
@@ -210,6 +210,7 @@ static NettyClientHandler newHandler(
210210
KeepAliveManager keepAliveManager,
211211
boolean autoFlowControl,
212212
int flowControlWindow,
213+
boolean disableHpackDynamicTable,
213214
int maxHeaderListSize,
214215
int softLimitHeaderListSize,
215216
Supplier<Stopwatch> stopwatchFactory,
@@ -257,6 +258,9 @@ static NettyClientHandler newHandler(
257258
settings.initialWindowSize(flowControlWindow);
258259
settings.maxConcurrentStreams(0);
259260
settings.maxHeaderListSize(maxHeaderListSize);
261+
if (disableHpackDynamicTable) {
262+
settings.headerTableSize(0);
263+
}
260264

261265
return new NettyClientHandler(
262266
decoder,

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ class NettyClientTransport implements ConnectionClientTransport,
8585
private final AsciiString userAgent;
8686
private final boolean autoFlowControl;
8787
private final int flowControlWindow;
88+
private final boolean disableHpackDynamicTable;
8889
private final int maxMessageSize;
8990
private final int maxHeaderListSize;
9091
private final int softLimitHeaderListSize;
@@ -120,6 +121,7 @@ class NettyClientTransport implements ConnectionClientTransport,
120121
ProtocolNegotiator negotiator,
121122
boolean autoFlowControl,
122123
int flowControlWindow,
124+
boolean disableHpackDynamicTable,
123125
int maxMessageSize,
124126
int maxHeaderListSize,
125127
int softLimitHeaderListSize,
@@ -145,6 +147,7 @@ class NettyClientTransport implements ConnectionClientTransport,
145147
this.channelOptions = Preconditions.checkNotNull(channelOptions, "channelOptions");
146148
this.autoFlowControl = autoFlowControl;
147149
this.flowControlWindow = flowControlWindow;
150+
this.disableHpackDynamicTable = disableHpackDynamicTable;
148151
this.maxMessageSize = maxMessageSize;
149152
this.maxHeaderListSize = maxHeaderListSize;
150153
this.softLimitHeaderListSize = softLimitHeaderListSize;
@@ -247,6 +250,7 @@ public Runnable start(Listener transportListener) {
247250
keepAliveManager,
248251
autoFlowControl,
249252
flowControlWindow,
253+
disableHpackDynamicTable,
250254
maxHeaderListSize,
251255
softLimitHeaderListSize,
252256
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
@@ -91,6 +91,7 @@ class NettyServer implements InternalServer, InternalWithLogId {
9191
private final ChannelGroup channelGroup;
9292
private final boolean autoFlowControl;
9393
private final int flowControlWindow;
94+
private final boolean disableHpackDynamicTable;
9495
private final int maxMessageSize;
9596
private final int maxHeaderListSize;
9697
private final int softLimitHeaderListSize;
@@ -129,6 +130,7 @@ class NettyServer implements InternalServer, InternalWithLogId {
129130
int maxStreamsPerConnection,
130131
boolean autoFlowControl,
131132
int flowControlWindow,
133+
boolean disableHpackDynamicTable,
132134
int maxMessageSize,
133135
int maxHeaderListSize,
134136
int softLimitHeaderListSize,
@@ -160,6 +162,7 @@ class NettyServer implements InternalServer, InternalWithLogId {
160162
this.maxStreamsPerConnection = maxStreamsPerConnection;
161163
this.autoFlowControl = autoFlowControl;
162164
this.flowControlWindow = flowControlWindow;
165+
this.disableHpackDynamicTable = disableHpackDynamicTable;
163166
this.maxMessageSize = maxMessageSize;
164167
this.maxHeaderListSize = maxHeaderListSize;
165168
this.softLimitHeaderListSize = softLimitHeaderListSize;
@@ -265,6 +268,7 @@ public void initChannel(Channel ch) {
265268
maxStreamsPerConnection,
266269
autoFlowControl,
267270
flowControlWindow,
271+
disableHpackDynamicTable,
268272
maxMessageSize,
269273
maxHeaderListSize,
270274
softLimitHeaderListSize,

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ public final class NettyServerBuilder extends ForwardingServerBuilder<NettyServe
105105
private int maxConcurrentCallsPerConnection = Integer.MAX_VALUE;
106106
private boolean autoFlowControl = true;
107107
private int flowControlWindow = DEFAULT_FLOW_CONTROL_WINDOW;
108+
private boolean disableHpackDynamicTable;
108109
private int maxMessageSize = DEFAULT_MAX_MESSAGE_SIZE;
109110
private int maxHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
110111
private int softLimitHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
@@ -440,6 +441,21 @@ public NettyServerBuilder flowControlWindow(int flowControlWindow) {
440441
return this;
441442
}
442443

444+
/**
445+
* Disables use of the HPACK dynamic table for HTTP/2 header compression.
446+
*
447+
* <p>HPACK itself remains enabled, as required by HTTP/2. Static table references may still be
448+
* used. Disabling the dynamic table reduces per-connection memory usage, but can increase the
449+
* size of header blocks. The inbound dynamic table is disabled after the peer acknowledges the
450+
* corresponding HTTP/2 setting, and requires a peer that correctly implements that setting. By
451+
* default, the dynamic table is enabled.
452+
*/
453+
@CanIgnoreReturnValue
454+
public NettyServerBuilder disableHpackDynamicTable() {
455+
disableHpackDynamicTable = true;
456+
return this;
457+
}
458+
443459
/**
444460
* Sets the maximum message size allowed to be received on the server. If not called,
445461
* defaults to 4 MiB. The default provides protection to services who haven't considered the
@@ -729,6 +745,7 @@ NettyServer buildTransportServers(
729745
maxConcurrentCallsPerConnection,
730746
autoFlowControl,
731747
flowControlWindow,
748+
disableHpackDynamicTable,
732749
maxMessageSize,
733750
maxHeaderListSize,
734751
softLimitHeaderListSize,

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

Lines changed: 7 additions & 3 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;
@@ -164,6 +163,7 @@ static NettyServerHandler newHandler(
164163
int maxStreams,
165164
boolean autoFlowControl,
166165
int flowControlWindow,
166+
boolean disableHpackDynamicTable,
167167
int maxHeaderListSize,
168168
int softLimitHeaderListSize,
169169
int maxMessageSize,
@@ -184,8 +184,7 @@ static NettyServerHandler newHandler(
184184
Http2HeadersDecoder headersDecoder = new GrpcHttp2ServerHeadersDecoder(maxHeaderListSize);
185185
Http2FrameReader frameReader = new Http2InboundFrameLogger(
186186
new DefaultHttp2FrameReader(headersDecoder), frameLogger);
187-
Http2HeadersEncoder encoder = new DefaultHttp2HeadersEncoder(
188-
Http2HeadersEncoder.NEVER_SENSITIVE, false, 16, Integer.MAX_VALUE);
187+
Http2HeadersEncoder encoder = new GrpcHttp2HeadersEncoder(disableHpackDynamicTable);
189188
Http2FrameWriter frameWriter =
190189
new Http2OutboundFrameLogger(new DefaultHttp2FrameWriter(encoder), frameLogger);
191190
return newHandler(
@@ -198,6 +197,7 @@ static NettyServerHandler newHandler(
198197
maxStreams,
199198
autoFlowControl,
200199
flowControlWindow,
200+
disableHpackDynamicTable,
201201
maxHeaderListSize,
202202
softLimitHeaderListSize,
203203
maxMessageSize,
@@ -225,6 +225,7 @@ static NettyServerHandler newHandler(
225225
int maxStreams,
226226
boolean autoFlowControl,
227227
int flowControlWindow,
228+
boolean disableHpackDynamicTable,
228229
int maxHeaderListSize,
229230
int softLimitHeaderListSize,
230231
int maxMessageSize,
@@ -282,6 +283,9 @@ static NettyServerHandler newHandler(
282283
settings.initialWindowSize(flowControlWindow);
283284
settings.maxConcurrentStreams(maxStreams);
284285
settings.maxHeaderListSize(maxHeaderListSize);
286+
if (disableHpackDynamicTable) {
287+
settings.headerTableSize(0);
288+
}
285289

286290
return new NettyServerHandler(
287291
channelUnused,

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ class NettyServerTransport implements ServerTransport {
6969
private boolean terminated;
7070
private final boolean autoFlowControl;
7171
private final int flowControlWindow;
72+
private final boolean disableHpackDynamicTable;
7273
private final int maxMessageSize;
7374
private final int maxHeaderListSize;
7475
private final int softLimitHeaderListSize;
@@ -95,6 +96,7 @@ class NettyServerTransport implements ServerTransport {
9596
int maxStreams,
9697
boolean autoFlowControl,
9798
int flowControlWindow,
99+
boolean disableHpackDynamicTable,
98100
int maxMessageSize,
99101
int maxHeaderListSize,
100102
int softLimitHeaderListSize,
@@ -118,6 +120,7 @@ class NettyServerTransport implements ServerTransport {
118120
this.maxStreams = maxStreams;
119121
this.autoFlowControl = autoFlowControl;
120122
this.flowControlWindow = flowControlWindow;
123+
this.disableHpackDynamicTable = disableHpackDynamicTable;
121124
this.maxMessageSize = maxMessageSize;
122125
this.maxHeaderListSize = maxHeaderListSize;
123126
this.softLimitHeaderListSize = softLimitHeaderListSize;
@@ -281,6 +284,7 @@ private NettyServerHandler createHandler(
281284
maxStreams,
282285
autoFlowControl,
283286
flowControlWindow,
287+
disableHpackDynamicTable,
284288
maxHeaderListSize,
285289
softLimitHeaderListSize,
286290
maxMessageSize,

0 commit comments

Comments
 (0)