Skip to content

Commit e9a8c2b

Browse files
petedmarshcodex
andauthored
netty: Support never-indexed metadata keys (grpc#12976)
Add NettyChannelBuilder.neverIndexMetadataKey() and neverIndexMetadataKeys() so callers can mark selected outbound metadata keys for HPACK's never-indexed literal representation. High-cardinality metadata values provide little compression benefit and can churn the server's dynamic HPACK table. Keeping them out of the table avoids unnecessary insertion and eviction work while preserving dynamic indexing for other headers. Propagate an immutable set of normalized metadata names through the client transport and use it in Netty's HPACK sensitivity detector. Add unit and interoperability coverage. Generated with AI using OpenAI Codex (GPT-5). Co-authored-by: Codex <noreply@openai.com>
1 parent cf92f2d commit e9a8c2b

13 files changed

Lines changed: 510 additions & 2 deletions

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import io.grpc.HttpConnectProxiedSocketAddress;
3939
import io.grpc.Internal;
4040
import io.grpc.ManagedChannelBuilder;
41+
import io.grpc.Metadata;
4142
import io.grpc.NameResolverProvider;
4243
import io.grpc.NameResolverRegistry;
4344
import io.grpc.internal.AtomicBackoff;
@@ -60,12 +61,15 @@
6061
import io.netty.channel.ReflectiveChannelFactory;
6162
import io.netty.channel.socket.nio.NioSocketChannel;
6263
import io.netty.handler.ssl.SslContext;
64+
import io.netty.util.AsciiString;
6365
import java.net.InetSocketAddress;
6466
import java.net.SocketAddress;
6567
import java.util.Collection;
6668
import java.util.Collections;
6769
import java.util.HashMap;
70+
import java.util.HashSet;
6871
import java.util.Map;
72+
import java.util.Set;
6973
import java.util.concurrent.Executor;
7074
import java.util.concurrent.ScheduledExecutorService;
7175
import java.util.concurrent.TimeUnit;
@@ -105,6 +109,7 @@ public final class NettyChannelBuilder extends ForwardingChannelBuilder2<NettyCh
105109
private ObjectPool<? extends EventLoopGroup> eventLoopGroupPool = DEFAULT_EVENT_LOOP_GROUP_POOL;
106110
private boolean autoFlowControl = DEFAULT_AUTO_FLOW_CONTROL;
107111
private int flowControlWindow = DEFAULT_FLOW_CONTROL_WINDOW;
112+
private final Set<AsciiString> neverIndexedMetadataKeys = new HashSet<>();
108113
private int maxHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
109114
private int softLimitHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
110115
private int maxInboundMessageSize = GrpcUtil.DEFAULT_MAX_MESSAGE_SIZE;
@@ -434,6 +439,43 @@ public NettyChannelBuilder flowControlWindow(int flowControlWindow) {
434439
return this;
435440
}
436441

442+
/**
443+
* Configures an outbound metadata key to use HPACK's never-indexed literal representation.
444+
*
445+
* <p>All values associated with the key's normalized name will be sent as literals and will not
446+
* be added to the peer's HPACK dynamic table. This method is additive and may be called multiple
447+
* times. Configuring the same normalized key name more than once has no additional effect. By
448+
* default, no metadata keys are configured as never indexed.
449+
*
450+
* @since 1.84.0
451+
*/
452+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12976")
453+
@CanIgnoreReturnValue
454+
public NettyChannelBuilder neverIndexMetadataKey(Metadata.Key<?> key) {
455+
neverIndexedMetadataKeys.add(AsciiString.of(checkNotNull(key, "key").name()));
456+
return this;
457+
}
458+
459+
/**
460+
* Configures outbound metadata keys to use HPACK's never-indexed literal representation.
461+
*
462+
* <p>This method is equivalent to calling {@link #neverIndexMetadataKey} for each key in {@code
463+
* keys}. Duplicate normalized key names are ignored.
464+
*
465+
* @since 1.84.0
466+
*/
467+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12976")
468+
@CanIgnoreReturnValue
469+
public NettyChannelBuilder neverIndexMetadataKeys(
470+
Collection<? extends Metadata.Key<?>> keys) {
471+
Set<AsciiString> normalizedKeys = new HashSet<>();
472+
for (Metadata.Key<?> key : checkNotNull(keys, "keys")) {
473+
normalizedKeys.add(AsciiString.of(checkNotNull(key, "key").name()));
474+
}
475+
neverIndexedMetadataKeys.addAll(normalizedKeys);
476+
return this;
477+
}
478+
437479
/**
438480
* Sets the maximum size of header list allowed to be received. This is cumulative size of the
439481
* headers with some overhead, as defined for
@@ -626,6 +668,7 @@ ClientTransportFactory buildTransportFactory() {
626668
eventLoopGroupPool,
627669
autoFlowControl,
628670
flowControlWindow,
671+
neverIndexedMetadataKeys,
629672
maxInboundMessageSize,
630673
maxHeaderListSize,
631674
softLimitHeaderListSize,
@@ -769,6 +812,7 @@ private static final class NettyTransportFactory implements ClientTransportFacto
769812
private final EventLoopGroup group;
770813
private final boolean autoFlowControl;
771814
private final int flowControlWindow;
815+
private final Set<AsciiString> neverIndexedMetadataKeys;
772816
private final int maxMessageSize;
773817
private final int maxHeaderListSize;
774818
private final int softLimitHeaderListSize;
@@ -790,6 +834,7 @@ private static final class NettyTransportFactory implements ClientTransportFacto
790834
ObjectPool<? extends EventLoopGroup> groupPool,
791835
boolean autoFlowControl,
792836
int flowControlWindow,
837+
Set<AsciiString> neverIndexedMetadataKeys,
793838
int maxMessageSize,
794839
int maxHeaderListSize,
795840
int softLimitHeaderListSize,
@@ -807,6 +852,8 @@ private static final class NettyTransportFactory implements ClientTransportFacto
807852
this.group = groupPool.getObject();
808853
this.autoFlowControl = autoFlowControl;
809854
this.flowControlWindow = flowControlWindow;
855+
this.neverIndexedMetadataKeys = Collections.unmodifiableSet(
856+
new HashSet<>(checkNotNull(neverIndexedMetadataKeys, "neverIndexedMetadataKeys")));
810857
this.maxMessageSize = maxMessageSize;
811858
this.maxHeaderListSize = maxHeaderListSize;
812859
this.softLimitHeaderListSize = softLimitHeaderListSize;
@@ -856,6 +903,7 @@ public void run() {
856903
localNegotiator,
857904
autoFlowControl,
858905
flowControlWindow,
906+
neverIndexedMetadataKeys,
859907
maxMessageSize,
860908
maxHeaderListSize,
861909
softLimitHeaderListSize,
@@ -895,6 +943,7 @@ public SwapChannelCredentialsResult swapChannelCredentials(ChannelCredentials ch
895943
groupPool,
896944
autoFlowControl,
897945
flowControlWindow,
946+
neverIndexedMetadataKeys,
898947
maxMessageSize,
899948
maxHeaderListSize,
900949
softLimitHeaderListSize,

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
import io.netty.handler.codec.http2.Http2Headers;
7777
import io.netty.handler.codec.http2.Http2HeadersDecoder;
7878
import io.netty.handler.codec.http2.Http2HeadersEncoder;
79+
import io.netty.handler.codec.http2.Http2HeadersEncoder.SensitivityDetector;
7980
import io.netty.handler.codec.http2.Http2InboundFrameLogger;
8081
import io.netty.handler.codec.http2.Http2OutboundFrameLogger;
8182
import io.netty.handler.codec.http2.Http2Settings;
@@ -84,12 +85,14 @@
8485
import io.netty.handler.codec.http2.StreamBufferingEncoder;
8586
import io.netty.handler.codec.http2.UniformStreamByteDistributor;
8687
import io.netty.handler.logging.LogLevel;
88+
import io.netty.util.AsciiString;
8789
import io.perfmark.PerfMark;
8890
import io.perfmark.Tag;
8991
import io.perfmark.TaskCloseable;
9092
import java.nio.channels.ClosedChannelException;
9193
import java.util.LinkedHashMap;
9294
import java.util.Map;
95+
import java.util.Set;
9396
import java.util.concurrent.Executor;
9497
import java.util.logging.Level;
9598
import java.util.logging.Logger;
@@ -158,6 +161,7 @@ static NettyClientHandler newHandler(
158161
@Nullable KeepAliveManager keepAliveManager,
159162
boolean autoFlowControl,
160163
int flowControlWindow,
164+
Set<AsciiString> neverIndexedMetadataKeys,
161165
int maxHeaderListSize,
162166
int softLimitHeaderListSize,
163167
Supplier<Stopwatch> stopwatchFactory,
@@ -172,7 +176,7 @@ static NettyClientHandler newHandler(
172176
Http2HeadersDecoder headersDecoder = new GrpcHttp2ClientHeadersDecoder(maxHeaderListSize);
173177
Http2FrameReader frameReader = new DefaultHttp2FrameReader(headersDecoder);
174178
Http2HeadersEncoder encoder = new DefaultHttp2HeadersEncoder(
175-
Http2HeadersEncoder.NEVER_SENSITIVE, false, 16, Integer.MAX_VALUE);
179+
sensitivityDetector(neverIndexedMetadataKeys), false, 16, Integer.MAX_VALUE);
176180
Http2FrameWriter frameWriter = new DefaultHttp2FrameWriter(encoder);
177181
Http2Connection connection = new DefaultHttp2Connection(false);
178182
UniformStreamByteDistributor dist = new UniformStreamByteDistributor(connection);
@@ -278,6 +282,20 @@ static NettyClientHandler newHandler(
278282
metricRecorder);
279283
}
280284

285+
@VisibleForTesting
286+
static SensitivityDetector sensitivityDetector(
287+
final Set<AsciiString> neverIndexedMetadataKeys) {
288+
if (neverIndexedMetadataKeys.isEmpty()) {
289+
return Http2HeadersEncoder.NEVER_SENSITIVE;
290+
}
291+
return new SensitivityDetector() {
292+
@Override
293+
public boolean isSensitive(CharSequence name, CharSequence value) {
294+
return neverIndexedMetadataKeys.contains(AsciiString.of(name));
295+
}
296+
};
297+
}
298+
281299
private NettyClientHandler(
282300
Http2ConnectionDecoder decoder,
283301
Http2ConnectionEncoder encoder,

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
import java.net.SocketAddress;
6565
import java.nio.channels.ClosedChannelException;
6666
import java.util.Map;
67+
import java.util.Set;
6768
import java.util.concurrent.Executor;
6869
import java.util.concurrent.TimeUnit;
6970
import javax.annotation.Nullable;
@@ -85,6 +86,7 @@ class NettyClientTransport implements ConnectionClientTransport,
8586
private final AsciiString userAgent;
8687
private final boolean autoFlowControl;
8788
private final int flowControlWindow;
89+
private final Set<AsciiString> neverIndexedMetadataKeys;
8890
private final int maxMessageSize;
8991
private final int maxHeaderListSize;
9092
private final int softLimitHeaderListSize;
@@ -120,6 +122,7 @@ class NettyClientTransport implements ConnectionClientTransport,
120122
ProtocolNegotiator negotiator,
121123
boolean autoFlowControl,
122124
int flowControlWindow,
125+
Set<AsciiString> neverIndexedMetadataKeys,
123126
int maxMessageSize,
124127
int maxHeaderListSize,
125128
int softLimitHeaderListSize,
@@ -145,6 +148,8 @@ class NettyClientTransport implements ConnectionClientTransport,
145148
this.channelOptions = Preconditions.checkNotNull(channelOptions, "channelOptions");
146149
this.autoFlowControl = autoFlowControl;
147150
this.flowControlWindow = flowControlWindow;
151+
this.neverIndexedMetadataKeys =
152+
Preconditions.checkNotNull(neverIndexedMetadataKeys, "neverIndexedMetadataKeys");
148153
this.maxMessageSize = maxMessageSize;
149154
this.maxHeaderListSize = maxHeaderListSize;
150155
this.softLimitHeaderListSize = softLimitHeaderListSize;
@@ -247,6 +252,7 @@ public Runnable start(Listener transportListener) {
247252
keepAliveManager,
248253
autoFlowControl,
249254
flowControlWindow,
255+
neverIndexedMetadataKeys,
250256
maxHeaderListSize,
251257
softLimitHeaderListSize,
252258
GrpcUtil.STOPWATCH_SUPPLIER,

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import io.netty.channel.group.ChannelGroupFutureListener;
5555
import io.netty.channel.group.DefaultChannelGroup;
5656
import io.netty.util.AbstractReferenceCounted;
57+
import io.netty.util.AsciiString;
5758
import io.netty.util.ReferenceCounted;
5859
import io.netty.util.concurrent.Future;
5960
import io.netty.util.concurrent.GenericFutureListener;
@@ -62,9 +63,11 @@
6263
import java.util.ArrayList;
6364
import java.util.Collections;
6465
import java.util.HashMap;
66+
import java.util.HashSet;
6567
import java.util.Iterator;
6668
import java.util.List;
6769
import java.util.Map;
70+
import java.util.Set;
6871
import java.util.concurrent.Callable;
6972
import java.util.logging.Level;
7073
import java.util.logging.Logger;
@@ -91,6 +94,7 @@ class NettyServer implements InternalServer, InternalWithLogId {
9194
private final ChannelGroup channelGroup;
9295
private final boolean autoFlowControl;
9396
private final int flowControlWindow;
97+
private final Set<AsciiString> neverIndexedMetadataKeys;
9498
private final int maxMessageSize;
9599
private final int maxHeaderListSize;
96100
private final int softLimitHeaderListSize;
@@ -129,6 +133,7 @@ class NettyServer implements InternalServer, InternalWithLogId {
129133
int maxStreamsPerConnection,
130134
boolean autoFlowControl,
131135
int flowControlWindow,
136+
Set<AsciiString> neverIndexedMetadataKeys,
132137
int maxMessageSize,
133138
int maxHeaderListSize,
134139
int softLimitHeaderListSize,
@@ -160,6 +165,8 @@ class NettyServer implements InternalServer, InternalWithLogId {
160165
this.maxStreamsPerConnection = maxStreamsPerConnection;
161166
this.autoFlowControl = autoFlowControl;
162167
this.flowControlWindow = flowControlWindow;
168+
this.neverIndexedMetadataKeys = Collections.unmodifiableSet(
169+
new HashSet<>(checkNotNull(neverIndexedMetadataKeys, "neverIndexedMetadataKeys")));
163170
this.maxMessageSize = maxMessageSize;
164171
this.maxHeaderListSize = maxHeaderListSize;
165172
this.softLimitHeaderListSize = softLimitHeaderListSize;
@@ -265,6 +272,7 @@ public void initChannel(Channel ch) {
265272
maxStreamsPerConnection,
266273
autoFlowControl,
267274
flowControlWindow,
275+
neverIndexedMetadataKeys,
268276
maxMessageSize,
269277
maxHeaderListSize,
270278
softLimitHeaderListSize,

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import io.grpc.ExperimentalApi;
3434
import io.grpc.ForwardingServerBuilder;
3535
import io.grpc.Internal;
36+
import io.grpc.Metadata;
3637
import io.grpc.MetricRecorder;
3738
import io.grpc.ServerBuilder;
3839
import io.grpc.ServerCredentials;
@@ -53,14 +54,18 @@
5354
import io.netty.channel.ServerChannel;
5455
import io.netty.channel.socket.nio.NioServerSocketChannel;
5556
import io.netty.handler.ssl.SslContext;
57+
import io.netty.util.AsciiString;
5658
import java.io.File;
5759
import java.io.InputStream;
5860
import java.net.InetSocketAddress;
5961
import java.net.SocketAddress;
6062
import java.util.ArrayList;
63+
import java.util.Collection;
6164
import java.util.HashMap;
65+
import java.util.HashSet;
6266
import java.util.List;
6367
import java.util.Map;
68+
import java.util.Set;
6469
import java.util.concurrent.TimeUnit;
6570
import javax.net.ssl.SSLException;
6671

@@ -105,6 +110,7 @@ public final class NettyServerBuilder extends ForwardingServerBuilder<NettyServe
105110
private int maxConcurrentCallsPerConnection = Integer.MAX_VALUE;
106111
private boolean autoFlowControl = true;
107112
private int flowControlWindow = DEFAULT_FLOW_CONTROL_WINDOW;
113+
private final Set<AsciiString> neverIndexedMetadataKeys = new HashSet<>();
108114
private int maxMessageSize = DEFAULT_MAX_MESSAGE_SIZE;
109115
private int maxHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
110116
private int softLimitHeaderListSize = GrpcUtil.DEFAULT_MAX_HEADER_LIST_SIZE;
@@ -440,6 +446,43 @@ public NettyServerBuilder flowControlWindow(int flowControlWindow) {
440446
return this;
441447
}
442448

449+
/**
450+
* Configures an outbound metadata key to use HPACK's never-indexed literal representation.
451+
*
452+
* <p>All values associated with the key's normalized name will be sent as literals and will not
453+
* be added to the peer's HPACK dynamic table. This method is additive and may be called multiple
454+
* times. Configuring the same normalized key name more than once has no additional effect. By
455+
* default, no metadata keys are configured as never indexed.
456+
*
457+
* @since 1.84.0
458+
*/
459+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12976")
460+
@CanIgnoreReturnValue
461+
public NettyServerBuilder neverIndexMetadataKey(Metadata.Key<?> key) {
462+
neverIndexedMetadataKeys.add(AsciiString.of(checkNotNull(key, "key").name()));
463+
return this;
464+
}
465+
466+
/**
467+
* Configures outbound metadata keys to use HPACK's never-indexed literal representation.
468+
*
469+
* <p>This method is equivalent to calling {@link #neverIndexMetadataKey} for each key in {@code
470+
* keys}. Duplicate normalized key names are ignored.
471+
*
472+
* @since 1.84.0
473+
*/
474+
@ExperimentalApi("https://github.com/grpc/grpc-java/issues/12976")
475+
@CanIgnoreReturnValue
476+
public NettyServerBuilder neverIndexMetadataKeys(
477+
Collection<? extends Metadata.Key<?>> keys) {
478+
Set<AsciiString> normalizedKeys = new HashSet<>();
479+
for (Metadata.Key<?> key : checkNotNull(keys, "keys")) {
480+
normalizedKeys.add(AsciiString.of(checkNotNull(key, "key").name()));
481+
}
482+
neverIndexedMetadataKeys.addAll(normalizedKeys);
483+
return this;
484+
}
485+
443486
/**
444487
* Sets the maximum message size allowed to be received on the server. If not called,
445488
* defaults to 4 MiB. The default provides protection to services who haven't considered the
@@ -729,6 +772,7 @@ NettyServer buildTransportServers(
729772
maxConcurrentCallsPerConnection,
730773
autoFlowControl,
731774
flowControlWindow,
775+
neverIndexedMetadataKeys,
732776
maxMessageSize,
733777
maxHeaderListSize,
734778
softLimitHeaderListSize,

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
import io.perfmark.TaskCloseable;
104104
import java.text.MessageFormat;
105105
import java.util.List;
106+
import java.util.Set;
106107
import java.util.concurrent.Future;
107108
import java.util.concurrent.ScheduledFuture;
108109
import java.util.concurrent.TimeUnit;
@@ -164,6 +165,7 @@ static NettyServerHandler newHandler(
164165
int maxStreams,
165166
boolean autoFlowControl,
166167
int flowControlWindow,
168+
Set<AsciiString> neverIndexedMetadataKeys,
167169
int maxHeaderListSize,
168170
int softLimitHeaderListSize,
169171
int maxMessageSize,
@@ -185,7 +187,10 @@ static NettyServerHandler newHandler(
185187
Http2FrameReader frameReader = new Http2InboundFrameLogger(
186188
new DefaultHttp2FrameReader(headersDecoder), frameLogger);
187189
Http2HeadersEncoder encoder = new DefaultHttp2HeadersEncoder(
188-
Http2HeadersEncoder.NEVER_SENSITIVE, false, 16, Integer.MAX_VALUE);
190+
NettyClientHandler.sensitivityDetector(neverIndexedMetadataKeys),
191+
false,
192+
16,
193+
Integer.MAX_VALUE);
189194
Http2FrameWriter frameWriter =
190195
new Http2OutboundFrameLogger(new DefaultHttp2FrameWriter(encoder), frameLogger);
191196
return newHandler(

0 commit comments

Comments
 (0)