From c553b8692dc2550d316550d5bb12889a3c78fd01 Mon Sep 17 00:00:00 2001 From: Shubhangi Singh Date: Wed, 16 Apr 2025 12:35:42 +0000 Subject: [PATCH 01/12] setting idempotency header --- google-apis-core/lib/google/apis/core/api_command.rb | 10 ++++++++-- google-apis-core/lib/google/apis/options.rb | 5 ++++- .../spec/google/apis/core/api_command_spec.rb | 7 ++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/google-apis-core/lib/google/apis/core/api_command.rb b/google-apis-core/lib/google/apis/core/api_command.rb index 22b9b2df8fa..896d6b4c729 100644 --- a/google-apis-core/lib/google/apis/core/api_command.rb +++ b/google-apis-core/lib/google/apis/core/api_command.rb @@ -18,7 +18,7 @@ require 'google/apis/errors' require 'json' require 'retriable' -require "securerandom" +require 'securerandom' module Google module Apis @@ -70,6 +70,7 @@ def initialize(method, url, body: nil, client_version: nil) def prepare! set_api_client_header set_user_project_header + set_idempotency_token_header if options.add_idempotency_token_header if options&.api_format_version header['X-Goog-Api-Format-Version'] = options.api_format_version.to_s end @@ -143,6 +144,7 @@ def allow_form_encoding? end private + INVOCATION_ID = SecureRandom.uuid def set_api_client_header old_xgac = header @@ -173,9 +175,13 @@ def set_user_project_header end header['X-Goog-User-Project'] = quota_project_id if quota_project_id end + + def set_idempotency_token_header + header['X-Goog-Gcs-Idempotency-Token'] = INVOCATION_ID + end def invocation_id_header - "gccl-invocation-id/#{SecureRandom.uuid}" + "gccl-invocation-id/#{INVOCATION_ID}" end # Attempt to parse a JSON error message diff --git a/google-apis-core/lib/google/apis/options.rb b/google-apis-core/lib/google/apis/options.rb index b5a0c99f4d3..3af43a24aa1 100644 --- a/google-apis-core/lib/google/apis/options.rb +++ b/google-apis-core/lib/google/apis/options.rb @@ -41,7 +41,9 @@ module Apis :quota_project, :query, :add_invocation_id_header, - :upload_chunk_size) + :upload_chunk_size, + :add_idempotency_token_header + ) # General client options class ClientOptions @@ -139,5 +141,6 @@ def merge(options) 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 = true end end diff --git a/google-apis-core/spec/google/apis/core/api_command_spec.rb b/google-apis-core/spec/google/apis/core/api_command_spec.rb index eeaf3e23803..ca91ac6f8d4 100644 --- a/google-apis-core/spec/google/apis/core/api_command_spec.rb +++ b/google-apis-core/spec/google/apis/core/api_command_spec.rb @@ -34,7 +34,6 @@ let(:client_version) { "1.2.3" } let(:x_goog_api_client_value) { "gl-ruby/#{RUBY_VERSION} gdcl/#{client_version}" } - context('with preparation') do let(:command) do Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals', client_version: client_version) @@ -90,6 +89,12 @@ command.prepare! expect(command.header["X-Goog-Api-Client"]).to include("gccl-invocation-id") end + + it "should set the X-Goog-Gcs-Idempotency-Token header" do + command.options.add_idempotency_token_header = true + command.prepare! + expect(command.header['X-Goog-Gcs-Idempotency-Token']).not_to be_nil + end end context('with a request body') do From 2f5a45aea03500a40c0322175800e38cab971c3b Mon Sep 17 00:00:00 2001 From: Shubhangi Singh Date: Thu, 17 Apr 2025 11:30:55 +0000 Subject: [PATCH 02/12] setting new token value --- google-apis-core/lib/google/apis/core/api_command.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/google-apis-core/lib/google/apis/core/api_command.rb b/google-apis-core/lib/google/apis/core/api_command.rb index 896d6b4c729..3aef30ad048 100644 --- a/google-apis-core/lib/google/apis/core/api_command.rb +++ b/google-apis-core/lib/google/apis/core/api_command.rb @@ -144,7 +144,6 @@ def allow_form_encoding? end private - INVOCATION_ID = SecureRandom.uuid def set_api_client_header old_xgac = header @@ -177,11 +176,11 @@ def set_user_project_header end def set_idempotency_token_header - header['X-Goog-Gcs-Idempotency-Token'] = INVOCATION_ID + header['X-Goog-Gcs-Idempotency-Token'] = SecureRandom.uuid end def invocation_id_header - "gccl-invocation-id/#{INVOCATION_ID}" + "gccl-invocation-id/#{SecureRandom.uuid}" end # Attempt to parse a JSON error message From a7a03f46eb58c44cfaf91a4217533ad543981851 Mon Sep 17 00:00:00 2001 From: Shubhangi Singh Date: Wed, 23 Apr 2025 12:17:11 +0000 Subject: [PATCH 03/12] fixing spec --- google-apis-core/spec/google/apis/core/service_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/google-apis-core/spec/google/apis/core/service_spec.rb b/google-apis-core/spec/google/apis/core/service_spec.rb index 53167a12700..1e175184242 100644 --- a/google-apis-core/spec/google/apis/core/service_spec.rb +++ b/google-apis-core/spec/google/apis/core/service_spec.rb @@ -341,6 +341,7 @@ POST /upload/zoo/animals\\? HTTP/1\\.1 X-Goog-Api-Client: #{Regexp.escape(x_goog_api_client_value)} +X-Goog-Gcs-Idempotency-Token: b1981e17-f622-49af-b2eb-203308b1b17d Content-Type: multipart/related; boundary=inner X-Goog-Upload-Protocol: multipart Authorization: Bearer a token From b53b80d744aaa2b83cc40c41897415a9cac56170 Mon Sep 17 00:00:00 2001 From: Shubhangi Singh Date: Fri, 2 May 2025 07:06:39 +0000 Subject: [PATCH 04/12] checking token in retry case --- .../spec/google/apis/core/api_command_spec.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/google-apis-core/spec/google/apis/core/api_command_spec.rb b/google-apis-core/spec/google/apis/core/api_command_spec.rb index ca91ac6f8d4..4efe04a5bbd 100644 --- a/google-apis-core/spec/google/apis/core/api_command_spec.rb +++ b/google-apis-core/spec/google/apis/core/api_command_spec.rb @@ -255,11 +255,17 @@ command.options.add_invocation_id_header = true result = command.execute(client) invocation_id_header = command.header["X-Goog-Api-Client"] - expect(invocation_id_header).to include("gccl-invocation-id") expect(a_request(:get, 'https://www.googleapis.com/zoo/animals') .with { |req| req.headers['X-Goog-Api-Client'] == invocation_id_header }).to have_been_made.times(2) end + + it 'should keep same idempotency_token across retries' do + command.options.add_idempotency_token_header = true + result = command.execute(client) + idempotency_token_header = command.header['X-Goog-Gcs-Idempotency-Token'] + expect(command.header['X-Goog-Gcs-Idempotency-Token']).to eq(idempotency_token_header) + end end context('with a project not linked response') do From cd1a7119dfb3ec3cb68a9c8ea4e0f45b50a04916 Mon Sep 17 00:00:00 2001 From: Shubhangi Singh Date: Tue, 20 May 2025 11:09:02 +0000 Subject: [PATCH 05/12] updating token value --- google-apis-core/lib/google/apis/core/api_command.rb | 8 ++++---- .../spec/google/apis/core/api_command_spec.rb | 8 ++++++++ google-apis-core/spec/google/apis/core/service_spec.rb | 4 +++- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/google-apis-core/lib/google/apis/core/api_command.rb b/google-apis-core/lib/google/apis/core/api_command.rb index 3aef30ad048..35f64c2fb47 100644 --- a/google-apis-core/lib/google/apis/core/api_command.rb +++ b/google-apis-core/lib/google/apis/core/api_command.rb @@ -18,7 +18,6 @@ require 'google/apis/errors' require 'json' require 'retriable' -require 'securerandom' module Google module Apis @@ -144,6 +143,7 @@ def allow_form_encoding? end private + INVOCATION_ID = SecureRandom.uuid def set_api_client_header old_xgac = header @@ -174,13 +174,13 @@ def set_user_project_header end header['X-Goog-User-Project'] = quota_project_id if quota_project_id end - + def set_idempotency_token_header - header['X-Goog-Gcs-Idempotency-Token'] = SecureRandom.uuid + header['X-Goog-Gcs-Idempotency-Token'] = INVOCATION_ID end def invocation_id_header - "gccl-invocation-id/#{SecureRandom.uuid}" + "gccl-invocation-id/#{INVOCATION_ID}" end # Attempt to parse a JSON error message diff --git a/google-apis-core/spec/google/apis/core/api_command_spec.rb b/google-apis-core/spec/google/apis/core/api_command_spec.rb index 4efe04a5bbd..f886081b6b1 100644 --- a/google-apis-core/spec/google/apis/core/api_command_spec.rb +++ b/google-apis-core/spec/google/apis/core/api_command_spec.rb @@ -38,6 +38,11 @@ let(:command) do Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals', client_version: client_version) end + let(:invocation_id) { 'test123' } + + before(:example) do + Google::Apis::Core::ApiCommand.const_set(:INVOCATION_ID, invocation_id) + end it 'should set X-Goog-Api-Client header if none is set' do command.prepare! @@ -88,12 +93,15 @@ 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 include(invocation_id) + end it "should set the X-Goog-Gcs-Idempotency-Token header" do command.options.add_idempotency_token_header = true command.prepare! expect(command.header['X-Goog-Gcs-Idempotency-Token']).not_to be_nil + expect(command.header['X-Goog-Gcs-Idempotency-Token']).to eql invocation_id end end diff --git a/google-apis-core/spec/google/apis/core/service_spec.rb b/google-apis-core/spec/google/apis/core/service_spec.rb index 1e175184242..773a03ed553 100644 --- a/google-apis-core/spec/google/apis/core/service_spec.rb +++ b/google-apis-core/spec/google/apis/core/service_spec.rb @@ -25,6 +25,7 @@ let(:service_ud) { Google::Apis::Core::BaseService.new('https://www.$UNIVERSE_DOMAIN$/', '', client_version: client_version) } let(:service_with_base_path) { Google::Apis::Core::BaseService.new('https://www.googleapis.com/', 'my_service/v1/', client_version: client_version) } let(:x_goog_api_client_value) { "gl-ruby/#{RUBY_VERSION} gdcl/#{client_version}" } + let(:invocation_id) { 'test123' } before do Google::Apis::ClientOptions.default.application_name = 'test' @@ -285,6 +286,7 @@ context 'with batch uploads' do before(:example) do + Google::Apis::Core::ApiCommand.const_set(:INVOCATION_ID, invocation_id) allow(SecureRandom).to receive(:uuid).and_return('b1981e17-f622-49af-b2eb-203308b1b17d') allow(Digest::SHA1).to receive(:hexdigest).and_return('outer', 'inner') response = < Date: Wed, 21 May 2025 14:26:24 +0000 Subject: [PATCH 06/12] updating test case --- google-apis-core/spec/google/apis/core/api_command_spec.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/google-apis-core/spec/google/apis/core/api_command_spec.rb b/google-apis-core/spec/google/apis/core/api_command_spec.rb index f886081b6b1..13306b5775f 100644 --- a/google-apis-core/spec/google/apis/core/api_command_spec.rb +++ b/google-apis-core/spec/google/apis/core/api_command_spec.rb @@ -273,6 +273,9 @@ result = command.execute(client) idempotency_token_header = command.header['X-Goog-Gcs-Idempotency-Token'] expect(command.header['X-Goog-Gcs-Idempotency-Token']).to eq(idempotency_token_header) + expect(a_request(:get, 'https://www.googleapis.com/zoo/animals') + .with { |req| req.headers['X-Goog-Gcs-Idempotency-Token'] == idempotency_token_header }) + .to have_been_made.times(2) end end From f032d766140fff810a463eaface1baa65c4cd693 Mon Sep 17 00:00:00 2001 From: Shubhangi Singh Date: Mon, 26 May 2025 10:24:00 +0000 Subject: [PATCH 07/12] removing not needed code --- google-apis-core/lib/google/apis/options.rb | 1 - google-apis-core/spec/google/apis/core/service_spec.rb | 3 --- 2 files changed, 4 deletions(-) diff --git a/google-apis-core/lib/google/apis/options.rb b/google-apis-core/lib/google/apis/options.rb index 3af43a24aa1..9af67d38ac5 100644 --- a/google-apis-core/lib/google/apis/options.rb +++ b/google-apis-core/lib/google/apis/options.rb @@ -141,6 +141,5 @@ def merge(options) 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 = true end end diff --git a/google-apis-core/spec/google/apis/core/service_spec.rb b/google-apis-core/spec/google/apis/core/service_spec.rb index 773a03ed553..53167a12700 100644 --- a/google-apis-core/spec/google/apis/core/service_spec.rb +++ b/google-apis-core/spec/google/apis/core/service_spec.rb @@ -25,7 +25,6 @@ let(:service_ud) { Google::Apis::Core::BaseService.new('https://www.$UNIVERSE_DOMAIN$/', '', client_version: client_version) } let(:service_with_base_path) { Google::Apis::Core::BaseService.new('https://www.googleapis.com/', 'my_service/v1/', client_version: client_version) } let(:x_goog_api_client_value) { "gl-ruby/#{RUBY_VERSION} gdcl/#{client_version}" } - let(:invocation_id) { 'test123' } before do Google::Apis::ClientOptions.default.application_name = 'test' @@ -286,7 +285,6 @@ context 'with batch uploads' do before(:example) do - Google::Apis::Core::ApiCommand.const_set(:INVOCATION_ID, invocation_id) allow(SecureRandom).to receive(:uuid).and_return('b1981e17-f622-49af-b2eb-203308b1b17d') allow(Digest::SHA1).to receive(:hexdigest).and_return('outer', 'inner') response = < Date: Mon, 26 May 2025 12:08:52 +0000 Subject: [PATCH 08/12] removing idempotency token header from options --- google-apis-core/lib/google/apis/core/api_command.rb | 2 +- google-apis-core/lib/google/apis/options.rb | 3 +-- google-apis-core/spec/google/apis/core/api_command_spec.rb | 3 --- google-apis-core/spec/google/apis/core/service_spec.rb | 3 +++ 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/google-apis-core/lib/google/apis/core/api_command.rb b/google-apis-core/lib/google/apis/core/api_command.rb index 35f64c2fb47..b84d0b06a64 100644 --- a/google-apis-core/lib/google/apis/core/api_command.rb +++ b/google-apis-core/lib/google/apis/core/api_command.rb @@ -69,7 +69,7 @@ def initialize(method, url, body: nil, client_version: nil) def prepare! set_api_client_header set_user_project_header - set_idempotency_token_header if options.add_idempotency_token_header + set_idempotency_token_header if options&.api_format_version header['X-Goog-Api-Format-Version'] = options.api_format_version.to_s end diff --git a/google-apis-core/lib/google/apis/options.rb b/google-apis-core/lib/google/apis/options.rb index 9af67d38ac5..f1ac437c9ed 100644 --- a/google-apis-core/lib/google/apis/options.rb +++ b/google-apis-core/lib/google/apis/options.rb @@ -41,8 +41,7 @@ module Apis :quota_project, :query, :add_invocation_id_header, - :upload_chunk_size, - :add_idempotency_token_header + :upload_chunk_size ) # General client options diff --git a/google-apis-core/spec/google/apis/core/api_command_spec.rb b/google-apis-core/spec/google/apis/core/api_command_spec.rb index 13306b5775f..e9bc1b4f1ad 100644 --- a/google-apis-core/spec/google/apis/core/api_command_spec.rb +++ b/google-apis-core/spec/google/apis/core/api_command_spec.rb @@ -98,7 +98,6 @@ end it "should set the X-Goog-Gcs-Idempotency-Token header" do - command.options.add_idempotency_token_header = true command.prepare! expect(command.header['X-Goog-Gcs-Idempotency-Token']).not_to be_nil expect(command.header['X-Goog-Gcs-Idempotency-Token']).to eql invocation_id @@ -269,10 +268,8 @@ end it 'should keep same idempotency_token across retries' do - command.options.add_idempotency_token_header = true result = command.execute(client) idempotency_token_header = command.header['X-Goog-Gcs-Idempotency-Token'] - expect(command.header['X-Goog-Gcs-Idempotency-Token']).to eq(idempotency_token_header) expect(a_request(:get, 'https://www.googleapis.com/zoo/animals') .with { |req| req.headers['X-Goog-Gcs-Idempotency-Token'] == idempotency_token_header }) .to have_been_made.times(2) diff --git a/google-apis-core/spec/google/apis/core/service_spec.rb b/google-apis-core/spec/google/apis/core/service_spec.rb index 05208b97488..855c28cc7e1 100644 --- a/google-apis-core/spec/google/apis/core/service_spec.rb +++ b/google-apis-core/spec/google/apis/core/service_spec.rb @@ -25,6 +25,7 @@ let(:service_ud) { Google::Apis::Core::BaseService.new('https://www.$UNIVERSE_DOMAIN$/', '', client_version: client_version) } let(:service_with_base_path) { Google::Apis::Core::BaseService.new('https://www.googleapis.com/', 'my_service/v1/', client_version: client_version) } let(:x_goog_api_client_value) { "gl-ruby/#{RUBY_VERSION} gdcl/#{client_version}" } + let(:invocation_id) { 'test123' } before do Google::Apis::ClientOptions.default.application_name = 'test' @@ -374,6 +375,7 @@ context 'with batch uploads' do before(:example) do + Google::Apis::Core::ApiCommand.const_set(:INVOCATION_ID, invocation_id) allow(SecureRandom).to receive(:uuid).and_return('b1981e17-f622-49af-b2eb-203308b1b17d') allow(Digest::SHA1).to receive(:hexdigest).and_return('outer', 'inner') response = < Date: Wed, 8 Jul 2026 06:41:10 +0000 Subject: [PATCH 09/12] wip --- .../lib/google/apis/core/api_command.rb | 9 +++-- google-apis-core/lib/google/apis/options.rb | 6 +++- .../spec/google/apis/core/api_command_spec.rb | 34 ++++++++++++++----- .../spec/google/apis/core/service_spec.rb | 7 ++-- .../spec/google/apis/options_spec.rb | 1 + 5 files changed, 40 insertions(+), 17 deletions(-) diff --git a/google-apis-core/lib/google/apis/core/api_command.rb b/google-apis-core/lib/google/apis/core/api_command.rb index b84d0b06a64..859b29bc2ed 100644 --- a/google-apis-core/lib/google/apis/core/api_command.rb +++ b/google-apis-core/lib/google/apis/core/api_command.rb @@ -18,6 +18,7 @@ require 'google/apis/errors' require 'json' require 'retriable' +require 'securerandom' module Google module Apis @@ -61,6 +62,7 @@ class ApiCommand < HttpCommand def initialize(method, url, body: nil, client_version: nil) super(method, url, body: body) self.client_version = client_version || Core::VERSION + @idempotency_token = SecureRandom.uuid end # Serialize the request body @@ -69,7 +71,7 @@ def initialize(method, url, body: nil, client_version: nil) def prepare! set_api_client_header set_user_project_header - set_idempotency_token_header + set_idempotency_token_header if options.add_idempotency_token_header if options&.api_format_version header['X-Goog-Api-Format-Version'] = options.api_format_version.to_s end @@ -176,7 +178,10 @@ def set_user_project_header end def set_idempotency_token_header - header['X-Goog-Gcs-Idempotency-Token'] = INVOCATION_ID + return if options&.header&.keys&.any? { |k| k.to_s.downcase == 'x-goog-gcs-idempotency-token' } + return if header.keys.any? { |k| k.to_s.downcase == 'x-goog-gcs-idempotency-token' } + + header['X-Goog-Gcs-Idempotency-Token'] = @idempotency_token end def invocation_id_header diff --git a/google-apis-core/lib/google/apis/options.rb b/google-apis-core/lib/google/apis/options.rb index f1ac437c9ed..2f01760dd8c 100644 --- a/google-apis-core/lib/google/apis/options.rb +++ b/google-apis-core/lib/google/apis/options.rb @@ -41,7 +41,8 @@ module Apis :quota_project, :query, :add_invocation_id_header, - :upload_chunk_size + :upload_chunk_size, + :add_idempotency_token_header ) # General client options @@ -104,6 +105,8 @@ class RequestOptions # @return [Boolean] True if the header gccl-invocation-id need to be set # @!attribute [rw] upload_chunk_size # @return [Integer] The chunk size of storage upload. The default value is 100 MB. + # @!attribute [rw] add_idempotency_token_header + # @return [Boolean] Flag to control whether the X-Goog-Gcs-Idempotency-Token is sent. # Get the default options # @return [Google::Apis::RequestOptions] @@ -140,5 +143,6 @@ def merge(options) 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 = true end end diff --git a/google-apis-core/spec/google/apis/core/api_command_spec.rb b/google-apis-core/spec/google/apis/core/api_command_spec.rb index e9bc1b4f1ad..6180e603fc3 100644 --- a/google-apis-core/spec/google/apis/core/api_command_spec.rb +++ b/google-apis-core/spec/google/apis/core/api_command_spec.rb @@ -38,11 +38,6 @@ let(:command) do Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals', client_version: client_version) end - let(:invocation_id) { 'test123' } - - before(:example) do - Google::Apis::Core::ApiCommand.const_set(:INVOCATION_ID, invocation_id) - end it 'should set X-Goog-Api-Client header if none is set' do command.prepare! @@ -92,15 +87,36 @@ it "should set the gccl-invocation-id to a random UUID" do 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 include(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}/) end it "should set the X-Goog-Gcs-Idempotency-Token header" do command.prepare! expect(command.header['X-Goog-Gcs-Idempotency-Token']).not_to be_nil - expect(command.header['X-Goog-Gcs-Idempotency-Token']).to eql invocation_id + expect(command.header['X-Goog-Gcs-Idempotency-Token']).to match(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/) + end + + it "should not set the X-Goog-Gcs-Idempotency-Token header when option is false" do + command.options.add_idempotency_token_header = false + command.prepare! + expect(command.header['X-Goog-Gcs-Idempotency-Token']).to be_nil + end + + it 'should generate different tokens for different command instances' do + cmd1 = Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals') + cmd2 = Google::Apis::Core::ApiCommand.new(:get, 'https://www.googleapis.com/zoo/animals') + + cmd1.prepare! + cmd2.prepare! + + expect(cmd1.header['X-Goog-Gcs-Idempotency-Token']).not_to eq(cmd2.header['X-Goog-Gcs-Idempotency-Token']) + end + + it "should respect custom X-Goog-Gcs-Idempotency-Token in options.header regardless of casing" do + 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 end end diff --git a/google-apis-core/spec/google/apis/core/service_spec.rb b/google-apis-core/spec/google/apis/core/service_spec.rb index 855c28cc7e1..630e29ec46f 100644 --- a/google-apis-core/spec/google/apis/core/service_spec.rb +++ b/google-apis-core/spec/google/apis/core/service_spec.rb @@ -25,7 +25,6 @@ let(:service_ud) { Google::Apis::Core::BaseService.new('https://www.$UNIVERSE_DOMAIN$/', '', client_version: client_version) } let(:service_with_base_path) { Google::Apis::Core::BaseService.new('https://www.googleapis.com/', 'my_service/v1/', client_version: client_version) } let(:x_goog_api_client_value) { "gl-ruby/#{RUBY_VERSION} gdcl/#{client_version}" } - let(:invocation_id) { 'test123' } before do Google::Apis::ClientOptions.default.application_name = 'test' @@ -375,8 +374,6 @@ context 'with batch uploads' do before(:example) do - Google::Apis::Core::ApiCommand.const_set(:INVOCATION_ID, invocation_id) - allow(SecureRandom).to receive(:uuid).and_return('b1981e17-f622-49af-b2eb-203308b1b17d') allow(Digest::SHA1).to receive(:hexdigest).and_return('outer', 'inner') response = < +Content-Id: <[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\+0> Content-Length: \\d+ Content-Transfer-Encoding: binary POST /upload/zoo/animals\\? HTTP/1\\.1 X-Goog-Api-Client: #{Regexp.escape(x_goog_api_client_value)} -X-Goog-Gcs-Idempotency-Token: #{invocation_id} +X-Goog-Gcs-Idempotency-Token: [0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12} Content-Type: multipart/related; boundary=inner X-Goog-Upload-Protocol: multipart Authorization: Bearer a token diff --git a/google-apis-core/spec/google/apis/options_spec.rb b/google-apis-core/spec/google/apis/options_spec.rb index fbf700528eb..7d1340d0b30 100644 --- a/google-apis-core/spec/google/apis/options_spec.rb +++ b/google-apis-core/spec/google/apis/options_spec.rb @@ -52,5 +52,6 @@ expect(defaults.use_opencensus).to be true expect(defaults.quota_project).to be_nil expect(defaults.upload_chunk_size).to eq(100 * 1024 * 1024) + expect(defaults.add_idempotency_token_header).to be true end end From af8dd7d62dab2945ef833251c45b173efc31124d Mon Sep 17 00:00:00 2001 From: Shubhangi Singh Date: Wed, 8 Jul 2026 10:47:49 +0000 Subject: [PATCH 10/12] adding lazzy load --- google-apis-core/lib/google/apis/core/api_command.rb | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/google-apis-core/lib/google/apis/core/api_command.rb b/google-apis-core/lib/google/apis/core/api_command.rb index 859b29bc2ed..c2e01693f1a 100644 --- a/google-apis-core/lib/google/apis/core/api_command.rb +++ b/google-apis-core/lib/google/apis/core/api_command.rb @@ -62,7 +62,6 @@ class ApiCommand < HttpCommand def initialize(method, url, body: nil, client_version: nil) super(method, url, body: body) self.client_version = client_version || Core::VERSION - @idempotency_token = SecureRandom.uuid end # Serialize the request body @@ -71,7 +70,7 @@ def initialize(method, url, body: nil, client_version: nil) def prepare! set_api_client_header set_user_project_header - set_idempotency_token_header if options.add_idempotency_token_header + set_idempotency_token_header if options&.add_idempotency_token_header if options&.api_format_version header['X-Goog-Api-Format-Version'] = options.api_format_version.to_s end @@ -145,7 +144,6 @@ def allow_form_encoding? end private - INVOCATION_ID = SecureRandom.uuid def set_api_client_header old_xgac = header @@ -178,14 +176,16 @@ def set_user_project_header end def set_idempotency_token_header - return if options&.header&.keys&.any? { |k| k.to_s.downcase == 'x-goog-gcs-idempotency-token' } - return if header.keys.any? { |k| k.to_s.downcase == 'x-goog-gcs-idempotency-token' } + 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' } + @idempotency_token ||= SecureRandom.uuid header['X-Goog-Gcs-Idempotency-Token'] = @idempotency_token end def invocation_id_header - "gccl-invocation-id/#{INVOCATION_ID}" + @invocation_id ||= SecureRandom.uuid + "gccl-invocation-id/#{@invocation_id}" end # Attempt to parse a JSON error message From fa8210463171f9bd514144364da44cfb969f935b Mon Sep 17 00:00:00 2001 From: Shubhangi Singh Date: Fri, 10 Jul 2026 05:56:00 +0000 Subject: [PATCH 11/12] additional changes --- google-apis-core/lib/google/apis/core/api_command.rb | 2 +- .../spec/google/apis/core/api_command_spec.rb | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/google-apis-core/lib/google/apis/core/api_command.rb b/google-apis-core/lib/google/apis/core/api_command.rb index c2e01693f1a..fd8c66d4b08 100644 --- a/google-apis-core/lib/google/apis/core/api_command.rb +++ b/google-apis-core/lib/google/apis/core/api_command.rb @@ -151,7 +151,7 @@ def set_api_client_header .map { |(a, b)| b } .join(' ') .split - .find_all { |s| s !~ %r{^gl-ruby/|^gdcl/} } + .find_all { |s| s !~ %r{^gl-ruby/|^gdcl/|^gccl-invocation-id/} } .join(' ') # Report 0.x.y versions that are in separate packages as 1.x.y. # Thus, reported gdcl/0.x.y versions are monopackage clients, while diff --git a/google-apis-core/spec/google/apis/core/api_command_spec.rb b/google-apis-core/spec/google/apis/core/api_command_spec.rb index 6180e603fc3..0cf330de980 100644 --- a/google-apis-core/spec/google/apis/core/api_command_spec.rb +++ b/google-apis-core/spec/google/apis/core/api_command_spec.rb @@ -66,6 +66,14 @@ expect(command.header['x-goog-api-client']).to be nil end + 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'] + command.prepare! + expect(command.header['X-Goog-Api-Client']).to eql first_header + end + it 'should not set the X-Goog-User-Project header if there is no quota_project' do command.prepare! expect(command.header['X-Goog-User-Project']).to be_nil From 3c6fa0720c7352636ec619278d2843f5ff9e9f9c Mon Sep 17 00:00:00 2001 From: Shubhangi Singh Date: Fri, 10 Jul 2026 08:37:01 +0000 Subject: [PATCH 12/12] Merge branch 'main' into add_idempotency_token_modified