Skip to content

Commit 1fa4e97

Browse files
authored
fix(sdk): fix race condition in bucket.tryget() and bucket.trygetjson() for aws targets (#3655)
Closes #2821 *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*.
1 parent 02e2056 commit 1fa4e97

File tree

2 files changed

+79
-35
lines changed

2 files changed

+79
-35
lines changed

libs/wingsdk/src/shared-aws/bucket.inflight.ts

+41-32
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
GetPublicAccessBlockCommandOutput,
1313
S3Client,
1414
GetObjectOutput,
15+
NoSuchKey,
1516
} from "@aws-sdk/client-s3";
1617
import { BucketDeleteOptions, IBucketClient } from "../cloud";
1718
import { Duration, Json } from "../std";
@@ -70,34 +71,46 @@ export class BucketClient implements IBucketClient {
7071
}
7172

7273
/**
73-
* Get an object from the bucket
74-
*
75-
* @param key Key of the object
76-
* @returns content of the object
74+
* See https://github.com/aws/aws-sdk-js-v3/issues/1877
7775
*/
78-
public async get(key: string): Promise<string> {
79-
// See https://github.com/aws/aws-sdk-js-v3/issues/1877
76+
private async getObjectContent(key: string): Promise<string | undefined> {
8077
const command = new GetObjectCommand({
8178
Bucket: this.bucketName,
8279
Key: key,
8380
});
84-
let resp: GetObjectOutput;
81+
8582
try {
86-
resp = await this.s3Client.send(command);
83+
const resp: GetObjectOutput = await this.s3Client.send(command);
84+
const objectContent = resp.Body as Readable;
85+
try {
86+
return await consumers.text(objectContent);
87+
} catch (e) {
88+
throw new Error(
89+
`Object content could not be read as text (key=${key}): ${
90+
(e as Error).stack
91+
})}`
92+
);
93+
}
8794
} catch (e) {
88-
throw new Error(
89-
`Object does not exist (key=${key}): ${(e as Error).stack}`
90-
);
95+
if (e instanceof NoSuchKey) {
96+
return undefined;
97+
}
98+
throw new Error((e as Error).stack);
9199
}
92-
try {
93-
return await consumers.text(resp.Body as Readable);
94-
} catch (e) {
95-
throw new Error(
96-
`Object contents could not be read as text (key=${key}): ${
97-
(e as Error).stack
98-
})}`
99-
);
100+
}
101+
102+
/**
103+
* Get an object from the bucket
104+
*
105+
* @param key Key of the object
106+
* @returns content of the object
107+
*/
108+
public async get(key: string): Promise<string> {
109+
const objectContent = await this.getObjectContent(key);
110+
if (objectContent !== undefined) {
111+
return objectContent;
100112
}
113+
throw new Error(`Object does not exist (key=${key}).`);
101114
}
102115

103116
/**
@@ -107,11 +120,7 @@ export class BucketClient implements IBucketClient {
107120
* @returns content of the object
108121
*/
109122
public async tryGet(key: string): Promise<string | undefined> {
110-
if (await this.exists(key)) {
111-
return this.get(key);
112-
}
113-
114-
return undefined;
123+
return this.getObjectContent(key);
115124
}
116125

117126
/**
@@ -131,11 +140,12 @@ export class BucketClient implements IBucketClient {
131140
* @returns Json content of the object
132141
*/
133142
public async tryGetJson(key: string): Promise<Json | undefined> {
134-
if (await this.exists(key)) {
135-
return this.getJson(key);
143+
const objectContent = await this.tryGet(key);
144+
if (objectContent !== undefined) {
145+
return JSON.parse(objectContent);
146+
} else {
147+
return undefined;
136148
}
137-
138-
return undefined;
139149
}
140150

141151
/**
@@ -158,13 +168,12 @@ export class BucketClient implements IBucketClient {
158168

159169
try {
160170
await this.s3Client.send(command);
161-
} catch (er) {
162-
const error = er as any;
163-
if (!mustExist && error.name === "NoSuchKey") {
171+
} catch (e: any) {
172+
if (!mustExist && e instanceof NoSuchKey) {
164173
return;
165174
}
166175

167-
throw new Error(`Unable to delete "${key}": ${error.message}`);
176+
throw new Error(`Unable to delete "${key}": ${e.message}`);
168177
}
169178
}
170179

libs/wingsdk/test/shared-aws/bucket.inflight.test.ts

+38-3
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
ListObjectsV2Command,
99
PutObjectCommand,
1010
S3Client,
11+
NoSuchKey,
1112
} from "@aws-sdk/client-s3";
1213
import { SdkStream } from "@aws-sdk/types";
1314
import { sdkStreamMixin } from "@aws-sdk/util-stream-node";
@@ -93,7 +94,7 @@ test("get a non-existent object from the bucket", async () => {
9394
const VALUE = "VALUE";
9495
s3Mock
9596
.on(GetObjectCommand, { Bucket: BUCKET_NAME, Key: KEY })
96-
.rejects(new Error("fake error"));
97+
.rejects(new Error("Object does not exist"));
9798

9899
// WHEN
99100
const client = new BucketClient(BUCKET_NAME);
@@ -372,7 +373,7 @@ test("tryGet a non-existent object from the bucket", async () => {
372373
const VALUE = "VALUE";
373374
s3Mock
374375
.on(GetObjectCommand, { Bucket: BUCKET_NAME, Key: KEY })
375-
.rejects(new Error("fake error"));
376+
.rejects(new NoSuchKey({ message: "NoSuchKey error", $metadata: {} }));
376377
s3Mock
377378
.on(HeadObjectCommand, { Bucket: BUCKET_NAME, Key: KEY })
378379
.rejects({ name: "NotFound" });
@@ -385,6 +386,23 @@ test("tryGet a non-existent object from the bucket", async () => {
385386
expect(objectTryGet).toEqual(undefined);
386387
});
387388

389+
test("tryGet object from the bucket throws an unknown error", async () => {
390+
// GIVEN
391+
const BUCKET_NAME = "BUCKET_NAME";
392+
const KEY = "KEY";
393+
const VALUE = "VALUE";
394+
s3Mock
395+
.on(GetObjectCommand, { Bucket: BUCKET_NAME, Key: KEY })
396+
.rejects(new Error("unknown error"));
397+
s3Mock.on(HeadObjectCommand, { Bucket: BUCKET_NAME, Key: KEY }).resolves({});
398+
399+
// WHEN
400+
const client = new BucketClient(BUCKET_NAME);
401+
402+
// THEN
403+
await expect(() => client.tryGet(KEY)).rejects.toThrowError(/unknown error/);
404+
});
405+
388406
test("tryGetJson an existing object from the bucket", async () => {
389407
// GIVEN
390408
const BUCKET_NAME = "BUCKET_NAME";
@@ -418,7 +436,7 @@ test("tryGetJson a non-existent object from the bucket", async () => {
418436
const VALUE = { msg: "Hello, World!" };
419437
s3Mock
420438
.on(GetObjectCommand, { Bucket: BUCKET_NAME, Key: KEY })
421-
.rejects(new Error("fake error"));
439+
.rejects(new NoSuchKey({ message: "NoSuchKey error", $metadata: {} }));
422440
s3Mock
423441
.on(HeadObjectCommand, { Bucket: BUCKET_NAME, Key: KEY })
424442
.rejects({ name: "NotFound" });
@@ -431,6 +449,23 @@ test("tryGetJson a non-existent object from the bucket", async () => {
431449
expect(objectTryGetJson).toEqual(undefined);
432450
});
433451

452+
test("tryGetJson object from the bucket throws an unknown error", async () => {
453+
// GIVEN
454+
const BUCKET_NAME = "BUCKET_NAME";
455+
const KEY = "KEY";
456+
const VALUE = { msg: "Hello, World!" };
457+
s3Mock
458+
.on(GetObjectCommand, { Bucket: BUCKET_NAME, Key: KEY })
459+
.rejects(new Error("unknown error"));
460+
s3Mock.on(HeadObjectCommand, { Bucket: BUCKET_NAME, Key: KEY }).resolves({});
461+
462+
// WHEN
463+
const client = new BucketClient(BUCKET_NAME);
464+
465+
// THEN
466+
await expect(() => client.tryGet(KEY)).rejects.toThrowError(/unknown error/);
467+
});
468+
434469
test("tryGetJson an existing non-Json object from the bucket", async () => {
435470
// GIVEN
436471
const BUCKET_NAME = "BUCKET_NAME";

0 commit comments

Comments
 (0)