Skip to content

Commit 18dcc3a

Browse files
committed
fix(s3): percent-encode object keys in download URLs
`getObjectPublicUrl` interpolated S3 keys directly into the URL path. S3 keys may legally contain reserved URI delimiters (`?`, `#`), space, `+`, etc., per AWS object key naming guidelines, and `ListObjectsV2` returns them verbatim. Per RFC 3986, `?` starts a query string and `#` a fragment, so a key like `scan?1.dcm` was sent as `GET /scan?1.dcm` (object `scan` + query `1.dcm` => 404), and `seg#1.dcm` had its fragment stripped before the request even left the browser. Split keys on `/` and `encodeURIComponent` each segment to preserve the hierarchy while encoding everything else. Tests cover `?`, `#`, space, `+`, and a multi-segment key.
1 parent 409da56 commit 18dcc3a

2 files changed

Lines changed: 33 additions & 1 deletion

File tree

src/io/__tests__/amazonS3.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,33 @@ describe('amazonS3', () => {
7777
expect(init).toBeUndefined();
7878
});
7979

80+
it('percent-encodes reserved chars in keys but preserves slashes', async () => {
81+
fetchSpy.mockResolvedValueOnce(
82+
okResponse(
83+
xmlListResponse([
84+
'scan?1.dcm',
85+
'seg#1.dcm',
86+
'my scan.dcm',
87+
'a+b.dcm',
88+
'sub/dir/file.dcm',
89+
])
90+
)
91+
);
92+
93+
const urls: string[] = [];
94+
await getObjectsFromS3('s3://bucket/prefix', (_name, url) =>
95+
urls.push(url)
96+
);
97+
98+
expect(urls).toEqual([
99+
'https://bucket.s3.amazonaws.com/scan%3F1.dcm',
100+
'https://bucket.s3.amazonaws.com/seg%231.dcm',
101+
'https://bucket.s3.amazonaws.com/my%20scan.dcm',
102+
'https://bucket.s3.amazonaws.com/a%2Bb.dcm',
103+
'https://bucket.s3.amazonaws.com/sub/dir/file.dcm',
104+
]);
105+
});
106+
80107
it('follows pagination via continuation token', async () => {
81108
fetchSpy
82109
.mockResolvedValueOnce(

src/io/amazonS3.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,13 @@ export const isAmazonS3Uri = (uri: string) =>
1010

1111
export type ObjectAvailableCallback = (name: string, url: string) => void;
1212

13+
// Percent-encode each path segment so keys containing reserved URI chars
14+
// (`?`, `#`, space, `+`, …) round-trip correctly. Slashes are preserved.
15+
const encodeS3Key = (key: string) =>
16+
key.split('/').map(encodeURIComponent).join('/');
17+
1318
const getObjectPublicUrl = (bucket: string, key: string) =>
14-
`https://${bucket}.s3.amazonaws.com/${key}`;
19+
`https://${bucket}.s3.amazonaws.com/${encodeS3Key(key)}`;
1520

1621
const buildListUrl = (
1722
bucket: string,

0 commit comments

Comments
 (0)