Skip to content

Commit a525007

Browse files
committed
feat: allow user-provided x-goog-gcs-idempotency-token and synchronize with gccl-invocation-id header
1 parent 0a09669 commit a525007

4 files changed

Lines changed: 104 additions & 7 deletions

File tree

handwritten/storage/src/nodejs-common/service.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -269,15 +269,23 @@ export class Service {
269269
if (this.providedUserAgent) {
270270
userAgent = `${this.providedUserAgent} ${userAgent}`;
271271
}
272-
const idempotencyToken = crypto.randomUUID();
272+
const headers = reqOpts.headers || {};
273+
const userTokenKey = Object.keys(headers).find(
274+
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
275+
);
276+
const idempotencyToken = userTokenKey
277+
? (headers[userTokenKey] as string)
278+
: crypto.randomUUID();
273279
reqOpts.headers = {
274-
...reqOpts.headers,
280+
...headers,
275281
'User-Agent': userAgent,
276282
'x-goog-api-client': `${getRuntimeTrackingString()} gccl/${
277283
pkg.version
278284
}-${getModuleFormat()} gccl-invocation-id/${idempotencyToken}`,
279-
'x-goog-gcs-idempotency-token': idempotencyToken,
280285
};
286+
if (!userTokenKey) {
287+
reqOpts.headers['x-goog-gcs-idempotency-token'] = idempotencyToken;
288+
}
281289

282290
if (reqOpts[GCCL_GCS_CMD_KEY]) {
283291
reqOpts.headers['x-goog-api-client'] +=

handwritten/storage/src/resumable-upload.ts

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -338,7 +338,11 @@ export class Upload extends Writable {
338338
timeOfFirstRequest: number;
339339
isPartialUpload: boolean;
340340

341-
private currentInvocationId = {
341+
private currentInvocationId: {
342+
checkUploadStatus: string;
343+
chunk: string;
344+
uri: string;
345+
} = {
342346
checkUploadStatus: crypto.randomUUID(),
343347
chunk: crypto.randomUUID(),
344348
uri: crypto.randomUUID(),
@@ -806,6 +810,13 @@ export class Upload extends Writable {
806810
delete metadata.contentType;
807811
}
808812

813+
const userTokenKey = Object.keys(this.customRequestOptions.headers || {}).find(
814+
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
815+
);
816+
if (userTokenKey) {
817+
this.currentInvocationId.uri = this.customRequestOptions.headers![userTokenKey] as string;
818+
}
819+
809820
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
810821
packageJson.version
811822
}-${getModuleFormat()} gccl-invocation-id/${this.currentInvocationId.uri}`;
@@ -829,11 +840,14 @@ export class Upload extends Writable {
829840
headers: {
830841
'User-Agent': getUserAgentString(),
831842
'x-goog-api-client': googAPIClient,
832-
'x-goog-gcs-idempotency-token': this.currentInvocationId.uri,
833843
...headers,
834844
},
835845
};
836846

847+
if (!userTokenKey) {
848+
reqOpts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.uri;
849+
}
850+
837851
if (metadata.contentLength) {
838852
reqOpts.headers!['X-Upload-Content-Length'] =
839853
metadata.contentLength.toString();
@@ -996,6 +1010,13 @@ export class Upload extends Writable {
9961010
},
9971011
});
9981012

1013+
const userTokenKey = Object.keys(this.customRequestOptions.headers || {}).find(
1014+
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
1015+
);
1016+
if (userTokenKey) {
1017+
this.currentInvocationId.chunk = this.customRequestOptions.headers![userTokenKey] as string;
1018+
}
1019+
9991020
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
10001021
packageJson.version
10011022
}-${getModuleFormat()} gccl-invocation-id/${
@@ -1009,9 +1030,12 @@ export class Upload extends Writable {
10091030
const headers: GaxiosOptions['headers'] = {
10101031
'User-Agent': getUserAgentString(),
10111032
'x-goog-api-client': googAPIClient,
1012-
'x-goog-gcs-idempotency-token': this.currentInvocationId.chunk,
10131033
};
10141034

1035+
if (!userTokenKey) {
1036+
headers['x-goog-gcs-idempotency-token'] = this.currentInvocationId.chunk;
1037+
}
1038+
10151039
// If using multiple chunk upload, set appropriate header
10161040
if (multiChunkMode) {
10171041
// 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 {
12111235
async checkUploadStatus(
12121236
config: CheckUploadStatusConfig = {},
12131237
): Promise<GaxiosResponse<FileMetadata | void>> {
1238+
const userTokenKey = Object.keys(this.customRequestOptions.headers || {}).find(
1239+
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
1240+
);
1241+
if (userTokenKey) {
1242+
this.currentInvocationId.checkUploadStatus = this.customRequestOptions.headers![userTokenKey] as string;
1243+
}
1244+
12141245
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
12151246
packageJson.version
12161247
}-${getModuleFormat()} gccl-invocation-id/${
@@ -1229,10 +1260,13 @@ export class Upload extends Writable {
12291260
'Content-Range': 'bytes */*',
12301261
'User-Agent': getUserAgentString(),
12311262
'x-goog-api-client': googAPIClient,
1232-
'x-goog-gcs-idempotency-token': this.currentInvocationId.checkUploadStatus,
12331263
},
12341264
};
12351265

1266+
if (!userTokenKey) {
1267+
opts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.checkUploadStatus;
1268+
}
1269+
12361270
try {
12371271
const resp = await this.makeRequest(opts);
12381272

handwritten/storage/test/nodejs-common/service.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,36 @@ describe('Service', () => {
506506
service.request_(reqOpts, assert.ifError);
507507
});
508508

509+
it('should respect user-provided x-goog-gcs-idempotency-token case-insensitively and align it with gccl-invocation-id', done => {
510+
const customToken = 'my-custom-token-123';
511+
const customReqOpts = {
512+
...reqOpts,
513+
headers: {
514+
'X-Goog-Gcs-Idempotency-Token': customToken,
515+
},
516+
};
517+
518+
service.makeAuthenticatedRequest = (reqOpts: DecorateRequestOptions) => {
519+
const pkg = service.packageJson;
520+
const r = new RegExp(
521+
`^gl-node/${process.versions.node} gccl/${
522+
pkg.version
523+
}-${getModuleFormat()} gccl-invocation-id/(?<gcclInvocationId>[^W]+)$`
524+
);
525+
const match = r.exec(reqOpts.headers!['x-goog-api-client']);
526+
assert.ok(match);
527+
const invocationId = match.groups!.gcclInvocationId;
528+
assert.strictEqual(invocationId, customToken);
529+
530+
// Verify there is no duplicate x-goog-gcs-idempotency-token header
531+
assert.strictEqual(reqOpts.headers!['x-goog-gcs-idempotency-token'], undefined);
532+
assert.strictEqual(reqOpts.headers!['X-Goog-Gcs-Idempotency-Token'], customToken);
533+
done();
534+
};
535+
536+
service.request_(customReqOpts, assert.ifError);
537+
});
538+
509539
it('should add the `gccl-gcs-cmd` to the api-client header when provided', done => {
510540
const expected = 'example.expected/value';
511541
service.makeAuthenticatedRequest = (reqOpts: DecorateRequestOptions) => {

handwritten/storage/test/resumable-upload.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -890,6 +890,31 @@ describe('resumable-upload', () => {
890890
up.createURI();
891891
});
892892

893+
it('should respect user-provided x-goog-gcs-idempotency-token case-insensitively and align it with gccl-invocation-id in createURI', async () => {
894+
const customToken = 'my-custom-resumable-token';
895+
up.customRequestOptions = {
896+
headers: {
897+
'X-Goog-Gcs-Idempotency-Token': customToken,
898+
},
899+
};
900+
901+
up.authClient.request = async (combinedReqOpts: GaxiosOptions) => {
902+
assert(combinedReqOpts.headers);
903+
const apiClientHeader = combinedReqOpts.headers['x-goog-api-client'];
904+
const match = X_GOOG_API_HEADER_REGEX.exec(apiClientHeader as string);
905+
assert.ok(match);
906+
const invocationId = match.groups!.gcclInvocationId;
907+
assert.strictEqual(invocationId, customToken);
908+
909+
// Verify there is no duplicate x-goog-gcs-idempotency-token header
910+
assert.strictEqual(combinedReqOpts.headers['x-goog-gcs-idempotency-token'], undefined);
911+
assert.strictEqual(combinedReqOpts.headers['X-Goog-Gcs-Idempotency-Token'], customToken);
912+
return {headers: {location: '/foo'}};
913+
};
914+
915+
await up.createURI();
916+
});
917+
893918
it('should reuse the same x-goog-gcs-idempotency-token on retry of createURI', async () => {
894919
let invocationCount = 0;
895920
let token1 = '';

0 commit comments

Comments
 (0)