From d720432ce11f6e862be8461cedd2db58cf1437a2 Mon Sep 17 00:00:00 2001 From: mahendra-google Date: Mon, 29 Jun 2026 23:33:55 -0700 Subject: [PATCH] feat(Storage): Add idempotency header --- .../StorageClientImplRetryTest.cs | 28 +++++++++++++++++++ .../Google.Cloud.Storage.V1/RetryHandler.cs | 20 +++++++++++++ 2 files changed, 48 insertions(+) diff --git a/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1.Tests/StorageClientImplRetryTest.cs b/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1.Tests/StorageClientImplRetryTest.cs index 67cd08bc295d..540c365bf0d9 100644 --- a/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1.Tests/StorageClientImplRetryTest.cs +++ b/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1.Tests/StorageClientImplRetryTest.cs @@ -491,5 +491,33 @@ Dictionary ConvertHeader(string header) => header.Split(' ').ToDictionary(piece => piece.Split('/')[0], piece => piece.Split('/')[1]); } #endregion + + #region Idempotency Header test case + [Fact] + public void IdempotencyHeaderIsSet() + { + var replayingMessageHandler = new ReplayingMessageHandler(RetryHandler.IdempotencyTokenHeader); + var service = new FakeStorageService(replayingMessageHandler); + + var request = service.Buckets.Get("bucket"); + service.ExpectRequest(request, HttpStatusCode.BadGateway); + service.ExpectRequest(request, HttpStatusCode.BadGateway); + service.ExpectRequest(request, new Bucket()); + + var client = new StorageClientImpl(service); + client.GetBucket("bucket"); + service.Verify(); + + var actualHeaders = replayingMessageHandler.CapturedHeaders; + + Assert.Equal(3, actualHeaders.Count); + Assert.All(actualHeaders, token => Assert.False(string.IsNullOrEmpty(token))); + + var firstToken = actualHeaders.First(); + // Just validate that it's a real GUID... + Guid.Parse(firstToken); + Assert.All(actualHeaders, token => Assert.Equal(firstToken, token)); + } + #endregion } } diff --git a/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/RetryHandler.cs b/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/RetryHandler.cs index 059d6a37c7f4..98f9f48757f6 100644 --- a/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/RetryHandler.cs +++ b/apis/Google.Cloud.Storage.V1/Google.Cloud.Storage.V1/RetryHandler.cs @@ -35,6 +35,7 @@ internal sealed class RetryHandler : IHttpUnsuccessfulResponseHandler // For testing internal const string InvocationIdHeaderPart = "gccl-invocation-id"; internal const string AttemptCountHeaderPart = "gccl-attempt-count"; + internal const string IdempotencyTokenHeader = "x-goog-gcs-idempotency-token"; private readonly RetryOptions _retryOptions; private readonly IScheduler _scheduler; @@ -60,6 +61,7 @@ internal static void MarkAsRetriable(StorageBaseServiceRequest + /// Interceptor that automatically attaches a unique idempotency token + /// to the x-goog-gcs-idempotency-token header. + /// + private sealed class IdempotencyTokenInterceptor : IHttpExecuteInterceptor + { + internal static IdempotencyTokenInterceptor Instance { get; } = new IdempotencyTokenInterceptor(); + private IdempotencyTokenInterceptor() { } + + public Task InterceptAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (!request.Headers.Contains(IdempotencyTokenHeader)) + { + request.Headers.TryAddWithoutValidation(IdempotencyTokenHeader, Guid.NewGuid().ToString()); + } return Task.CompletedTask; } }