Skip to content

feat(storage): setting idempotency token header - #27338

Open
shubhangi-google wants to merge 19 commits into
googleapis:mainfrom
shubhangi-google:add_idempotency_token_modified
Open

shubhangi-google wants to merge 19 commits into
googleapis:mainfrom
shubhangi-google:add_idempotency_token_modified

Conversation

@shubhangi-google

@shubhangi-google shubhangi-google commented Jul 17, 2026

Copy link
Copy Markdown
Contributor

This pull request introduces support for sending an idempotency token header (X-Goog-Gcs-Idempotency-Token) in API commands, enabled by default via the new add_idempotency_token_header option.
linked storage library Pr : googleapis/google-cloud-ruby#34956

@cpriti-os cpriti-os left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AI review: there are two issues.

  1. Global Default Scope Leak (Critical Issue):
    By setting RequestOptions.default.add_idempotency_token_header = true in google/apis/options.rb, this PR enables the GCS-specific X-Goog-Gcs-Idempotency-Token header globally for all 400+ Google API clients using google-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: Default add_idempotency_token_header to false in google-apis-core and explicitly enable it in the google-apis-storage_v1 client generation or configuration.

  2. Redundant Memoization:
    In set_idempotency_token_header, the @idempotency_token ||= SecureRandom.uuid memoization 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.uuid

Because 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.

@shubhangi-google
shubhangi-google marked this pull request as draft September 16, 2026 06:01
@shubhangi-google
shubhangi-google marked this pull request as ready for review September 18, 2026 06:54

def invocation_id_header
"gccl-invocation-id/#{SecureRandom.uuid}"
@invocation_id ||= SecureRandom.uuid

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we making this an instance variable?

.join(' ')
.split
.find_all { |s| s !~ %r{^gl-ruby/|^gdcl/} }
.find_all { |s| s !~ %r{^gl-ruby/|^gdcl/|^gccl-invocation-id/} }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we adding invocation_id header regex here?


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' }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please help me understand how header is populated?

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is adding idempotency token going to be optional?

I thought it was going to be added by default.

it 'should not accumulate duplicate gccl-invocation-id clauses when prepare! is called multiple times' do
command.options.add_invocation_id_header = true
command.prepare!
first_header = command.header['X-Goog-Api-Client']

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test talks about checking duplicity of gccl-invocation-id while over here we're checking X-Goog-Api-Client .
Is this intentional?

command.options.add_invocation_id_header = true
command.prepare!
expect(command.header["X-Goog-Api-Client"]).to include("gccl-invocation-id")
expect(command.header["X-Goog-Api-Client"]).to match(/gccl-invocation-id\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same thing here. We're setting X-Goog-Api-Client to match with gccl-invocation-id value.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would it be nil here?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants