transport: set default header list size to 8KB and guard behind an env variable. (#9019)
Part of: https://github.com/grpc/grpc-go/issues/2308
Follow up of PR: https://github.com/grpc/grpc-go/pull/8845
This PR implements a change to restrict the default maximum header list
size to 8KB. The change is guarded by a new environment variable
GRPC_GO_EXPERIMENTAL_DEFAULT_HEADER_LIST_SIZE, which is disabled by
default.
When the environment variable is disabled (default), the legacy default
of 16MB is used. When enabled, RPCs with a total size of headers
exceeding 8KB will fail unless explicitly configured otherwise by the
user.
## Changes
`internal/envconfig`
* Added DefaultHeaderListSize: A boolean flag controlled by the
GRPC_GO_EXPERIMENTAL_DEFAULT_HEADER_LIST_SIZE environment variable.
`internal/transport/http2_client.go` and
`internal/transport/http2_server.go`
* Use upcomingDefaultHeaderListSize (8KB) as the default value if
`DefaultHeaderListSize` is enabled.
* If env variable is disabled and header size is greater than
upcomingDefaultHeaderListSize, log a warning.
RELEASE NOTES:
* transport: change default header list size from 16MB to 8KB and guard
it behind `GRPC_GO_EXPERIMENTAL_DEFAULT_HEADER_LIST_SIZE` env var which
is disabled by default. The env var will be enabled by default in
v1.82.0 release.
diff --git a/internal/envconfig/envconfig.go b/internal/envconfig/envconfig.go
index f6eb3b0..da15c37 100644
--- a/internal/envconfig/envconfig.go
+++ b/internal/envconfig/envconfig.go
@@ -126,6 +126,18 @@
// enabled by setting the env variable
// GRPC_EXPERIMENTAL_ENABLE_PRIORITY_LB_CHILD_POLICY_CACHE to true.
EnablePriorityLBChildPolicyCache = boolFromEnv("GRPC_EXPERIMENTAL_ENABLE_PRIORITY_LB_CHILD_POLICY_CACHE", false)
+
+ // Enable8KBDefaultHeaderListSize indicates that default maximum header list
+ // size is restricted to 8KB. This is disabled by default, but can be enabled
+ // by setting the environment variable
+ // "GRPC_GO_EXPERIMENTAL_ENABLE_8KB_DEFAULT_HEADER_LIST_SIZE" to "true".
+ // When disabled, the default maximum header list size of 16MB is used.
+ //
+ // When enabled, RPCs with a total size of headers exceeding 8KB will fail
+ // unless explicitly configured otherwise by the user.
+ //
+ // TODO: In release v1.82.0, env var will be enabled by default.
+ Enable8KBDefaultHeaderListSize = boolFromEnv("GRPC_GO_EXPERIMENTAL_ENABLE_8KB_DEFAULT_HEADER_LIST_SIZE", false)
)
func boolFromEnv(envVar string, def bool) bool {
diff --git a/internal/transport/http2_client.go b/internal/transport/http2_client.go
index d6bc6a6..0f4f3ef 100644
--- a/internal/transport/http2_client.go
+++ b/internal/transport/http2_client.go
@@ -39,6 +39,7 @@
"google.golang.org/grpc/internal"
"google.golang.org/grpc/internal/channelz"
icredentials "google.golang.org/grpc/internal/credentials"
+ "google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/grpclog"
"google.golang.org/grpc/internal/grpcsync"
"google.golang.org/grpc/internal/grpcutil"
@@ -318,7 +319,13 @@
}
writeBufSize := opts.WriteBufferSize
readBufSize := opts.ReadBufferSize
+ // The default header list size is moving from 16MB to 8KB. The 8KB limit
+ // is only used if Enable8KBDefaultHeaderListSize is true; otherwise, the
+ // old 16MB default is used. User-specified options always take precedence.
maxHeaderListSize := defaultClientMaxHeaderListSize
+ if envconfig.Enable8KBDefaultHeaderListSize {
+ maxHeaderListSize = upcomingDefaultHeaderListSize
+ }
if opts.MaxHeaderListSize != nil {
maxHeaderListSize = *opts.MaxHeaderListSize
}
@@ -879,8 +886,8 @@
return false
}
}
- if sz > int64(upcomingDefaultHeaderListSize) {
- t.logger.Warningf("Header list size to send (%d bytes) is larger than the upcoming default limit (%d bytes). In a future release, this will be restricted to %d bytes.", sz, upcomingDefaultHeaderListSize, upcomingDefaultHeaderListSize)
+ if !envconfig.Enable8KBDefaultHeaderListSize && sz > int64(upcomingDefaultHeaderListSize) {
+ t.logger.Warningf("Header list size to send (%d bytes) is larger than the upcoming default limit (%d bytes). In release v1.82.0, GRPC_GO_EXPERIMENTAL_ENABLE_8KB_DEFAULT_HEADER_LIST_SIZE will be enabled by default, enforcing this limit.", sz, upcomingDefaultHeaderListSize)
}
return true
}
diff --git a/internal/transport/http2_server.go b/internal/transport/http2_server.go
index 3a8c36e..9d25bb3 100644
--- a/internal/transport/http2_server.go
+++ b/internal/transport/http2_server.go
@@ -38,6 +38,7 @@
"google.golang.org/protobuf/proto"
"google.golang.org/grpc/internal"
+ "google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/grpclog"
"google.golang.org/grpc/internal/grpcutil"
"google.golang.org/grpc/internal/pretty"
@@ -165,7 +166,13 @@
}
writeBufSize := config.WriteBufferSize
readBufSize := config.ReadBufferSize
+ // The default header list size is moving from 16MB to 8KB. The 8KB limit
+ // is only used if Enable8KBDefaultHeaderListSize is true; otherwise, the
+ // old 16MB default is used. User-specified options always take precedence.
maxHeaderListSize := defaultServerMaxHeaderListSize
+ if envconfig.Enable8KBDefaultHeaderListSize {
+ maxHeaderListSize = upcomingDefaultHeaderListSize
+ }
if config.MaxHeaderListSize != nil {
maxHeaderListSize = *config.MaxHeaderListSize
}
@@ -948,8 +955,8 @@
return false
}
}
- if sz > int64(upcomingDefaultHeaderListSize) {
- t.logger.Warningf("Header list size to send (%d bytes) is larger than the upcoming default limit (%d bytes). In a future release, this will be restricted to %d bytes.", sz, upcomingDefaultHeaderListSize, upcomingDefaultHeaderListSize)
+ if !envconfig.Enable8KBDefaultHeaderListSize && sz > int64(upcomingDefaultHeaderListSize) {
+ t.logger.Warningf("Header list size to send (%d bytes) is larger than the upcoming default limit (%d bytes). In release v1.82.0, GRPC_GO_EXPERIMENTAL_ENABLE_8KB_DEFAULT_HEADER_LIST_SIZE will be enabled by default, enforcing this limit.", sz, upcomingDefaultHeaderListSize)
}
return true
}
diff --git a/test/end2end_test.go b/test/end2end_test.go
index e23ee5f..534c416 100644
--- a/test/end2end_test.go
+++ b/test/end2end_test.go
@@ -56,6 +56,7 @@
"google.golang.org/grpc/internal"
"google.golang.org/grpc/internal/binarylog"
"google.golang.org/grpc/internal/channelz"
+ "google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/grpcsync"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/stubserver"
@@ -7211,3 +7212,97 @@
t.Fatalf("sh.delayedPickComplete count: %v, want: %v", got, want)
}
}
+
+// TestEnable8KBDefaultHeaderListSize_ClientSendsLargeHeaders verifies that
+// when the Enable8KBDefaultHeaderListSize env var is enabled, and the client
+// sends metadata with a total size exceeding the 8KB default limit, the server
+// rejects the request and RPC will fail.
+func (s) TestEnable8KBDefaultHeaderListSize_ClientSendsLargeHeaders(t *testing.T) {
+ tests := []struct {
+ name string
+ enable bool
+ wantCode codes.Code
+ }{
+ {
+ name: "env_var_enabled",
+ enable: true,
+ wantCode: codes.Unavailable,
+ },
+ {
+ name: "env_var_disabled",
+ enable: false,
+ wantCode: codes.OK,
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ testutils.SetEnvConfig(t, &envconfig.Enable8KBDefaultHeaderListSize, tc.enable)
+
+ ss := stubserver.StartTestService(t, nil)
+ if err := ss.StartClient(); err != nil {
+ t.Fatal("Failed to create client to stub server:", err)
+ }
+ defer ss.Stop()
+
+ ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
+ defer cancel()
+ md := metadata.MD{"large-key": []string{strings.Repeat("a", 9000)}}
+ ctx = metadata.NewOutgoingContext(ctx, md)
+
+ _, err := ss.Client.EmptyCall(ctx, &testpb.Empty{})
+ if got := status.Code(err); got != tc.wantCode {
+ t.Fatalf("EmptyCall() failed with code: %v, want: %v; full error: %v", got, tc.wantCode, err)
+ }
+ })
+ }
+}
+
+// TestEnable8KBDefaultHeaderListSize_ServerSendsLargeHeaders verifies that
+// when the Enable8KBDefaultHeaderListSize env var is enabled, and server
+// sends metadata with a total size exceeding the 8KB default limit, the RPC
+// will fail.
+func (s) TestEnable8KBDefaultHeaderListSize_ServerSendsLargeHeaders(t *testing.T) {
+ const largeHeaderSize = 9000
+ tests := []struct {
+ name string
+ enable bool
+ wantCode codes.Code
+ }{
+ {
+ name: "env_var_enabled",
+ enable: true,
+ wantCode: codes.Unavailable,
+ },
+ {
+ name: "env_var_disabled",
+ enable: false,
+ wantCode: codes.OK,
+ },
+ }
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ testutils.SetEnvConfig(t, &envconfig.Enable8KBDefaultHeaderListSize, tc.enable)
+
+ ss := &stubserver.StubServer{
+ EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
+ md := metadata.MD{"large-key": []string{strings.Repeat("a", largeHeaderSize)}}
+ grpc.SetHeader(ctx, md)
+ return &testpb.Empty{}, nil
+ },
+ }
+ if err := ss.Start(nil); err != nil {
+ t.Fatal("Error starting server:", err)
+ }
+ defer ss.Stop()
+
+ ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
+ defer cancel()
+
+ _, err := ss.Client.EmptyCall(ctx, &testpb.Empty{})
+ if got := status.Code(err); got != tc.wantCode {
+ t.Fatalf("EmptyCall() failed with code: %v, want: %v; full error: %v", got, tc.wantCode, err)
+ }
+ })
+ }
+}