feat(storage): setting idempotency token header - #27338
shubhangi-google wants to merge 20 commits into
Conversation
There was a problem hiding this comment.
AI review: there are two issues.
-
Global Default Scope Leak (Critical Issue):
By settingRequestOptions.default.add_idempotency_token_header = trueingoogle/apis/options.rb, this PR enables the GCS-specificX-Goog-Gcs-Idempotency-Tokenheader globally for all 400+ Google API clients usinggoogle-apis-core(e.g., YouTube, Drive, Compute Engine, Calendar, Bigtable). This violates service abstraction boundaries and adds a 36-byte UUID overhead and confusing headers to every non-GCS request.
Fix: Defaultadd_idempotency_token_headertofalseingoogle-apis-coreand explicitly enable it in thegoogle-apis-storage_v1client generation or configuration. -
Redundant Memoization:
Inset_idempotency_token_header, the@idempotency_token ||= SecureRandom.uuidmemoization is functionally dead code because of the early return on the line right before it:
return if header.any? { |k, _| k.to_s.downcase == 'x-goog-gcs-idempotency-token' }
@idempotency_token ||= SecureRandom.uuidBecause the header Hash is mutated in place and retained across retries of the ApiCommand instance, the first network attempt injects the header into header. On subsequent retries, the loop evaluates return if header.any? { ... }, successfully finding the original key, and returning early. Therefore, @idempotency_token is never evaluated or used again across retries.
Fix: You can simply do header['X-Goog-Gcs-Idempotency-Token'] = SecureRandom.uuid, as caching the UUID as an instance variable is unnecessary.
|
|
||
| def invocation_id_header | ||
| "gccl-invocation-id/#{SecureRandom.uuid}" | ||
| @invocation_id ||= SecureRandom.uuid |
There was a problem hiding this comment.
Why are we making this an instance variable?
There was a problem hiding this comment.
without an instance variable, every retry would generate a brand new UUID. We use an instance variable to memoize the UUID.
There was a problem hiding this comment.
As discussed offline, please do the required testing.
There was a problem hiding this comment.
hi @bajajneha27 we already have test to check the Invocation ID across retries
https://github.com/googleapis/google-api-ruby-client/blob/main/google-apis-core/spec/google/apis/core/api_command_spec.rb#L249
we did the testing ,the functionality is working fine with instance variable
|
|
||
| def set_idempotency_token_header | ||
| return if options&.header&.any? { |k, _| k.to_s.downcase == 'x-goog-gcs-idempotency-token' } | ||
| return if header.any? { |k, _| k.to_s.downcase == 'x-goog-gcs-idempotency-token' } |
There was a problem hiding this comment.
Can you please help me understand how header is populated?
There was a problem hiding this comment.
When a request is made for the first time, this hash doesn't have the idempotency token, so the code populates it with a SecureRandom.uuid.
However, if the API call fails (e.g., due to a 503 Service Unavailable) and the library automatically retries the request, the prepare! method is called again. During this retry, the internal header hash still contains the UUID that was generated on the first attempt. Above line detects this and returns early, ensuring the exact same UUID is sent again on the retry.
There was a problem hiding this comment.
My question is how we're populating header hash ?
There was a problem hiding this comment.
The header hash is an instance attribute (accessor for @Header) of the command object, which is inherited from the base HttpCommand class. It gets initialized (typically as an empty hash) when the command object is first instantiated.
Throughout the lifecycle of the request, the prepare! method and its helpers (like set_api_client_header, set_user_project_header, and set_idempotency_token_header) mutate this exact same instance variable. Because the library reuses the same command object instance during a retry, the header hash retains the values (like the UUID) that were populated into it during the initial request's prepare! call.
| RequestOptions.default.quota_project = nil | ||
| RequestOptions.default.add_invocation_id_header = false | ||
| RequestOptions.default.upload_chunk_size = 100 * 1024 * 1024 # 100 MB | ||
| RequestOptions.default.add_idempotency_token_header = false |
There was a problem hiding this comment.
Is adding idempotency token going to be optional?
I thought it was going to be added by default.
There was a problem hiding this comment.
as per this comment #27338 (review)
It has to be optional and default to false in core library, It is being enabled by default inside the Storage API client specifically.
There was a problem hiding this comment.
My question is, do we need this option at all?
Isn't the requirement to always keep the header?
There was a problem hiding this comment.
x-goog-gcs-idempotency-token is being used for storage operations specifically (as the header name contained x-goog-gcs) we don't need to keep it optional and we make it default as it will be ignored by other libraries (e.g., YouTube, Drive, Compute Engine, Calendar) which are using core library
| command.options.header = { 'x-goog-gcs-idempotency-token' => 'my-custom-token' } | ||
| command.prepare! | ||
| expect(command.header['x-goog-gcs-idempotency-token']).to eql 'my-custom-token' | ||
| expect(command.header['X-Goog-Gcs-Idempotency-Token']).to be_nil |
There was a problem hiding this comment.
Why would it be nil here?
There was a problem hiding this comment.
expect(command.header['x-goog-gcs-idempotency-token']).to eql 'my-custom-token'
Confirms that the user's custom lowercase header ('x-goog-gcs-idempotency-token') was preserved with the custom value.
expect(command.header['X-Goog-Gcs-Idempotency-Token']).to be_nil Confirms that the library didn't accidentally fall back to its default behavior and append a second, PascalCased header ('X-Goog-Gcs-Idempotency-Token'). If this were not nil, the final HTTP request would contain two conflicting idempotency headers, which could cause errors on the backend server.
hi @cpriti-os |
This pull request introduces support for sending an idempotency token header (X-Goog-Gcs-Idempotency-Token) in API commands