From 0629b7f844c87ab50a716031347e9eb2722f203c Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Fri, 3 Jul 2026 09:18:08 +0000 Subject: [PATCH 1/7] feat(storage): add x-goog-gcs-idempotency-token header linked to gccl-invocation-id for request tracking --- .../storage/src/nodejs-common/service.ts | 4 +- handwritten/storage/src/nodejs-common/util.ts | 4 +- handwritten/storage/src/resumable-upload.ts | 3 ++ handwritten/storage/test/headers.ts | 48 +++++++++++++++---- .../storage/test/nodejs-common/service.ts | 19 ++++++++ .../storage/test/nodejs-common/util.ts | 13 +++-- handwritten/storage/test/resumable-upload.ts | 36 ++++++++++++++ 7 files changed, 111 insertions(+), 16 deletions(-) diff --git a/handwritten/storage/src/nodejs-common/service.ts b/handwritten/storage/src/nodejs-common/service.ts index 9173a38f73d7..24610ca1dfed 100644 --- a/handwritten/storage/src/nodejs-common/service.ts +++ b/handwritten/storage/src/nodejs-common/service.ts @@ -269,12 +269,14 @@ export class Service { if (this.providedUserAgent) { userAgent = `${this.providedUserAgent} ${userAgent}`; } + const idempotencyToken = crypto.randomUUID(); reqOpts.headers = { ...reqOpts.headers, 'User-Agent': userAgent, 'x-goog-api-client': `${getRuntimeTrackingString()} gccl/${ pkg.version - }-${getModuleFormat()} gccl-invocation-id/${crypto.randomUUID()}`, + }-${getModuleFormat()} gccl-invocation-id/${idempotencyToken}`, + 'x-goog-gcs-idempotency-token': idempotencyToken, }; if (reqOpts[GCCL_GCS_CMD_KEY]) { diff --git a/handwritten/storage/src/nodejs-common/util.ts b/handwritten/storage/src/nodejs-common/util.ts index e6c4db98b095..f0c4c7f419b2 100644 --- a/handwritten/storage/src/nodejs-common/util.ts +++ b/handwritten/storage/src/nodejs-common/util.ts @@ -1042,11 +1042,13 @@ export class Util { } _getDefaultHeaders(gcclGcsCmd?: string) { + const idempotencyToken = crypto.randomUUID(); const headers = { 'User-Agent': getUserAgentString(), 'x-goog-api-client': `${getRuntimeTrackingString()} gccl/${ packageJson.version - }-${getModuleFormat()} gccl-invocation-id/${crypto.randomUUID()}`, + }-${getModuleFormat()} gccl-invocation-id/${idempotencyToken}`, + 'x-goog-gcs-idempotency-token': idempotencyToken, }; if (gcclGcsCmd) { diff --git a/handwritten/storage/src/resumable-upload.ts b/handwritten/storage/src/resumable-upload.ts index 9ebbb6f37a85..16e84351e059 100644 --- a/handwritten/storage/src/resumable-upload.ts +++ b/handwritten/storage/src/resumable-upload.ts @@ -829,6 +829,7 @@ export class Upload extends Writable { headers: { 'User-Agent': getUserAgentString(), 'x-goog-api-client': googAPIClient, + 'x-goog-gcs-idempotency-token': this.currentInvocationId.uri, ...headers, }, }; @@ -1008,6 +1009,7 @@ export class Upload extends Writable { const headers: GaxiosOptions['headers'] = { 'User-Agent': getUserAgentString(), 'x-goog-api-client': googAPIClient, + 'x-goog-gcs-idempotency-token': this.currentInvocationId.chunk, }; // If using multiple chunk upload, set appropriate header @@ -1227,6 +1229,7 @@ export class Upload extends Writable { 'Content-Range': 'bytes */*', 'User-Agent': getUserAgentString(), 'x-goog-api-client': googAPIClient, + 'x-goog-gcs-idempotency-token': this.currentInvocationId.checkUploadStatus, }, }; diff --git a/handwritten/storage/test/headers.ts b/handwritten/storage/test/headers.ts index 9ccc685814bb..a19e56556597 100644 --- a/handwritten/storage/test/headers.ts +++ b/handwritten/storage/test/headers.ts @@ -65,11 +65,15 @@ describe('headers', () => { } catch (err) { if (err !== error) throw err; } - assert.ok( - /^gl-node\/(?[^W]+) gccl\/(?[^W]+) gccl-invocation-id\/(?[^W]+)$/.test( - requests[0].headers['x-goog-api-client'] - ) - ); + const apiClientHeader = requests[0].headers['x-goog-api-client']; + const match = + /^gl-node\/(?[^W]+) gccl\/(?[^W]+) gccl-invocation-id\/(?[^W]+)$/.exec( + apiClientHeader + ); + assert.ok(match); + const invocationId = match.groups!.gcclInvocationId; + const idempotencyToken = requests[0].headers['x-goog-gcs-idempotency-token']; + assert.strictEqual(idempotencyToken, invocationId); }); it('populates x-goog-api-client header (deno)', async () => { @@ -87,10 +91,34 @@ describe('headers', () => { } catch (err) { if (err !== error) throw err; } - assert.ok( - /^gl-deno\/0.00.0 gccl\/(?[^W]+) gccl-invocation-id\/(?[^W]+)$/.test( - requests[1].headers['x-goog-api-client'] - ) - ); + const apiClientHeader = requests[1].headers['x-goog-api-client']; + const match = + /^gl-deno\/0.00.0 gccl\/(?[^W]+) gccl-invocation-id\/(?[^W]+)$/.exec( + apiClientHeader + ); + assert.ok(match); + const invocationId = match.groups!.gcclInvocationId; + const idempotencyToken = requests[1].headers['x-goog-gcs-idempotency-token']; + assert.strictEqual(idempotencyToken, invocationId); + }); + + it('generates unique tokens for different requests', async () => { + const storage = new Storage(); + const bucket = storage.bucket('foo-bucket'); + try { + await bucket.create(); + } catch (err) { + if (err !== error) throw err; + } + try { + await bucket.create(); + } catch (err) { + if (err !== error) throw err; + } + const token1 = requests[requests.length - 2].headers['x-goog-gcs-idempotency-token']; + const token2 = requests[requests.length - 1].headers['x-goog-gcs-idempotency-token']; + assert.ok(token1); + assert.ok(token2); + assert.notStrictEqual(token1, token2); }); }); diff --git a/handwritten/storage/test/nodejs-common/service.ts b/handwritten/storage/test/nodejs-common/service.ts index 502c4e5419f9..b830c65eb680 100644 --- a/handwritten/storage/test/nodejs-common/service.ts +++ b/handwritten/storage/test/nodejs-common/service.ts @@ -487,6 +487,25 @@ describe('Service', () => { service.request_(reqOpts, assert.ifError); }); + it('should add the x-goog-gcs-idempotency-token header matching the gccl-invocation-id', done => { + service.makeAuthenticatedRequest = (reqOpts: DecorateRequestOptions) => { + const pkg = service.packageJson; + const r = new RegExp( + `^gl-node/${process.versions.node} gccl/${ + pkg.version + }-${getModuleFormat()} gccl-invocation-id/(?[^W]+)$` + ); + const match = r.exec(reqOpts.headers!['x-goog-api-client']); + assert.ok(match); + const invocationId = match.groups!.gcclInvocationId; + const idempotencyToken = reqOpts.headers!['x-goog-gcs-idempotency-token']; + assert.strictEqual(idempotencyToken, invocationId); + done(); + }; + + service.request_(reqOpts, assert.ifError); + }); + it('should add the `gccl-gcs-cmd` to the api-client header when provided', done => { const expected = 'example.expected/value'; service.makeAuthenticatedRequest = (reqOpts: DecorateRequestOptions) => { diff --git a/handwritten/storage/test/nodejs-common/util.ts b/handwritten/storage/test/nodejs-common/util.ts index 88e6905ffb52..cf3a9ef47d25 100644 --- a/handwritten/storage/test/nodejs-common/util.ts +++ b/handwritten/storage/test/nodejs-common/util.ts @@ -77,10 +77,15 @@ function fakeRequest() { } fakeRequest.defaults = (defaults: r.CoreOptions) => { - assert.ok( - /^gl-node\/(?[^W]+) gccl\/(?[^W]+) gccl-invocation-id\/(?[^W]+)$/.test( - defaults.headers!['x-goog-api-client'] - ) + const match = + /^gl-node\/(?[^W]+) gccl\/(?[^W]+) gccl-invocation-id\/(?[^W]+)$/.exec( + defaults.headers!['x-goog-api-client'] as string + ); + assert.ok(match); + const invocationId = match.groups!.gcclInvocationId; + assert.strictEqual( + defaults.headers!['x-goog-gcs-idempotency-token'], + invocationId ); return fakeRequest; }; diff --git a/handwritten/storage/test/resumable-upload.ts b/handwritten/storage/test/resumable-upload.ts index 381044d64d9d..7f260248224a 100644 --- a/handwritten/storage/test/resumable-upload.ts +++ b/handwritten/storage/test/resumable-upload.ts @@ -877,12 +877,48 @@ describe('resumable-upload', () => { delete metadataNoHeaders.contentLength; delete metadataNoHeaders.contentType; assert.deepStrictEqual(reqOpts.data, metadataNoHeaders); + assert(reqOpts.headers); + const apiClientHeader = reqOpts.headers['x-goog-api-client']; + const match = X_GOOG_API_HEADER_REGEX.exec(apiClientHeader as string); + assert.ok(match); + const invocationId = match.groups!.gcclInvocationId; + const idempotencyToken = reqOpts.headers['x-goog-gcs-idempotency-token']; + assert.strictEqual(idempotencyToken, invocationId); done(); return {headers: {location: '/foo'}}; }; up.createURI(); }); + it('should reuse the same x-goog-gcs-idempotency-token on retry of createURI', async () => { + let invocationCount = 0; + let token1 = ''; + let token2 = ''; + + up.makeRequest = async (reqOpts: GaxiosOptions) => { + invocationCount++; + assert(reqOpts.headers); + if (invocationCount === 1) { + token1 = reqOpts.headers['x-goog-gcs-idempotency-token'] as string; + const error = new GaxiosError( + 'Retriable error', + {} as GaxiosOptions, + { status: 500 } as GaxiosResponse + ); + throw error; + } else if (invocationCount === 2) { + token2 = reqOpts.headers['x-goog-gcs-idempotency-token'] as string; + return { headers: { location: '/foo' } }; + } + return { headers: { location: '/foo' } }; + }; + + await up.createURI(); + assert.strictEqual(invocationCount, 2); + assert.ok(token1); + assert.strictEqual(token1, token2); + }); + it('should pass through the KMS key name', done => { const kmsKeyName = 'kms-key-name'; const up = upload({ From 86c1c743fc484a185bbdbc9a002f99a8fda04cd3 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Fri, 3 Jul 2026 11:19:34 +0000 Subject: [PATCH 2/7] feat: allow user-provided x-goog-gcs-idempotency-token and synchronize with gccl-invocation-id header --- .../storage/src/nodejs-common/service.ts | 14 +++++-- handwritten/storage/src/resumable-upload.ts | 42 +++++++++++++++++-- .../storage/test/nodejs-common/service.ts | 30 +++++++++++++ handwritten/storage/test/resumable-upload.ts | 25 +++++++++++ 4 files changed, 104 insertions(+), 7 deletions(-) diff --git a/handwritten/storage/src/nodejs-common/service.ts b/handwritten/storage/src/nodejs-common/service.ts index 24610ca1dfed..b3087172bca6 100644 --- a/handwritten/storage/src/nodejs-common/service.ts +++ b/handwritten/storage/src/nodejs-common/service.ts @@ -269,15 +269,23 @@ export class Service { if (this.providedUserAgent) { userAgent = `${this.providedUserAgent} ${userAgent}`; } - const idempotencyToken = crypto.randomUUID(); + const headers = reqOpts.headers || {}; + const userTokenKey = Object.keys(headers).find( + key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' + ); + const idempotencyToken = userTokenKey + ? (headers[userTokenKey] as string) + : crypto.randomUUID(); reqOpts.headers = { - ...reqOpts.headers, + ...headers, 'User-Agent': userAgent, 'x-goog-api-client': `${getRuntimeTrackingString()} gccl/${ pkg.version }-${getModuleFormat()} gccl-invocation-id/${idempotencyToken}`, - 'x-goog-gcs-idempotency-token': idempotencyToken, }; + if (!userTokenKey) { + reqOpts.headers['x-goog-gcs-idempotency-token'] = idempotencyToken; + } if (reqOpts[GCCL_GCS_CMD_KEY]) { reqOpts.headers['x-goog-api-client'] += diff --git a/handwritten/storage/src/resumable-upload.ts b/handwritten/storage/src/resumable-upload.ts index 16e84351e059..0c042430555f 100644 --- a/handwritten/storage/src/resumable-upload.ts +++ b/handwritten/storage/src/resumable-upload.ts @@ -338,7 +338,11 @@ export class Upload extends Writable { timeOfFirstRequest: number; isPartialUpload: boolean; - private currentInvocationId = { + private currentInvocationId: { + checkUploadStatus: string; + chunk: string; + uri: string; + } = { checkUploadStatus: crypto.randomUUID(), chunk: crypto.randomUUID(), uri: crypto.randomUUID(), @@ -806,6 +810,13 @@ export class Upload extends Writable { delete metadata.contentType; } + const userTokenKey = Object.keys(this.customRequestOptions.headers || {}).find( + key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' + ); + if (userTokenKey) { + this.currentInvocationId.uri = this.customRequestOptions.headers![userTokenKey] as string; + } + let googAPIClient = `${getRuntimeTrackingString()} gccl/${ packageJson.version }-${getModuleFormat()} gccl-invocation-id/${this.currentInvocationId.uri}`; @@ -829,11 +840,14 @@ export class Upload extends Writable { headers: { 'User-Agent': getUserAgentString(), 'x-goog-api-client': googAPIClient, - 'x-goog-gcs-idempotency-token': this.currentInvocationId.uri, ...headers, }, }; + if (!userTokenKey) { + reqOpts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.uri; + } + if (metadata.contentLength) { reqOpts.headers!['X-Upload-Content-Length'] = metadata.contentLength.toString(); @@ -996,6 +1010,13 @@ export class Upload extends Writable { }, }); + const userTokenKey = Object.keys(this.customRequestOptions.headers || {}).find( + key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' + ); + if (userTokenKey) { + this.currentInvocationId.chunk = this.customRequestOptions.headers![userTokenKey] as string; + } + let googAPIClient = `${getRuntimeTrackingString()} gccl/${ packageJson.version }-${getModuleFormat()} gccl-invocation-id/${ @@ -1009,9 +1030,12 @@ export class Upload extends Writable { const headers: GaxiosOptions['headers'] = { 'User-Agent': getUserAgentString(), 'x-goog-api-client': googAPIClient, - 'x-goog-gcs-idempotency-token': this.currentInvocationId.chunk, }; + if (!userTokenKey) { + headers['x-goog-gcs-idempotency-token'] = this.currentInvocationId.chunk; + } + // If using multiple chunk upload, set appropriate header if (multiChunkMode) { // We need to know how much data is available upstream to set the `Content-Range` header. @@ -1211,6 +1235,13 @@ export class Upload extends Writable { async checkUploadStatus( config: CheckUploadStatusConfig = {}, ): Promise> { + const userTokenKey = Object.keys(this.customRequestOptions.headers || {}).find( + key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' + ); + if (userTokenKey) { + this.currentInvocationId.checkUploadStatus = this.customRequestOptions.headers![userTokenKey] as string; + } + let googAPIClient = `${getRuntimeTrackingString()} gccl/${ packageJson.version }-${getModuleFormat()} gccl-invocation-id/${ @@ -1229,10 +1260,13 @@ export class Upload extends Writable { 'Content-Range': 'bytes */*', 'User-Agent': getUserAgentString(), 'x-goog-api-client': googAPIClient, - 'x-goog-gcs-idempotency-token': this.currentInvocationId.checkUploadStatus, }, }; + if (!userTokenKey) { + opts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.checkUploadStatus; + } + try { const resp = await this.makeRequest(opts); diff --git a/handwritten/storage/test/nodejs-common/service.ts b/handwritten/storage/test/nodejs-common/service.ts index b830c65eb680..857ec514f10a 100644 --- a/handwritten/storage/test/nodejs-common/service.ts +++ b/handwritten/storage/test/nodejs-common/service.ts @@ -506,6 +506,36 @@ describe('Service', () => { service.request_(reqOpts, assert.ifError); }); + it('should respect user-provided x-goog-gcs-idempotency-token case-insensitively and align it with gccl-invocation-id', done => { + const customToken = 'my-custom-token-123'; + const customReqOpts = { + ...reqOpts, + headers: { + 'X-Goog-Gcs-Idempotency-Token': customToken, + }, + }; + + service.makeAuthenticatedRequest = (reqOpts: DecorateRequestOptions) => { + const pkg = service.packageJson; + const r = new RegExp( + `^gl-node/${process.versions.node} gccl/${ + pkg.version + }-${getModuleFormat()} gccl-invocation-id/(?[^W]+)$` + ); + const match = r.exec(reqOpts.headers!['x-goog-api-client']); + assert.ok(match); + const invocationId = match.groups!.gcclInvocationId; + assert.strictEqual(invocationId, customToken); + + // Verify there is no duplicate x-goog-gcs-idempotency-token header + assert.strictEqual(reqOpts.headers!['x-goog-gcs-idempotency-token'], undefined); + assert.strictEqual(reqOpts.headers!['X-Goog-Gcs-Idempotency-Token'], customToken); + done(); + }; + + service.request_(customReqOpts, assert.ifError); + }); + it('should add the `gccl-gcs-cmd` to the api-client header when provided', done => { const expected = 'example.expected/value'; service.makeAuthenticatedRequest = (reqOpts: DecorateRequestOptions) => { diff --git a/handwritten/storage/test/resumable-upload.ts b/handwritten/storage/test/resumable-upload.ts index 7f260248224a..a90072fa974f 100644 --- a/handwritten/storage/test/resumable-upload.ts +++ b/handwritten/storage/test/resumable-upload.ts @@ -890,6 +890,31 @@ describe('resumable-upload', () => { up.createURI(); }); + it('should respect user-provided x-goog-gcs-idempotency-token case-insensitively and align it with gccl-invocation-id in createURI', async () => { + const customToken = 'my-custom-resumable-token'; + up.customRequestOptions = { + headers: { + 'X-Goog-Gcs-Idempotency-Token': customToken, + }, + }; + + up.authClient.request = async (combinedReqOpts: GaxiosOptions) => { + assert(combinedReqOpts.headers); + const apiClientHeader = combinedReqOpts.headers['x-goog-api-client']; + const match = X_GOOG_API_HEADER_REGEX.exec(apiClientHeader as string); + assert.ok(match); + const invocationId = match.groups!.gcclInvocationId; + assert.strictEqual(invocationId, customToken); + + // Verify there is no duplicate x-goog-gcs-idempotency-token header + assert.strictEqual(combinedReqOpts.headers['x-goog-gcs-idempotency-token'], undefined); + assert.strictEqual(combinedReqOpts.headers['X-Goog-Gcs-Idempotency-Token'], customToken); + return {headers: {location: '/foo'}}; + }; + + await up.createURI(); + }); + it('should reuse the same x-goog-gcs-idempotency-token on retry of createURI', async () => { let invocationCount = 0; let token1 = ''; From d96e9b86b26a71be882d49ad458bee8a3316e6a4 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Fri, 3 Jul 2026 11:36:08 +0000 Subject: [PATCH 3/7] refactor: simplify sourcemap compilation scripts and add optional chaining for idempotency token access --- handwritten/storage/package.json | 9 +++++---- handwritten/storage/src/resumable-upload.ts | 12 ++++++------ 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/handwritten/storage/package.json b/handwritten/storage/package.json index e38124dfa44f..c856420deaa1 100644 --- a/handwritten/storage/package.json +++ b/handwritten/storage/package.json @@ -54,6 +54,7 @@ "compile:cjs": "tsc -p ./tsconfig.cjs.json", "compile:esm": "tsc -p .", "compile": "npm run compile:cjs && npm run compile:esm", + "compile:sourcemaps": "npm run compile:cjs -- --sourceMap && npm run compile:esm -- --sourceMap", "conformance-test": "mocha --parallel build/cjs/conformance-test/ --require build/cjs/conformance-test/globalHooks.js", "docs": "jsdoc -c .jsdoc.json", "fix": "gts fix", @@ -61,13 +62,13 @@ "postcompile": "cp ./src/package-json-helper.cjs ./build/cjs/src && cp ./src/package-json-helper.cjs ./build/esm/src", "postcompile:cjs": "babel --plugins gapic-tools/build/src/replaceImportMetaUrl,gapic-tools/build/src/toggleESMFlagVariable build/cjs/src/util.js -o build/cjs/src/util.js && cp internal-tooling/helpers/package.cjs.json build/cjs/package.json", "precompile": "rm -rf build/", - "preconformance-test": "npm run compile:cjs -- --sourceMap", - "predocs": "npm run compile:cjs -- --sourceMap", + "preconformance-test": "npm run compile:sourcemaps", + "predocs": "npm run compile:sourcemaps", "prelint": "cd samples; npm link ../; npm install", "prepare": "npm run compile", "presystem-test:esm": "npm run compile:esm", - "presystem-test": "npm run compile -- --sourceMap", - "pretest": "npm run compile -- --sourceMap", + "presystem-test": "npm run compile:sourcemaps", + "pretest": "npm run compile:sourcemaps", "samples-test": "npm link && cd samples/ && npm link ../ && npm test && cd ../", "system-test:esm": "mocha build/esm/system-test --timeout 600000 --exit", "system-test": "mocha build/cjs/system-test --timeout 600000 --exit", diff --git a/handwritten/storage/src/resumable-upload.ts b/handwritten/storage/src/resumable-upload.ts index 0c042430555f..3565a879c7ac 100644 --- a/handwritten/storage/src/resumable-upload.ts +++ b/handwritten/storage/src/resumable-upload.ts @@ -810,11 +810,11 @@ export class Upload extends Writable { delete metadata.contentType; } - const userTokenKey = Object.keys(this.customRequestOptions.headers || {}).find( + const userTokenKey = Object.keys(this.customRequestOptions?.headers || {}).find( key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' ); if (userTokenKey) { - this.currentInvocationId.uri = this.customRequestOptions.headers![userTokenKey] as string; + this.currentInvocationId.uri = this.customRequestOptions?.headers?.[userTokenKey] as string; } let googAPIClient = `${getRuntimeTrackingString()} gccl/${ @@ -1010,11 +1010,11 @@ export class Upload extends Writable { }, }); - const userTokenKey = Object.keys(this.customRequestOptions.headers || {}).find( + const userTokenKey = Object.keys(this.customRequestOptions?.headers || {}).find( key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' ); if (userTokenKey) { - this.currentInvocationId.chunk = this.customRequestOptions.headers![userTokenKey] as string; + this.currentInvocationId.chunk = this.customRequestOptions?.headers?.[userTokenKey] as string; } let googAPIClient = `${getRuntimeTrackingString()} gccl/${ @@ -1235,11 +1235,11 @@ export class Upload extends Writable { async checkUploadStatus( config: CheckUploadStatusConfig = {}, ): Promise> { - const userTokenKey = Object.keys(this.customRequestOptions.headers || {}).find( + const userTokenKey = Object.keys(this.customRequestOptions?.headers || {}).find( key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' ); if (userTokenKey) { - this.currentInvocationId.checkUploadStatus = this.customRequestOptions.headers![userTokenKey] as string; + this.currentInvocationId.checkUploadStatus = this.customRequestOptions?.headers?.[userTokenKey] as string; } let googAPIClient = `${getRuntimeTrackingString()} gccl/${ From f42ee80c851ca79caabb33ac3c72dc0d27b3603e Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Fri, 3 Jul 2026 11:54:00 +0000 Subject: [PATCH 4/7] fix: ignore invalid empty or undefined idempotency tokens and fallback to generating a UUID --- .../storage/src/nodejs-common/service.ts | 8 +++--- handwritten/storage/src/resumable-upload.ts | 24 ++++++++++------- .../storage/test/nodejs-common/service.ts | 27 +++++++++++++++++++ handwritten/storage/test/resumable-upload.ts | 23 ++++++++++++++++ 4 files changed, 70 insertions(+), 12 deletions(-) diff --git a/handwritten/storage/src/nodejs-common/service.ts b/handwritten/storage/src/nodejs-common/service.ts index b3087172bca6..7f60dc7a14d3 100644 --- a/handwritten/storage/src/nodejs-common/service.ts +++ b/handwritten/storage/src/nodejs-common/service.ts @@ -273,8 +273,10 @@ export class Service { const userTokenKey = Object.keys(headers).find( key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' ); - const idempotencyToken = userTokenKey - ? (headers[userTokenKey] as string) + const userTokenValue = userTokenKey ? headers[userTokenKey] : undefined; + const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue; + const idempotencyToken = hasValidUserToken + ? (userTokenValue as string) : crypto.randomUUID(); reqOpts.headers = { ...headers, @@ -283,7 +285,7 @@ export class Service { pkg.version }-${getModuleFormat()} gccl-invocation-id/${idempotencyToken}`, }; - if (!userTokenKey) { + if (!hasValidUserToken) { reqOpts.headers['x-goog-gcs-idempotency-token'] = idempotencyToken; } diff --git a/handwritten/storage/src/resumable-upload.ts b/handwritten/storage/src/resumable-upload.ts index 3565a879c7ac..cdf56cfd46b0 100644 --- a/handwritten/storage/src/resumable-upload.ts +++ b/handwritten/storage/src/resumable-upload.ts @@ -813,8 +813,10 @@ export class Upload extends Writable { const userTokenKey = Object.keys(this.customRequestOptions?.headers || {}).find( key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' ); - if (userTokenKey) { - this.currentInvocationId.uri = this.customRequestOptions?.headers?.[userTokenKey] as string; + const userTokenValue = userTokenKey ? this.customRequestOptions?.headers?.[userTokenKey] : undefined; + const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue; + if (hasValidUserToken) { + this.currentInvocationId.uri = userTokenValue as string; } let googAPIClient = `${getRuntimeTrackingString()} gccl/${ @@ -844,7 +846,7 @@ export class Upload extends Writable { }, }; - if (!userTokenKey) { + if (!hasValidUserToken) { reqOpts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.uri; } @@ -1013,8 +1015,10 @@ export class Upload extends Writable { const userTokenKey = Object.keys(this.customRequestOptions?.headers || {}).find( key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' ); - if (userTokenKey) { - this.currentInvocationId.chunk = this.customRequestOptions?.headers?.[userTokenKey] as string; + const userTokenValue = userTokenKey ? this.customRequestOptions?.headers?.[userTokenKey] : undefined; + const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue; + if (hasValidUserToken) { + this.currentInvocationId.chunk = userTokenValue as string; } let googAPIClient = `${getRuntimeTrackingString()} gccl/${ @@ -1032,7 +1036,7 @@ export class Upload extends Writable { 'x-goog-api-client': googAPIClient, }; - if (!userTokenKey) { + if (!hasValidUserToken) { headers['x-goog-gcs-idempotency-token'] = this.currentInvocationId.chunk; } @@ -1238,8 +1242,10 @@ export class Upload extends Writable { const userTokenKey = Object.keys(this.customRequestOptions?.headers || {}).find( key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' ); - if (userTokenKey) { - this.currentInvocationId.checkUploadStatus = this.customRequestOptions?.headers?.[userTokenKey] as string; + const userTokenValue = userTokenKey ? this.customRequestOptions?.headers?.[userTokenKey] : undefined; + const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue; + if (hasValidUserToken) { + this.currentInvocationId.checkUploadStatus = userTokenValue as string; } let googAPIClient = `${getRuntimeTrackingString()} gccl/${ @@ -1263,7 +1269,7 @@ export class Upload extends Writable { }, }; - if (!userTokenKey) { + if (!hasValidUserToken) { opts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.checkUploadStatus; } diff --git a/handwritten/storage/test/nodejs-common/service.ts b/handwritten/storage/test/nodejs-common/service.ts index 857ec514f10a..66ebd1cc8f53 100644 --- a/handwritten/storage/test/nodejs-common/service.ts +++ b/handwritten/storage/test/nodejs-common/service.ts @@ -536,6 +536,33 @@ describe('Service', () => { service.request_(customReqOpts, assert.ifError); }); + it('should ignore invalid user-provided idempotency tokens and fallback to generating a UUID', done => { + const customReqOpts = { + ...reqOpts, + headers: { + 'X-Goog-Gcs-Idempotency-Token': undefined as unknown as string, + }, + }; + + service.makeAuthenticatedRequest = (reqOpts: DecorateRequestOptions) => { + const pkg = service.packageJson; + const r = new RegExp( + `^gl-node/${process.versions.node} gccl/${pkg.version + }-${getModuleFormat()} gccl-invocation-id/(?[^W]+)$` + ); + const match = r.exec(reqOpts.headers!['x-goog-api-client']); + assert.ok(match); + const invocationId = match.groups!.gcclInvocationId; + + // Verify a fallback token was generated and matches the invocation ID + const idempotencyToken = reqOpts.headers!['x-goog-gcs-idempotency-token']; + assert.strictEqual(idempotencyToken, invocationId); + done(); + }; + + service.request_(customReqOpts, assert.ifError); + }); + it('should add the `gccl-gcs-cmd` to the api-client header when provided', done => { const expected = 'example.expected/value'; service.makeAuthenticatedRequest = (reqOpts: DecorateRequestOptions) => { diff --git a/handwritten/storage/test/resumable-upload.ts b/handwritten/storage/test/resumable-upload.ts index a90072fa974f..ad8243e28cee 100644 --- a/handwritten/storage/test/resumable-upload.ts +++ b/handwritten/storage/test/resumable-upload.ts @@ -915,6 +915,29 @@ describe('resumable-upload', () => { await up.createURI(); }); + it('should ignore invalid user-provided idempotency tokens and fallback to generating a UUID in createURI', async () => { + up.customRequestOptions = { + headers: { + 'X-Goog-Gcs-Idempotency-Token': '', // invalid empty string + }, + }; + + up.authClient.request = async (combinedReqOpts: GaxiosOptions) => { + assert(combinedReqOpts.headers); + const apiClientHeader = combinedReqOpts.headers['x-goog-api-client']; + const match = X_GOOG_API_HEADER_REGEX.exec(apiClientHeader as string); + assert.ok(match); + const invocationId = match.groups!.gcclInvocationId; + + // Verify a fallback token was generated and matches the invocation ID + const idempotencyToken = combinedReqOpts.headers['x-goog-gcs-idempotency-token']; + assert.strictEqual(idempotencyToken, invocationId); + return {headers: {location: '/foo'}}; + }; + + await up.createURI(); + }); + it('should reuse the same x-goog-gcs-idempotency-token on retry of createURI', async () => { let invocationCount = 0; let token1 = ''; From f482198c01773d75c30d0b83416e57f751e50d63 Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Fri, 3 Jul 2026 13:19:11 +0000 Subject: [PATCH 5/7] fix: remove user-provided token header when injecting GCS idempotency token to prevent conflicts --- handwritten/storage/src/nodejs-common/service.ts | 3 +++ handwritten/storage/src/resumable-upload.ts | 9 +++++++++ 2 files changed, 12 insertions(+) diff --git a/handwritten/storage/src/nodejs-common/service.ts b/handwritten/storage/src/nodejs-common/service.ts index 7f60dc7a14d3..4273ed349744 100644 --- a/handwritten/storage/src/nodejs-common/service.ts +++ b/handwritten/storage/src/nodejs-common/service.ts @@ -286,6 +286,9 @@ export class Service { }-${getModuleFormat()} gccl-invocation-id/${idempotencyToken}`, }; if (!hasValidUserToken) { + if (userTokenKey) { + delete reqOpts.headers[userTokenKey]; + } reqOpts.headers['x-goog-gcs-idempotency-token'] = idempotencyToken; } diff --git a/handwritten/storage/src/resumable-upload.ts b/handwritten/storage/src/resumable-upload.ts index cdf56cfd46b0..6c9b680eee52 100644 --- a/handwritten/storage/src/resumable-upload.ts +++ b/handwritten/storage/src/resumable-upload.ts @@ -847,6 +847,9 @@ export class Upload extends Writable { }; if (!hasValidUserToken) { + if (userTokenKey) { + delete reqOpts.headers![userTokenKey]; + } reqOpts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.uri; } @@ -1037,6 +1040,9 @@ export class Upload extends Writable { }; if (!hasValidUserToken) { + if (userTokenKey) { + delete headers[userTokenKey]; + } headers['x-goog-gcs-idempotency-token'] = this.currentInvocationId.chunk; } @@ -1270,6 +1276,9 @@ export class Upload extends Writable { }; if (!hasValidUserToken) { + if (userTokenKey) { + delete opts.headers![userTokenKey]; + } opts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.checkUploadStatus; } From b9aa424225e68c5ff86a63191eaa18142e2da39d Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Fri, 3 Jul 2026 13:55:22 +0000 Subject: [PATCH 6/7] fix: improve idempotency token validation and standardize token removal logic during resumable uploads --- .../storage/src/nodejs-common/service.ts | 2 +- handwritten/storage/src/resumable-upload.ts | 21 ++++++++----------- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/handwritten/storage/src/nodejs-common/service.ts b/handwritten/storage/src/nodejs-common/service.ts index 4273ed349744..5a54ebf6ff86 100644 --- a/handwritten/storage/src/nodejs-common/service.ts +++ b/handwritten/storage/src/nodejs-common/service.ts @@ -274,7 +274,7 @@ export class Service { key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' ); const userTokenValue = userTokenKey ? headers[userTokenKey] : undefined; - const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue; + const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue !== ''; const idempotencyToken = hasValidUserToken ? (userTokenValue as string) : crypto.randomUUID(); diff --git a/handwritten/storage/src/resumable-upload.ts b/handwritten/storage/src/resumable-upload.ts index 6c9b680eee52..5b7554dafe07 100644 --- a/handwritten/storage/src/resumable-upload.ts +++ b/handwritten/storage/src/resumable-upload.ts @@ -814,9 +814,11 @@ export class Upload extends Writable { key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' ); const userTokenValue = userTokenKey ? this.customRequestOptions?.headers?.[userTokenKey] : undefined; - const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue; + const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue !== ''; if (hasValidUserToken) { this.currentInvocationId.uri = userTokenValue as string; + } else if (userTokenKey && this.customRequestOptions?.headers) { + delete this.customRequestOptions.headers[userTokenKey]; } let googAPIClient = `${getRuntimeTrackingString()} gccl/${ @@ -847,9 +849,6 @@ export class Upload extends Writable { }; if (!hasValidUserToken) { - if (userTokenKey) { - delete reqOpts.headers![userTokenKey]; - } reqOpts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.uri; } @@ -1019,9 +1018,11 @@ export class Upload extends Writable { key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' ); const userTokenValue = userTokenKey ? this.customRequestOptions?.headers?.[userTokenKey] : undefined; - const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue; + const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue !== ''; if (hasValidUserToken) { this.currentInvocationId.chunk = userTokenValue as string; + } else if (userTokenKey && this.customRequestOptions?.headers) { + delete this.customRequestOptions.headers[userTokenKey]; } let googAPIClient = `${getRuntimeTrackingString()} gccl/${ @@ -1040,9 +1041,6 @@ export class Upload extends Writable { }; if (!hasValidUserToken) { - if (userTokenKey) { - delete headers[userTokenKey]; - } headers['x-goog-gcs-idempotency-token'] = this.currentInvocationId.chunk; } @@ -1249,9 +1247,11 @@ export class Upload extends Writable { key => key.toLowerCase() === 'x-goog-gcs-idempotency-token' ); const userTokenValue = userTokenKey ? this.customRequestOptions?.headers?.[userTokenKey] : undefined; - const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue; + const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue !== ''; if (hasValidUserToken) { this.currentInvocationId.checkUploadStatus = userTokenValue as string; + } else if (userTokenKey && this.customRequestOptions?.headers) { + delete this.customRequestOptions.headers[userTokenKey]; } let googAPIClient = `${getRuntimeTrackingString()} gccl/${ @@ -1276,9 +1276,6 @@ export class Upload extends Writable { }; if (!hasValidUserToken) { - if (userTokenKey) { - delete opts.headers![userTokenKey]; - } opts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.checkUploadStatus; } From 46f25f74c49ab2e7d23e5a20d74e7274470d479b Mon Sep 17 00:00:00 2001 From: Thiyagu K Date: Thu, 9 Jul 2026 07:08:24 +0000 Subject: [PATCH 7/7] fix: perform immutable updates on customRequestOptions to avoid mutating shared header objects --- handwritten/storage/src/resumable-upload.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/handwritten/storage/src/resumable-upload.ts b/handwritten/storage/src/resumable-upload.ts index 5b7554dafe07..9515868c051a 100644 --- a/handwritten/storage/src/resumable-upload.ts +++ b/handwritten/storage/src/resumable-upload.ts @@ -818,7 +818,11 @@ export class Upload extends Writable { if (hasValidUserToken) { this.currentInvocationId.uri = userTokenValue as string; } else if (userTokenKey && this.customRequestOptions?.headers) { - delete this.customRequestOptions.headers[userTokenKey]; + this.customRequestOptions = { + ...this.customRequestOptions, + headers: { ...this.customRequestOptions.headers }, + }; + delete this.customRequestOptions.headers![userTokenKey]; } let googAPIClient = `${getRuntimeTrackingString()} gccl/${ @@ -1022,7 +1026,11 @@ export class Upload extends Writable { if (hasValidUserToken) { this.currentInvocationId.chunk = userTokenValue as string; } else if (userTokenKey && this.customRequestOptions?.headers) { - delete this.customRequestOptions.headers[userTokenKey]; + this.customRequestOptions = { + ...this.customRequestOptions, + headers: { ...this.customRequestOptions.headers }, + }; + delete this.customRequestOptions.headers![userTokenKey]; } let googAPIClient = `${getRuntimeTrackingString()} gccl/${ @@ -1251,7 +1259,11 @@ export class Upload extends Writable { if (hasValidUserToken) { this.currentInvocationId.checkUploadStatus = userTokenValue as string; } else if (userTokenKey && this.customRequestOptions?.headers) { - delete this.customRequestOptions.headers[userTokenKey]; + this.customRequestOptions = { + ...this.customRequestOptions, + headers: { ...this.customRequestOptions.headers }, + }; + delete this.customRequestOptions.headers![userTokenKey]; } let googAPIClient = `${getRuntimeTrackingString()} gccl/${