@@ -85,6 +85,56 @@ describe("registerStaticRoutes", () => {
8585 expect ( await response . text ( ) ) . toBe ( "<h1>Hello</h1>" ) ;
8686 } ) ;
8787
88+ test ( "does not advertise Accept-Ranges when storage ignores Range" , async ( ) => {
89+ const handler = staticHandler ( ) ;
90+ const runQuery = vi . fn ( ) . mockResolvedValue ( {
91+ storageUrl : "https://storage.example/ep.mp3" ,
92+ contentType : "audio/mpeg" ,
93+ etag : '"storage-id"' ,
94+ } ) ;
95+ vi . stubGlobal (
96+ "fetch" ,
97+ vi . fn ( ) . mockResolvedValue ( new Response ( "0123456789" ) ) ,
98+ ) ;
99+
100+ const response = await invokeHandler (
101+ handler ,
102+ runQuery ,
103+ new Request ( "https://app.convex.site/ep.mp3" , {
104+ headers : { Range : "bytes=2-4" } ,
105+ } ) ,
106+ ) ;
107+
108+ expect ( response . status ) . toBe ( 200 ) ;
109+ expect ( response . headers . get ( "Accept-Ranges" ) ) . toBeNull ( ) ;
110+ } ) ;
111+
112+ test ( "ignores Range when If-Range does not match the current ETag" , async ( ) => {
113+ const handler = staticHandler ( ) ;
114+ const runQuery = vi . fn ( ) . mockResolvedValue ( {
115+ storageUrl : "https://storage.example/ep.mp3" ,
116+ contentType : "audio/mpeg" ,
117+ etag : '"storage-id"' ,
118+ } ) ;
119+ const fetchMock = vi
120+ . fn ( )
121+ . mockResolvedValue ( new Response ( "0123456789" , { status : 200 } ) ) ;
122+ vi . stubGlobal ( "fetch" , fetchMock ) ;
123+
124+ const response = await invokeHandler (
125+ handler ,
126+ runQuery ,
127+ new Request ( "https://app.convex.site/ep.mp3" , {
128+ headers : { Range : "bytes=2-4" , "If-Range" : '"old-storage-id"' } ,
129+ } ) ,
130+ ) ;
131+
132+ expect ( response . status ) . toBe ( 200 ) ;
133+ expect ( await response . text ( ) ) . toBe ( "0123456789" ) ;
134+ const init = fetchMock . mock . calls [ 0 ] ?. [ 1 ] as RequestInit | undefined ;
135+ expect ( new Headers ( init ?. headers ) . get ( "Range" ) ) . toBeNull ( ) ;
136+ } ) ;
137+
88138 test ( "serves an inherited v1 asset from the app's storage during migration" , async ( ) => {
89139 const handler = staticHandler ( ) ;
90140 const runQuery = vi . fn ( ) . mockResolvedValue ( {
@@ -302,3 +352,141 @@ describe("exposeDeploymentQuery", () => {
302352 } ) ;
303353 } ) ;
304354} ) ;
355+
356+ describe ( "storage-URL Range passthrough" , ( ) => {
357+ const storedFile = {
358+ storageUrl : "https://storage.example/ep.mp3" ,
359+ contentType : "audio/mpeg" ,
360+ etag : '"storage-id"' ,
361+ } ;
362+
363+ test ( "passes a storage 206 through with its Content-Range and Accept-Ranges" , async ( ) => {
364+ const handler = staticHandler ( ) ;
365+ const runQuery = vi . fn ( ) . mockResolvedValue ( storedFile ) ;
366+ const fetchMock = vi . fn ( ) . mockResolvedValue (
367+ new Response ( "234" , {
368+ status : 206 ,
369+ headers : { "Content-Range" : "bytes 2-4/10" , "Accept-Ranges" : "bytes" } ,
370+ } ) ,
371+ ) ;
372+ vi . stubGlobal ( "fetch" , fetchMock ) ;
373+
374+ const response = await invokeHandler (
375+ handler ,
376+ runQuery ,
377+ new Request ( "https://app.convex.site/ep.mp3" , {
378+ headers : { Range : "bytes=2-4" } ,
379+ } ) ,
380+ ) ;
381+
382+ expect ( response . status ) . toBe ( 206 ) ;
383+ expect ( response . headers . get ( "Content-Range" ) ) . toBe ( "bytes 2-4/10" ) ;
384+ expect ( response . headers . get ( "Accept-Ranges" ) ) . toBe ( "bytes" ) ;
385+ expect ( await response . text ( ) ) . toBe ( "234" ) ;
386+ const init = fetchMock . mock . calls [ 0 ] ?. [ 1 ] as RequestInit | undefined ;
387+ expect ( new Headers ( init ?. headers ) . get ( "Range" ) ) . toBe ( "bytes=2-4" ) ;
388+ } ) ;
389+
390+ test ( "passes a storage 416 through with its Content-Range" , async ( ) => {
391+ const handler = staticHandler ( ) ;
392+ const runQuery = vi . fn ( ) . mockResolvedValue ( storedFile ) ;
393+ vi . stubGlobal (
394+ "fetch" ,
395+ vi . fn ( ) . mockResolvedValue (
396+ new Response ( null , {
397+ status : 416 ,
398+ headers : { "Content-Range" : "bytes */10" } ,
399+ } ) ,
400+ ) ,
401+ ) ;
402+
403+ const response = await invokeHandler (
404+ handler ,
405+ runQuery ,
406+ new Request ( "https://app.convex.site/ep.mp3" , {
407+ headers : { Range : "bytes=50-" } ,
408+ } ) ,
409+ ) ;
410+
411+ expect ( response . status ) . toBe ( 416 ) ;
412+ expect ( response . headers . get ( "Content-Range" ) ) . toBe ( "bytes */10" ) ;
413+ expect ( response . headers . get ( "X-Content-Type-Options" ) ) . toBe ( "nosniff" ) ;
414+ expect ( response . headers . get ( "Cache-Control" ) ) . toBe ( "no-store" ) ;
415+ expect ( response . headers . get ( "ETag" ) ) . toBe ( '"storage-id"' ) ;
416+ } ) ;
417+
418+ test ( "forwards Range when If-Range matches the current ETag" , async ( ) => {
419+ const handler = staticHandler ( ) ;
420+ const runQuery = vi . fn ( ) . mockResolvedValue ( storedFile ) ;
421+ const fetchMock = vi . fn ( ) . mockResolvedValue (
422+ new Response ( "234" , {
423+ status : 206 ,
424+ headers : { "Content-Range" : "bytes 2-4/10" } ,
425+ } ) ,
426+ ) ;
427+ vi . stubGlobal ( "fetch" , fetchMock ) ;
428+
429+ const response = await invokeHandler (
430+ handler ,
431+ runQuery ,
432+ new Request ( "https://app.convex.site/ep.mp3" , {
433+ headers : { Range : "bytes=2-4" , "If-Range" : '"storage-id"' } ,
434+ } ) ,
435+ ) ;
436+
437+ expect ( response . status ) . toBe ( 206 ) ;
438+ const init = fetchMock . mock . calls [ 0 ] ?. [ 1 ] as RequestInit | undefined ;
439+ expect ( new Headers ( init ?. headers ) . get ( "Range" ) ) . toBe ( "bytes=2-4" ) ;
440+ } ) ;
441+ } ) ;
442+
443+ describe ( "storage Accept-Ranges advertisement" , ( ) => {
444+ test ( "does not advertise bytes when storage answers Accept-Ranges: none" , async ( ) => {
445+ const handler = staticHandler ( ) ;
446+ const runQuery = vi . fn ( ) . mockResolvedValue ( {
447+ storageUrl : "https://storage.example/ep.mp3" ,
448+ contentType : "audio/mpeg" ,
449+ etag : '"storage-id"' ,
450+ } ) ;
451+ vi . stubGlobal (
452+ "fetch" ,
453+ vi . fn ( ) . mockResolvedValue (
454+ new Response ( "0123456789" , { headers : { "Accept-Ranges" : "none" } } ) ,
455+ ) ,
456+ ) ;
457+
458+ const response = await invokeHandler (
459+ handler ,
460+ runQuery ,
461+ new Request ( "https://app.convex.site/ep.mp3" ) ,
462+ ) ;
463+
464+ expect ( response . status ) . toBe ( 200 ) ;
465+ expect ( response . headers . get ( "Accept-Ranges" ) ) . toBeNull ( ) ;
466+ } ) ;
467+ } ) ;
468+
469+ describe ( "storage passthrough with multi-range headers" , ( ) => {
470+ test ( "a multi-range header is not forwarded to storage" , async ( ) => {
471+ const handler = staticHandler ( ) ;
472+ const runQuery = vi . fn ( ) . mockResolvedValue ( {
473+ storageUrl : "https://storage.example/ep.mp3" ,
474+ contentType : "audio/mpeg" ,
475+ etag : '"storage-id"' ,
476+ } ) ;
477+ const fetchMock = vi . fn ( ) . mockResolvedValue ( new Response ( "0123456789" ) ) ;
478+ vi . stubGlobal ( "fetch" , fetchMock ) ;
479+
480+ const response = await invokeHandler (
481+ handler ,
482+ runQuery ,
483+ new Request ( "https://app.convex.site/ep.mp3" , {
484+ headers : { Range : "bytes=0-1,3-4" } ,
485+ } ) ,
486+ ) ;
487+
488+ expect ( response . status ) . toBe ( 200 ) ;
489+ const init = fetchMock . mock . calls [ 0 ] ?. [ 1 ] as RequestInit | undefined ;
490+ expect ( new Headers ( init ?. headers ) . get ( "Range" ) ) . toBeNull ( ) ;
491+ } ) ;
492+ } ) ;
0 commit comments