From 74e6d66186444ab24f5bbf9341d61e8cb37a33d5 Mon Sep 17 00:00:00 2001 From: Sean Pan Date: Mon, 19 Oct 2026 17:32:21 +0100 Subject: [PATCH] fix(storage): increase gRPC response metadata limit Allow bounded response metadata larger than gRPC's default budget so an otherwise valid Storage response does not fail during header decoding. Cover accepted metadata and rejection above the new limit using loopback gRPC transport tests. Co-Authored-By: Codex (GPT-5) --- .../cloud/storage/GrpcStorageOptions.java | 1 + .../storage/GrpcResponseMetadataTest.java | 106 ++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/GrpcResponseMetadataTest.java diff --git a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java index 1a6726b9c01b..eb209b9c716d 100644 --- a/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java +++ b/java-storage/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageOptions.java @@ -321,6 +321,7 @@ private Tuple> resolveSettingsAndOpts() throw InstantiatingGrpcChannelProvider.Builder channelProviderBuilder = InstantiatingGrpcChannelProvider.newBuilder() .setEndpoint(endpoint) + .setMaxInboundMetadataSize(64 * 1024) .setAllowNonDefaultServiceAccount(true) .setAttemptDirectPath(attemptDirectPath); diff --git a/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/GrpcResponseMetadataTest.java b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/GrpcResponseMetadataTest.java new file mode 100644 index 000000000000..9c283f3353fb --- /dev/null +++ b/java-storage/google-cloud-storage/src/test/java/com/google/cloud/storage/GrpcResponseMetadataTest.java @@ -0,0 +1,106 @@ +/* + * Copyright 2026 Google LLC + * + * 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 + * + * https://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 com.google.cloud.storage; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import com.google.api.gax.retrying.RetrySettings; +import com.google.cloud.NoCredentials; +import com.google.common.base.Throwables; +import com.google.storage.v2.GetObjectRequest; +import com.google.storage.v2.Object; +import com.google.storage.v2.StorageGrpc; +import io.grpc.Metadata; +import io.grpc.Server; +import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder; +import io.grpc.stub.MetadataUtils; +import io.grpc.stub.StreamObserver; +import java.net.InetSocketAddress; +import java.time.Duration; +import java.util.Arrays; +import java.util.concurrent.TimeUnit; +import org.junit.Test; + +public final class GrpcResponseMetadataTest { + + @Test + public void getAcceptsSmallResponseMetadata() throws Exception { + assertThat(getWithResponseMetadata(1024).getGeneration()).isEqualTo(1L); + } + + @Test + public void getAcceptsResponseMetadataLargerThanGrpcDefault() throws Exception { + assertThat(getWithResponseMetadata(18000).getGeneration()).isEqualTo(1L); + } + + @Test + public void getRejectsResponseMetadataAboveLimit() { + StorageException exception = + assertThrows(StorageException.class, () -> getWithResponseMetadata(96 * 1024)); + + assertThat(Throwables.getStackTraceAsString(exception)) + .contains("Header size exceeded max allowed size"); + } + + private static Blob getWithResponseMetadata(int metadataSize) throws Exception { + char[] value = new char[metadataSize]; + Arrays.fill(value, 'a'); + Metadata headers = new Metadata(); + headers.put( + Metadata.Key.of("x-test-metadata", Metadata.ASCII_STRING_MARSHALLER), new String(value)); + StorageGrpc.StorageImplBase service = + new StorageGrpc.StorageImplBase() { + @Override + public void getObject(GetObjectRequest request, StreamObserver responseObserver) { + responseObserver.onNext( + Object.newBuilder() + .setBucket(request.getBucket()) + .setName(request.getObject()) + .setGeneration(1) + .build()); + responseObserver.onCompleted(); + } + }; + Server server = + NettyServerBuilder.forAddress(new InetSocketAddress("127.0.0.1", 0)) + .addService(service) + .intercept(MetadataUtils.newAttachMetadataServerInterceptor(headers)) + .build() + .start(); + try (Storage storage = + StorageOptions.grpc() + .setHost("http://127.0.0.1:" + server.getPort()) + .setProjectId("test-project") + .setCredentials(NoCredentials.getInstance()) + .setAttemptDirectPath(false) + .setEnableGrpcClientMetrics(false) + .setRetrySettings( + RetrySettings.newBuilder() + .setMaxAttempts(1) + .setTotalTimeoutDuration(Duration.ofSeconds(5)) + .setInitialRpcTimeoutDuration(Duration.ofSeconds(5)) + .setMaxRpcTimeoutDuration(Duration.ofSeconds(5)) + .build()) + .build() + .getService()) { + return storage.get(BlobId.of("test-bucket", "test-object")); + } finally { + server.shutdownNow().awaitTermination(5, TimeUnit.SECONDS); + } + } +}