@@ -339,6 +339,64 @@ describeAcceptance(
339339 client . destroy ( )
340340 }
341341 } )
342+
343+ it ( 'correctly handles objects with special characters in key' , async ( ) => {
344+ const client = createAcceptanceS3Client ( )
345+ const bucketName = uniqueBucketName ( 's3chars' )
346+ const keyFullyValid = uniqueObjectKey ( "test!$&'() *+,:;=@ing" , 'bin' )
347+ const keyInvalid = uniqueObjectKey ( 'test[]^|ing' , 'bin' )
348+
349+ const payload = Buffer . from ( 'acceptance-s3-char-key' )
350+
351+ try {
352+ await client . send ( new CreateBucketCommand ( { Bucket : bucketName } ) )
353+
354+ // storage api rejects []^| but does not hit a signature rejection due to escaping
355+ await expect (
356+ client . send (
357+ new PutObjectCommand ( {
358+ Body : payload ,
359+ Bucket : bucketName ,
360+ ContentType : 'application/octet-stream' ,
361+ Key : keyInvalid ,
362+ } )
363+ )
364+ ) . rejects . toThrow ( 'Invalid key: ' + keyInvalid )
365+
366+ await client . send (
367+ new PutObjectCommand ( {
368+ Body : payload ,
369+ Bucket : bucketName ,
370+ ContentType : 'application/octet-stream' ,
371+ Key : keyFullyValid ,
372+ } )
373+ )
374+
375+ const downloaded = await client . send (
376+ new GetObjectCommand ( {
377+ Bucket : bucketName ,
378+ Key : keyFullyValid ,
379+ } )
380+ )
381+ expect ( await downloaded . Body ?. transformToString ( ) ) . toBe ( payload . toString ( ) )
382+
383+ const listed = await client . send (
384+ new ListObjectsV2Command ( {
385+ Bucket : bucketName ,
386+ Prefix : keyFullyValid . split ( '/' ) [ 0 ] ,
387+ } )
388+ )
389+ expect ( listed . Contents ?. map ( ( object ) => object . Key ) ) . toContain ( keyFullyValid )
390+
391+ const deleted = await client . send (
392+ new DeleteObjectCommand ( { Bucket : bucketName , Key : keyFullyValid } )
393+ )
394+ expect ( deleted . $metadata . httpStatusCode ) . toBe ( 204 )
395+ } finally {
396+ await cleanupS3Bucket ( client , bucketName )
397+ client . destroy ( )
398+ }
399+ } )
342400 }
343401)
344402
0 commit comments