@@ -279,6 +279,80 @@ describe("the no-JS form convention honors returned redirects (#3096)", () => {
279279 } ) ;
280280} ) ;
281281
282+ describe ( "the bare address reads the caller kind off Sec-Fetch-Mode (#3139)" , ( ) => {
283+ // The no-JS convention — 303, outcome in a flash cookie — exists for
284+ // form NAVIGATIONS. It used to engage on shape alone (form content type,
285+ // no format tag), which a page script's fetch(url, { body: new
286+ // URLSearchParams(...) }) also matches: the script got the 303, followed
287+ // it to the referrer's HTML, read `response.ok === true`, and its answer
288+ // sat in a cookie it would never look at. The browser's own word tells
289+ // the callers apart: navigations send `Sec-Fetch-Mode: navigate` (or
290+ // nothing, on older browsers), a script's fetch never does.
291+ function formShaped ( id : string , mode ?: string ) {
292+ return new Request ( `https://app.example/_server/${ id } ` , {
293+ method : "POST" ,
294+ headers : {
295+ "Sec-Fetch-Site" : "same-origin" ,
296+ "Content-Type" : "application/x-www-form-urlencoded" ,
297+ Referer : "https://app.example/current-page" ,
298+ ...( mode ? { "Sec-Fetch-Mode" : mode } : { } )
299+ } ,
300+ body : "a=1"
301+ } ) ;
302+ }
303+
304+ it ( "keeps the convention for navigations, with or without fetch metadata" , async ( ) => {
305+ registerServerFunction ( "nojs-mode-nav" , async ( ) => ( { saved : true } ) ) ;
306+
307+ // a modern browser's form navigation declares itself
308+ const declared = await handleServerFunctionRequest ( formShaped ( "nojs-mode-nav" , "navigate" ) ) ;
309+ expect ( declared . status ) . toBe ( 303 ) ;
310+ expect ( declared . headers . get ( "Set-Cookie" ) ) . toContain ( "flash=" ) ;
311+
312+ // an older browser sends no fetch metadata at all: same convention
313+ const bare = await handleServerFunctionRequest ( formShaped ( "nojs-mode-nav" ) ) ;
314+ expect ( bare . status ) . toBe ( 303 ) ;
315+ } ) ;
316+
317+ it ( "refuses a page script's form-shaped post before the function runs" , async ( ) => {
318+ const fn = vi . fn ( async ( ) => ( { receipt : "SECRET" } ) ) ;
319+ registerServerFunction ( "nojs-mode-script" , fn ) ;
320+
321+ for ( const mode of [ "cors" , "same-origin" , "no-cors" ] ) {
322+ const response = await handleServerFunctionRequest ( formShaped ( "nojs-mode-script" , mode ) ) ;
323+ // refused as malformed — not a 303 whose answer vanishes into the
324+ // caller's cookie jar, and not a 200 wearing the referrer's HTML
325+ expect ( [ mode , response . status ] ) . toEqual ( [ mode , 400 ] ) ;
326+ expect ( response . headers . get ( "Set-Cookie" ) ) . toBeNull ( ) ;
327+ }
328+ // before dispatch: the old shape's real harm was running the mutation
329+ // and then hiding the outcome
330+ expect ( fn ) . not . toHaveBeenCalled ( ) ;
331+ } ) ;
332+
333+ it ( "a tagged script call at the bare address keeps the plain response" , async ( ) => {
334+ // the documented direct-HTTP spelling: the format tag takes the call
335+ // out of the form shape entirely, whatever its fetch mode says
336+ registerServerFunction ( "nojs-mode-tagged" , async ( params : URLSearchParams ) => ( {
337+ got : params . get ( "a" )
338+ } ) ) ;
339+ const response = await handleServerFunctionRequest (
340+ new Request ( "https://app.example/_server/nojs-mode-tagged" , {
341+ method : "POST" ,
342+ headers : {
343+ "Sec-Fetch-Site" : "same-origin" ,
344+ "Sec-Fetch-Mode" : "cors" ,
345+ "Content-Type" : "application/x-www-form-urlencoded" ,
346+ "X-Server-Function-Format" : "3" // URLSearchParams
347+ } ,
348+ body : "a=1"
349+ } )
350+ ) ;
351+ expect ( response . status ) . toBe ( 200 ) ;
352+ expect ( await response . json ( ) ) . toEqual ( { got : "1" } ) ;
353+ } ) ;
354+ } ) ;
355+
282356/**
283357 * The mask is justified by ONE fact — that fetch follows these statuses —
284358 * so it has to cover exactly the set fetch follows. Exercising 302 alone
0 commit comments