@@ -12,10 +12,12 @@ import {
1212describe ( 'output export fallback helpers' , ( ) => {
1313 const originalFetch = global . fetch
1414 const originalBasePath = process . env . __NEXT_ROUTER_BASEPATH
15+ const originalTrailingSlash = process . env . __NEXT_TRAILING_SLASH
1516
1617 afterEach ( ( ) => {
1718 global . fetch = originalFetch
1819 process . env . __NEXT_ROUTER_BASEPATH = originalBasePath
20+ process . env . __NEXT_TRAILING_SLASH = originalTrailingSlash
1921 clearOutputExportFallbackManifestCache ( )
2022 jest . restoreAllMocks ( )
2123 } )
@@ -93,18 +95,49 @@ describe('output export fallback helpers', () => {
9395 ] )
9496 } )
9597
98+ it ( 'tries the direct fallback artifact before manifest lookups' , async ( ) => {
99+ process . env . __NEXT_TRAILING_SLASH = 'false'
100+ const fetchMock = jest . fn ( async ( input : RequestInfo | URL ) => {
101+ const url = String ( input )
102+
103+ if ( url . endsWith ( '/org/acme/chat/123/__fallback.txt' ) ) {
104+ return new Response ( 'payload' , {
105+ status : 200 ,
106+ headers : { 'content-type' : 'text/plain' } ,
107+ } )
108+ }
109+
110+ return new Response ( 'not found' , {
111+ status : 404 ,
112+ headers : { 'content-type' : 'text/html' } ,
113+ } )
114+ } )
115+
116+ global . fetch = fetchMock as typeof fetch
117+
118+ const renderedUrl = new URL ( 'https://example.com/org/acme/chat/123' )
119+ const result = await fetchOutputExportFallbackResponse ( renderedUrl )
120+
121+ expect ( result ) . not . toBeNull ( )
122+ expect ( result ?. renderedUrl . href ) . toBe ( renderedUrl . href )
123+ expect ( fetchMock . mock . calls . map ( ( [ url ] ) => String ( url ) ) ) . toEqual ( [
124+ 'https://example.com/org/acme/chat/123/__fallback.txt' ,
125+ ] )
126+ } )
127+
96128 it ( 'falls through deeper prefixes before using a shallower fallback artifact' , async ( ) => {
129+ process . env . __NEXT_TRAILING_SLASH = 'false'
97130 const fetchMock = jest . fn ( async ( input : RequestInfo | URL ) => {
98131 const url = String ( input )
99132
100- if ( url . endsWith ( '/docs/guides/export/__fallback.meta.json ' ) ) {
133+ if ( url . endsWith ( '/docs/guides/export/__fallback.txt ' ) ) {
101134 return new Response ( 'not found' , {
102135 status : 404 ,
103136 headers : { 'content-type' : 'text/html' } ,
104137 } )
105138 }
106139
107- if ( url . endsWith ( '/docs/guides/export/__fallback.txt ' ) ) {
140+ if ( url . endsWith ( '/docs/guides/export/__fallback.meta.json ' ) ) {
108141 return new Response ( 'not found' , {
109142 status : 404 ,
110143 headers : { 'content-type' : 'text/html' } ,
@@ -133,18 +166,57 @@ describe('output export fallback helpers', () => {
133166 expect ( result ?. renderedUrl . href ) . toBe ( renderedUrl . href )
134167 expect ( result ?. fallbackUrl . pathname ) . toBe ( '/docs/guides/__fallback' )
135168 expect ( fetchMock . mock . calls . map ( ( [ url ] ) => String ( url ) ) ) . toEqual ( [
136- 'https://example.com/docs/guides/export/__fallback.meta.json' ,
137169 'https://example.com/docs/guides/export/__fallback.txt' ,
138- 'https://example.com/docs/guides/export/__fallback/index.txt' ,
139- 'https://example.com/docs/guides/__fallback.meta.json' ,
170+ 'https://example.com/docs/guides/export/__fallback.meta.json' ,
140171 'https://example.com/docs/guides/__fallback.txt' ,
141172 ] )
142173 } )
143174
175+ it ( 'prefers the trailing-slash fallback artifact when the request URL ends with /' , async ( ) => {
176+ delete process . env . __NEXT_TRAILING_SLASH
177+
178+ const fetchMock = jest . fn ( async ( input : RequestInfo | URL ) => {
179+ const url = String ( input )
180+
181+ if ( url . endsWith ( '/org/umbrella/chat/thread-789/__fallback/index.txt' ) ) {
182+ return new Response ( 'flight' , {
183+ status : 200 ,
184+ headers : { 'content-type' : 'text/x-component' } ,
185+ } )
186+ }
187+
188+ return new Response ( 'not found' , {
189+ status : 404 ,
190+ headers : { 'content-type' : 'text/html' } ,
191+ } )
192+ } )
193+
194+ global . fetch = fetchMock as typeof fetch
195+
196+ const renderedUrl = new URL (
197+ 'https://example.com/org/umbrella/chat/thread-789/'
198+ )
199+ const result = await fetchOutputExportFallbackResponse ( renderedUrl )
200+
201+ expect ( result ) . not . toBeNull ( )
202+ expect ( result ?. renderedUrl . href ) . toBe ( renderedUrl . href )
203+ expect ( fetchMock . mock . calls . map ( ( [ url ] ) => String ( url ) ) ) . toEqual ( [
204+ 'https://example.com/org/umbrella/chat/thread-789/__fallback/index.txt' ,
205+ ] )
206+ } )
207+
144208 it ( 'uses fallback metadata to select the most specific conflicting route' , async ( ) => {
209+ process . env . __NEXT_TRAILING_SLASH = 'false'
145210 const fetchMock = jest . fn ( async ( input : RequestInfo | URL ) => {
146211 const url = String ( input )
147212
213+ if ( url . endsWith ( '/docs/__fallback.txt' ) ) {
214+ return new Response ( 'not found' , {
215+ status : 404 ,
216+ headers : { 'content-type' : 'text/html' } ,
217+ } )
218+ }
219+
148220 if ( url . endsWith ( '/docs/__fallback.meta.json' ) ) {
149221 return new Response (
150222 JSON . stringify ( {
@@ -187,16 +259,11 @@ describe('output export fallback helpers', () => {
187259
188260 expect ( result ) . not . toBeNull ( )
189261 expect ( result ?. fallbackUrl . pathname ) . toBe ( '/docs/__fallback/__route_0' )
190- const requestedUrls = fetchMock . mock . calls . map ( ( [ url ] ) => String ( url ) )
191- expect ( requestedUrls ) . toContain (
192- 'https://example.com/docs/__fallback.meta.json'
193- )
194- expect ( requestedUrls ) . toContain (
195- 'https://example.com/docs/__fallback/__route_0.txt'
196- )
197- expect ( requestedUrls ) . not . toContain (
198- 'https://example.com/docs/__fallback/__route_1.txt'
199- )
262+ expect ( fetchMock . mock . calls . map ( ( [ url ] ) => String ( url ) ) ) . toEqual ( [
263+ 'https://example.com/docs/__fallback.txt' ,
264+ 'https://example.com/docs/__fallback.meta.json' ,
265+ 'https://example.com/docs/__fallback/__route_0.txt' ,
266+ ] )
200267 } )
201268
202269 it ( 'matches and fetches conflicting fallback branches under basePath' , async ( ) => {
@@ -259,6 +326,7 @@ describe('output export fallback helpers', () => {
259326 } )
260327
261328 it ( 'caches the resolved fallback data URL for later RSC fetches' , async ( ) => {
329+ process . env . __NEXT_TRAILING_SLASH = 'false'
262330 const fetchMock = jest . fn ( async ( input : RequestInfo | URL ) => {
263331 const url = String ( input )
264332
@@ -325,9 +393,17 @@ describe('output export fallback helpers', () => {
325393 } )
326394
327395 it ( 'dedupes fallback artifact fetches across sibling routes' , async ( ) => {
396+ process . env . __NEXT_TRAILING_SLASH = 'false'
328397 const fetchMock = jest . fn ( async ( input : RequestInfo | URL ) => {
329398 const url = String ( input )
330399
400+ if ( url . endsWith ( '/docs/__fallback.txt' ) ) {
401+ return new Response ( 'not found' , {
402+ status : 404 ,
403+ headers : { 'content-type' : 'text/html' } ,
404+ } )
405+ }
406+
331407 if ( url . endsWith ( '/docs/__fallback.meta.json' ) ) {
332408 return new Response (
333409 JSON . stringify ( {
@@ -373,6 +449,7 @@ describe('output export fallback helpers', () => {
373449 )
374450
375451 expect ( fetchMock . mock . calls . map ( ( [ url ] ) => String ( url ) ) ) . toEqual ( [
452+ 'https://example.com/docs/__fallback.txt' ,
376453 'https://example.com/docs/__fallback.meta.json' ,
377454 'https://example.com/docs/__fallback/__route_0.txt' ,
378455 ] )
0 commit comments