@@ -146,13 +146,31 @@ class TestR2Bucket implements R2Bucket {
146146 options ?: R2PutOptions
147147 ) : Promise < R2Object > {
148148 const url = `http://localhost/${ encodeURIComponent ( this . ns + key ) } ` ;
149+
150+ let valueBlob : Blob ;
151+ if ( value === null ) {
152+ valueBlob = new Blob ( [ ] ) ;
153+ } else if ( value instanceof ArrayBuffer ) {
154+ valueBlob = new Blob ( [ new Uint8Array ( value ) ] ) ;
155+ } else if ( ArrayBuffer . isView ( value ) ) {
156+ valueBlob = new Blob ( [ viewToArray ( value ) ] ) ;
157+ } else if ( value instanceof ReadableStream ) {
158+ // @ts -expect-error `ReadableStream` is an `AsyncIterable`
159+ valueBlob = await blob ( value ) ;
160+ } else {
161+ valueBlob = new Blob ( [ value ] ) ;
162+ }
163+
164+ // We can't store options in headers as some put() tests include extended
165+ // characters in them, and `undici` validates all headers are byte strings,
166+ // so use a form data body instead
167+ const formData = new FormData ( ) ;
168+ formData . set ( "options" , maybeJsonStringify ( options ) ) ;
169+ formData . set ( "value" , valueBlob ) ;
149170 const res = await this . mf . dispatchFetch ( url , {
150171 method : "PUT" ,
151- headers : {
152- Accept : "multipart/form-data" ,
153- "Test-Options" : maybeJsonStringify ( options ) ,
154- } ,
155- body : ArrayBuffer . isView ( value ) ? viewToArray ( value ) : value ,
172+ headers : { Accept : "multipart/form-data" } ,
173+ body : formData ,
156174 } ) ;
157175 return deconstructResponse ( res ) ;
158176 }
@@ -376,11 +394,12 @@ const test = miniflareTest<{ BUCKET: R2Bucket }, Context>(
376394 const options = maybeJsonParse ( optionsHeader ) ;
377395 return constructResponse ( await env . BUCKET . get ( key , options ) ) ;
378396 } else if ( method === "PUT" ) {
379- const optionsHeader = request . headers . get ( "Test-Options" ) ;
380- const options = maybeJsonParse ( optionsHeader ) ;
381- return constructResponse (
382- await env . BUCKET . put ( key , await request . arrayBuffer ( ) , options )
383- ) ;
397+ const formData = await request . formData ( ) ;
398+ const optionsData = formData . get ( "options" ) ;
399+ if ( typeof optionsData !== "string" ) throw new TypeError ( ) ;
400+ const options = maybeJsonParse ( optionsData ) ;
401+ const value = formData . get ( "value" ) ;
402+ return constructResponse ( await env . BUCKET . put ( key , value , options ) ) ;
384403 } else if ( method === "DELETE" ) {
385404 const keys = await request . json < string | string [ ] > ( ) ;
386405 await env . BUCKET . delete ( keys ) ;
@@ -796,6 +815,39 @@ test("put: stores only if passes onlyIf", async (t) => {
796815 const object = await r2 . put ( "no-key" , "2" , { onlyIf : { etagMatches : etag } } ) ;
797816 t . is ( object as R2Object | null , null ) ;
798817} ) ;
818+ test ( "put: validates metadata size" , async ( t ) => {
819+ const { r2 } = t . context ;
820+
821+ // TODO(soon): add check for max value size once we have streaming support
822+ // (don't really want to allocate 5GB buffers in tests :sweat_smile:)
823+
824+ const expectations : ThrowsExpectation = {
825+ instanceOf : Error ,
826+ message :
827+ "put: Your metadata headers exceed the maximum allowed metadata size. (10012)" ,
828+ } ;
829+
830+ // Check with ASCII characters
831+ await r2 . put ( "key" , "value" , { customMetadata : { key : "x" . repeat ( 2045 ) } } ) ;
832+ await t . throwsAsync (
833+ r2 . put ( "key" , "value" , { customMetadata : { key : "x" . repeat ( 2046 ) } } ) ,
834+ expectations
835+ ) ;
836+ await r2 . put ( "key" , "value" , { customMetadata : { hi : "x" . repeat ( 2046 ) } } ) ;
837+
838+ // Check with extended characters: note "🙂" is 2 UTF-16 code units, so
839+ // `"🙂".length === 2`, and it requires 4 bytes to store
840+ await r2 . put ( "key" , "value" , { customMetadata : { key : "🙂" . repeat ( 511 ) } } ) ; // 3 + 4*511 = 2047
841+ await r2 . put ( "key" , "value" , { customMetadata : { key1 : "🙂" . repeat ( 511 ) } } ) ; // 4 + 4*511 = 2048
842+ await t . throwsAsync (
843+ r2 . put ( "key" , "value" , { customMetadata : { key12 : "🙂" . repeat ( 511 ) } } ) , // 5 + 4*511 = 2049
844+ expectations
845+ ) ;
846+ await t . throwsAsync (
847+ r2 . put ( "key" , "value" , { customMetadata : { key : "🙂" . repeat ( 512 ) } } ) , // 3 + 4*512 = 2051
848+ expectations
849+ ) ;
850+ } ) ;
799851
800852test ( "delete: deletes existing keys" , async ( t ) => {
801853 const { r2 } = t . context ;
0 commit comments