Skip to content

Commit f42ee80

Browse files
committed
fix: ignore invalid empty or undefined idempotency tokens and fallback to generating a UUID
1 parent d96e9b8 commit f42ee80

4 files changed

Lines changed: 70 additions & 12 deletions

File tree

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

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -273,8 +273,10 @@ export class Service {
273273
const userTokenKey = Object.keys(headers).find(
274274
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
275275
);
276-
const idempotencyToken = userTokenKey
277-
? (headers[userTokenKey] as string)
276+
const userTokenValue = userTokenKey ? headers[userTokenKey] : undefined;
277+
const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue;
278+
const idempotencyToken = hasValidUserToken
279+
? (userTokenValue as string)
278280
: crypto.randomUUID();
279281
reqOpts.headers = {
280282
...headers,
@@ -283,7 +285,7 @@ export class Service {
283285
pkg.version
284286
}-${getModuleFormat()} gccl-invocation-id/${idempotencyToken}`,
285287
};
286-
if (!userTokenKey) {
288+
if (!hasValidUserToken) {
287289
reqOpts.headers['x-goog-gcs-idempotency-token'] = idempotencyToken;
288290
}
289291

handwritten/storage/src/resumable-upload.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -813,8 +813,10 @@ export class Upload extends Writable {
813813
const userTokenKey = Object.keys(this.customRequestOptions?.headers || {}).find(
814814
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
815815
);
816-
if (userTokenKey) {
817-
this.currentInvocationId.uri = this.customRequestOptions?.headers?.[userTokenKey] as string;
816+
const userTokenValue = userTokenKey ? this.customRequestOptions?.headers?.[userTokenKey] : undefined;
817+
const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue;
818+
if (hasValidUserToken) {
819+
this.currentInvocationId.uri = userTokenValue as string;
818820
}
819821

820822
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
@@ -844,7 +846,7 @@ export class Upload extends Writable {
844846
},
845847
};
846848

847-
if (!userTokenKey) {
849+
if (!hasValidUserToken) {
848850
reqOpts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.uri;
849851
}
850852

@@ -1013,8 +1015,10 @@ export class Upload extends Writable {
10131015
const userTokenKey = Object.keys(this.customRequestOptions?.headers || {}).find(
10141016
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
10151017
);
1016-
if (userTokenKey) {
1017-
this.currentInvocationId.chunk = this.customRequestOptions?.headers?.[userTokenKey] as string;
1018+
const userTokenValue = userTokenKey ? this.customRequestOptions?.headers?.[userTokenKey] : undefined;
1019+
const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue;
1020+
if (hasValidUserToken) {
1021+
this.currentInvocationId.chunk = userTokenValue as string;
10181022
}
10191023

10201024
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
@@ -1032,7 +1036,7 @@ export class Upload extends Writable {
10321036
'x-goog-api-client': googAPIClient,
10331037
};
10341038

1035-
if (!userTokenKey) {
1039+
if (!hasValidUserToken) {
10361040
headers['x-goog-gcs-idempotency-token'] = this.currentInvocationId.chunk;
10371041
}
10381042

@@ -1238,8 +1242,10 @@ export class Upload extends Writable {
12381242
const userTokenKey = Object.keys(this.customRequestOptions?.headers || {}).find(
12391243
key => key.toLowerCase() === 'x-goog-gcs-idempotency-token'
12401244
);
1241-
if (userTokenKey) {
1242-
this.currentInvocationId.checkUploadStatus = this.customRequestOptions?.headers?.[userTokenKey] as string;
1245+
const userTokenValue = userTokenKey ? this.customRequestOptions?.headers?.[userTokenKey] : undefined;
1246+
const hasValidUserToken = typeof userTokenValue === 'string' && userTokenValue;
1247+
if (hasValidUserToken) {
1248+
this.currentInvocationId.checkUploadStatus = userTokenValue as string;
12431249
}
12441250

12451251
let googAPIClient = `${getRuntimeTrackingString()} gccl/${
@@ -1263,7 +1269,7 @@ export class Upload extends Writable {
12631269
},
12641270
};
12651271

1266-
if (!userTokenKey) {
1272+
if (!hasValidUserToken) {
12671273
opts.headers!['x-goog-gcs-idempotency-token'] = this.currentInvocationId.checkUploadStatus;
12681274
}
12691275

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,33 @@ describe('Service', () => {
536536
service.request_(customReqOpts, assert.ifError);
537537
});
538538

539+
it('should ignore invalid user-provided idempotency tokens and fallback to generating a UUID', done => {
540+
const customReqOpts = {
541+
...reqOpts,
542+
headers: {
543+
'X-Goog-Gcs-Idempotency-Token': undefined as unknown as string,
544+
},
545+
};
546+
547+
service.makeAuthenticatedRequest = (reqOpts: DecorateRequestOptions) => {
548+
const pkg = service.packageJson;
549+
const r = new RegExp(
550+
`^gl-node/${process.versions.node} gccl/${pkg.version
551+
}-${getModuleFormat()} gccl-invocation-id/(?<gcclInvocationId>[^W]+)$`
552+
);
553+
const match = r.exec(reqOpts.headers!['x-goog-api-client']);
554+
assert.ok(match);
555+
const invocationId = match.groups!.gcclInvocationId;
556+
557+
// Verify a fallback token was generated and matches the invocation ID
558+
const idempotencyToken = reqOpts.headers!['x-goog-gcs-idempotency-token'];
559+
assert.strictEqual(idempotencyToken, invocationId);
560+
done();
561+
};
562+
563+
service.request_(customReqOpts, assert.ifError);
564+
});
565+
539566
it('should add the `gccl-gcs-cmd` to the api-client header when provided', done => {
540567
const expected = 'example.expected/value';
541568
service.makeAuthenticatedRequest = (reqOpts: DecorateRequestOptions) => {

handwritten/storage/test/resumable-upload.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -915,6 +915,29 @@ describe('resumable-upload', () => {
915915
await up.createURI();
916916
});
917917

918+
it('should ignore invalid user-provided idempotency tokens and fallback to generating a UUID in createURI', async () => {
919+
up.customRequestOptions = {
920+
headers: {
921+
'X-Goog-Gcs-Idempotency-Token': '', // invalid empty string
922+
},
923+
};
924+
925+
up.authClient.request = async (combinedReqOpts: GaxiosOptions) => {
926+
assert(combinedReqOpts.headers);
927+
const apiClientHeader = combinedReqOpts.headers['x-goog-api-client'];
928+
const match = X_GOOG_API_HEADER_REGEX.exec(apiClientHeader as string);
929+
assert.ok(match);
930+
const invocationId = match.groups!.gcclInvocationId;
931+
932+
// Verify a fallback token was generated and matches the invocation ID
933+
const idempotencyToken = combinedReqOpts.headers['x-goog-gcs-idempotency-token'];
934+
assert.strictEqual(idempotencyToken, invocationId);
935+
return {headers: {location: '/foo'}};
936+
};
937+
938+
await up.createURI();
939+
});
940+
918941
it('should reuse the same x-goog-gcs-idempotency-token on retry of createURI', async () => {
919942
let invocationCount = 0;
920943
let token1 = '';

0 commit comments

Comments
 (0)