@@ -225,6 +225,146 @@ describe("Blob Storage", () => {
225225 } ) ;
226226 } ) ;
227227
228+ describe ( "Content-Type Detection" , ( ) => {
229+ it ( "should detect video/mp4 from magic bytes when stored with */*" , async ( ) => {
230+ // Create a valid MP4 header (ftyp box with isom brand)
231+ const mp4Header = new Uint8Array ( [
232+ 0x00 , 0x00 , 0x00 , 0x14 , // box size (20 bytes)
233+ 0x66 , 0x74 , 0x79 , 0x70 , // "ftyp"
234+ 0x69 , 0x73 , 0x6f , 0x6d , // "isom" brand
235+ 0x00 , 0x00 , 0x00 , 0x01 , // minor version
236+ 0x69 , 0x73 , 0x6f , 0x6d , // compatible brand
237+ ] ) ;
238+
239+ // Upload with wildcard content type (simulating the bug)
240+ const uploadResponse = await worker . fetch (
241+ new Request ( "http://pds.test/xrpc/com.atproto.repo.uploadBlob" , {
242+ method : "POST" ,
243+ headers : {
244+ "Content-Type" : "*/*" ,
245+ Authorization : `Bearer ${ env . AUTH_TOKEN } ` ,
246+ } ,
247+ body : mp4Header ,
248+ } ) ,
249+ env ,
250+ ) ;
251+
252+ expect ( uploadResponse . status ) . toBe ( 200 ) ;
253+
254+ const uploadData = ( await uploadResponse . json ( ) ) as {
255+ blob : { ref : { $link : string } } ;
256+ } ;
257+ const cid = uploadData . blob . ref . $link ;
258+
259+ // Retrieve - should detect video/mp4 from magic bytes
260+ const getResponse = await worker . fetch (
261+ new Request (
262+ `http://pds.test/xrpc/com.atproto.sync.getBlob?did=${ env . DID } &cid=${ cid } ` ,
263+ ) ,
264+ env ,
265+ ) ;
266+
267+ expect ( getResponse . status ) . toBe ( 200 ) ;
268+ expect ( getResponse . headers . get ( "Content-Type" ) ) . toBe ( "video/mp4" ) ;
269+ } ) ;
270+
271+ it ( "should detect image/jpeg from magic bytes when stored with */*" , async ( ) => {
272+ // JPEG magic bytes
273+ const jpegData = new Uint8Array ( [ 0xFF , 0xD8 , 0xFF , 0xE0 , 0x00 , 0x10 , 0x4A , 0x46 , 0x49 , 0x46 , 0x00 , 0x01 ] ) ;
274+
275+ const uploadResponse = await worker . fetch (
276+ new Request ( "http://pds.test/xrpc/com.atproto.repo.uploadBlob" , {
277+ method : "POST" ,
278+ headers : {
279+ "Content-Type" : "*/*" ,
280+ Authorization : `Bearer ${ env . AUTH_TOKEN } ` ,
281+ } ,
282+ body : jpegData ,
283+ } ) ,
284+ env ,
285+ ) ;
286+
287+ const uploadData = ( await uploadResponse . json ( ) ) as {
288+ blob : { ref : { $link : string } } ;
289+ } ;
290+ const cid = uploadData . blob . ref . $link ;
291+
292+ const getResponse = await worker . fetch (
293+ new Request (
294+ `http://pds.test/xrpc/com.atproto.sync.getBlob?did=${ env . DID } &cid=${ cid } ` ,
295+ ) ,
296+ env ,
297+ ) ;
298+
299+ expect ( getResponse . status ) . toBe ( 200 ) ;
300+ expect ( getResponse . headers . get ( "Content-Type" ) ) . toBe ( "image/jpeg" ) ;
301+ } ) ;
302+
303+ it ( "should detect image/png from magic bytes" , async ( ) => {
304+ // PNG magic bytes
305+ const pngData = new Uint8Array ( [ 0x89 , 0x50 , 0x4E , 0x47 , 0x0D , 0x0A , 0x1A , 0x0A , 0x00 , 0x00 , 0x00 , 0x0D ] ) ;
306+
307+ const uploadResponse = await worker . fetch (
308+ new Request ( "http://pds.test/xrpc/com.atproto.repo.uploadBlob" , {
309+ method : "POST" ,
310+ headers : {
311+ "Content-Type" : "*/*" ,
312+ Authorization : `Bearer ${ env . AUTH_TOKEN } ` ,
313+ } ,
314+ body : pngData ,
315+ } ) ,
316+ env ,
317+ ) ;
318+
319+ const uploadData = ( await uploadResponse . json ( ) ) as {
320+ blob : { ref : { $link : string } } ;
321+ } ;
322+ const cid = uploadData . blob . ref . $link ;
323+
324+ const getResponse = await worker . fetch (
325+ new Request (
326+ `http://pds.test/xrpc/com.atproto.sync.getBlob?did=${ env . DID } &cid=${ cid } ` ,
327+ ) ,
328+ env ,
329+ ) ;
330+
331+ expect ( getResponse . status ) . toBe ( 200 ) ;
332+ expect ( getResponse . headers . get ( "Content-Type" ) ) . toBe ( "image/png" ) ;
333+ } ) ;
334+
335+ it ( "should fallback to application/octet-stream for unknown content" , async ( ) => {
336+ // Random bytes that don't match any known format
337+ const unknownData = new Uint8Array ( [ 0x01 , 0x02 , 0x03 , 0x04 , 0x05 , 0x06 , 0x07 , 0x08 , 0x09 , 0x0A , 0x0B , 0x0C ] ) ;
338+
339+ const uploadResponse = await worker . fetch (
340+ new Request ( "http://pds.test/xrpc/com.atproto.repo.uploadBlob" , {
341+ method : "POST" ,
342+ headers : {
343+ "Content-Type" : "*/*" ,
344+ Authorization : `Bearer ${ env . AUTH_TOKEN } ` ,
345+ } ,
346+ body : unknownData ,
347+ } ) ,
348+ env ,
349+ ) ;
350+
351+ const uploadData = ( await uploadResponse . json ( ) ) as {
352+ blob : { ref : { $link : string } } ;
353+ } ;
354+ const cid = uploadData . blob . ref . $link ;
355+
356+ const getResponse = await worker . fetch (
357+ new Request (
358+ `http://pds.test/xrpc/com.atproto.sync.getBlob?did=${ env . DID } &cid=${ cid } ` ,
359+ ) ,
360+ env ,
361+ ) ;
362+
363+ expect ( getResponse . status ) . toBe ( 200 ) ;
364+ expect ( getResponse . headers . get ( "Content-Type" ) ) . toBe ( "application/octet-stream" ) ;
365+ } ) ;
366+ } ) ;
367+
228368 describe ( "Integration" , ( ) => {
229369 it ( "should handle upload and retrieval flow" , async ( ) => {
230370 // Create test data
0 commit comments