Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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<String> REQUEST_METADATA_KEY =
Metadata.Key.of("hpack-request-metadata", Metadata.ASCII_STRING_MARSHALLER);
private static final Metadata.Key<String> 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 <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
Metadata headers,
ServerCallHandler<ReqT, RespT> 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<Metadata> responseHeaders = new AtomicReference<>();
AtomicReference<Metadata> 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<EmptyProtos.Empty> responseObserver) {
responseObserver.onNext(EMPTY);
responseObserver.onCompleted();
}
}
}
52 changes: 52 additions & 0 deletions netty/src/main/java/io/grpc/netty/GrpcHttp2HeadersEncoder.java
Original file line number Diff line number Diff line change
@@ -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);
}
}
22 changes: 22 additions & 0 deletions netty/src/main/java/io/grpc/netty/NettyChannelBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ public final class NettyChannelBuilder extends ForwardingChannelBuilder2<NettyCh
private ObjectPool<? extends EventLoopGroup> 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;
Expand Down Expand Up @@ -434,6 +435,21 @@ public NettyChannelBuilder flowControlWindow(int flowControlWindow) {
return this;
}

/**
* Disables use of the HPACK dynamic table for HTTP/2 header compression.
*
* <p>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
Expand Down Expand Up @@ -626,6 +642,7 @@ ClientTransportFactory buildTransportFactory() {
eventLoopGroupPool,
autoFlowControl,
flowControlWindow,
disableHpackDynamicTable,
maxInboundMessageSize,
maxHeaderListSize,
softLimitHeaderListSize,
Expand Down Expand Up @@ -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;
Expand All @@ -790,6 +808,7 @@ private static final class NettyTransportFactory implements ClientTransportFacto
ObjectPool<? extends EventLoopGroup> groupPool,
boolean autoFlowControl,
int flowControlWindow,
boolean disableHpackDynamicTable,
int maxMessageSize,
int maxHeaderListSize,
int softLimitHeaderListSize,
Expand All @@ -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;
Expand Down Expand Up @@ -856,6 +876,7 @@ public void run() {
localNegotiator,
autoFlowControl,
flowControlWindow,
disableHpackDynamicTable,
maxMessageSize,
maxHeaderListSize,
softLimitHeaderListSize,
Expand Down Expand Up @@ -895,6 +916,7 @@ public SwapChannelCredentialsResult swapChannelCredentials(ChannelCredentials ch
groupPool,
autoFlowControl,
flowControlWindow,
disableHpackDynamicTable,
maxMessageSize,
maxHeaderListSize,
softLimitHeaderListSize,
Expand Down
10 changes: 7 additions & 3 deletions netty/src/main/java/io/grpc/netty/NettyClientHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -158,6 +157,7 @@ static NettyClientHandler newHandler(
@Nullable KeepAliveManager keepAliveManager,
boolean autoFlowControl,
int flowControlWindow,
boolean disableHpackDynamicTable,
int maxHeaderListSize,
int softLimitHeaderListSize,
Supplier<Stopwatch> stopwatchFactory,
Expand All @@ -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);
Expand All @@ -189,6 +188,7 @@ static NettyClientHandler newHandler(
keepAliveManager,
autoFlowControl,
flowControlWindow,
disableHpackDynamicTable,
maxHeaderListSize,
softLimitHeaderListSize,
stopwatchFactory,
Expand All @@ -210,6 +210,7 @@ static NettyClientHandler newHandler(
KeepAliveManager keepAliveManager,
boolean autoFlowControl,
int flowControlWindow,
boolean disableHpackDynamicTable,
int maxHeaderListSize,
int softLimitHeaderListSize,
Supplier<Stopwatch> stopwatchFactory,
Expand Down Expand Up @@ -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,
Expand Down
4 changes: 4 additions & 0 deletions netty/src/main/java/io/grpc/netty/NettyClientTransport.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -120,6 +121,7 @@ class NettyClientTransport implements ConnectionClientTransport,
ProtocolNegotiator negotiator,
boolean autoFlowControl,
int flowControlWindow,
boolean disableHpackDynamicTable,
int maxMessageSize,
int maxHeaderListSize,
int softLimitHeaderListSize,
Expand All @@ -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;
Expand Down Expand Up @@ -247,6 +250,7 @@ public Runnable start(Listener transportListener) {
keepAliveManager,
autoFlowControl,
flowControlWindow,
disableHpackDynamicTable,
maxHeaderListSize,
softLimitHeaderListSize,
GrpcUtil.STOPWATCH_SUPPLIER,
Expand Down
4 changes: 4 additions & 0 deletions netty/src/main/java/io/grpc/netty/NettyServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -129,6 +130,7 @@ class NettyServer implements InternalServer, InternalWithLogId {
int maxStreamsPerConnection,
boolean autoFlowControl,
int flowControlWindow,
boolean disableHpackDynamicTable,
int maxMessageSize,
int maxHeaderListSize,
int softLimitHeaderListSize,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -265,6 +268,7 @@ public void initChannel(Channel ch) {
maxStreamsPerConnection,
autoFlowControl,
flowControlWindow,
disableHpackDynamicTable,
maxMessageSize,
maxHeaderListSize,
softLimitHeaderListSize,
Expand Down
Loading
Loading