@@ -64,10 +64,11 @@ export interface BrowserPreviewOrigins {
6464export interface BrowserPreviewRouteTracker {
6565 pending : Set < Promise < void > >
6666 errors : unknown [ ]
67+ registrations : number
6768}
6869
6970export function createBrowserPreviewRouteTracker ( ) : BrowserPreviewRouteTracker {
70- return { pending : new Set ( ) , errors : [ ] }
71+ return { pending : new Set ( ) , errors : [ ] , registrations : 0 }
7172}
7273
7374export function browserPreviewRouting ( args : string [ ] , runtimeSpec : RuntimeCreateSpec | undefined , localPreviewOrigin : string ) : BrowserProbePreviewRouting {
@@ -325,19 +326,23 @@ export async function routeBrowserPreviewContextNetwork(context: import("playwri
325326
326327export async function drainBrowserPreviewRouteTracker ( tracker : BrowserPreviewRouteTracker , timeoutMs = BROWSER_PREVIEW_ROUTE_DRAIN_TIMEOUT_MS ) : Promise < void > {
327328 const deadline = Date . now ( ) + timeoutMs
328- while ( tracker . pending . size > 0 ) {
329+ let observedRegistrations = - 1
330+ while ( tracker . pending . size > 0 || tracker . registrations !== observedRegistrations ) {
331+ observedRegistrations = tracker . registrations
329332 const remainingMs = deadline - Date . now ( )
330333 if ( remainingMs <= 0 ) {
331334 throw new Error ( `wordpress.browser-probe route-host timed out waiting for ${ tracker . pending . size } routed request(s) to finish` )
332335 }
333336
337+ const pending = [ ...tracker . pending ]
334338 const result = await Promise . race ( [
335- Promise . allSettled ( [ ... tracker . pending ] ) . then ( ( ) => "drained" as const ) ,
339+ Promise . allSettled ( pending ) . then ( ( ) => "drained" as const ) ,
336340 wait ( remainingMs ) . then ( ( ) => "timeout" as const ) ,
337341 ] )
338342 if ( result === "timeout" ) {
339343 throw new Error ( `wordpress.browser-probe route-host timed out waiting for ${ tracker . pending . size } routed request(s) to finish` )
340344 }
345+ await wait ( 0 )
341346 }
342347
343348 if ( tracker . errors . length > 0 ) {
@@ -361,53 +366,77 @@ async function routeBrowserPreviewNetwork(routePattern: (url: string, handler: (
361366
362367 const origin = new URL ( previewOrigin )
363368 await routePattern ( "**/*" , async ( route ) => {
364- const request = route . request ( )
365- let requestUrl : URL
366- try {
367- requestUrl = new URL ( request . url ( ) )
368- } catch {
369- await route . continue ( )
370- return
371- }
372-
373- const host = normalizeBrowserPreviewHost ( requestUrl . hostname )
374- const stat = browserPreviewNetworkPolicyHostStat ( policy , host )
375- stat . requests += 1
376- stat . external = ! policy . firstPartyHosts . has ( host )
377-
378- if ( policy . blockHosts . has ( host ) ) {
379- stat . blocked += 1
380- await route . abort ( "blockedbyclient" )
381- return
369+ let operation = "inspect-request"
370+ const task = handleBrowserPreviewRoute ( route , policy , origin , ( nextOperation ) => {
371+ operation = nextOperation
372+ } )
373+ if ( tracker ) {
374+ tracker . registrations += 1
375+ tracker . pending . add ( task )
382376 }
383-
384- if ( policy . routeHosts . has ( host ) ) {
385- stat . routed += 1
386- if ( policy . preserveRoutedOrigin ) {
387- await route . continue ( )
388- return
377+ try {
378+ await task
379+ } catch ( error ) {
380+ if ( ! isBrowserPreviewRouteClosedError ( error ) ) {
381+ tracker ?. errors . push ( browserPreviewRouteCallbackError ( route , operation , error ) )
389382 }
390- const task = fulfillBrowserPreviewRoutedHost ( route , requestUrl , policy , origin )
391- tracker ?. pending . add ( task )
392383 try {
393- await task
394- } catch ( error ) {
395- tracker ?. errors . push ( sanitizeBrowserPreviewRouteError ( error ) )
396- await route . abort ( "failed" ) . catch ( ( ) => undefined )
397- } finally {
398- tracker ?. pending . delete ( task )
384+ await route . abort ( "failed" )
385+ } catch ( abortError ) {
386+ if ( ! isBrowserPreviewRouteClosedError ( abortError ) ) {
387+ tracker ?. errors . push ( browserPreviewRouteCallbackError ( route , "abort-after-error" , abortError ) )
388+ }
399389 }
400- return
390+ } finally {
391+ tracker ?. pending . delete ( task )
401392 }
393+ } )
394+ }
402395
403- if ( policy . preserveRoutedOrigin || ( policy . mode === "block" && stat . external && ! policy . allowHosts . has ( host ) ) || ( request . resourceType ( ) === "document" && stat . external ) ) {
404- stat . blocked += 1
405- await route . abort ( "blockedbyclient" )
396+ async function handleBrowserPreviewRoute ( route : Route , policy : BrowserPreviewNetworkPolicy , origin : URL , setOperation : ( operation : string ) => void ) : Promise < void > {
397+ const request = route . request ( )
398+ let requestUrl : URL
399+ try {
400+ requestUrl = new URL ( request . url ( ) )
401+ } catch {
402+ setOperation ( "continue-invalid-url" )
403+ await route . continue ( )
404+ return
405+ }
406+
407+ const host = normalizeBrowserPreviewHost ( requestUrl . hostname )
408+ const stat = browserPreviewNetworkPolicyHostStat ( policy , host )
409+ stat . requests += 1
410+ stat . external = ! policy . firstPartyHosts . has ( host )
411+
412+ if ( policy . blockHosts . has ( host ) ) {
413+ stat . blocked += 1
414+ setOperation ( "abort-policy-block" )
415+ await route . abort ( "blockedbyclient" )
416+ return
417+ }
418+
419+ if ( policy . routeHosts . has ( host ) ) {
420+ stat . routed += 1
421+ if ( policy . preserveRoutedOrigin ) {
422+ setOperation ( "continue-preserved-routed-origin" )
423+ await route . continue ( )
406424 return
407425 }
426+ setOperation ( "fulfill-routed-host" )
427+ await fulfillBrowserPreviewRoutedHost ( route , requestUrl , policy , origin )
428+ return
429+ }
408430
409- await route . continue ( )
410- } )
431+ if ( policy . preserveRoutedOrigin || ( policy . mode === "block" && stat . external && ! policy . allowHosts . has ( host ) ) || ( request . resourceType ( ) === "document" && stat . external ) ) {
432+ stat . blocked += 1
433+ setOperation ( "abort-policy-block" )
434+ await route . abort ( "blockedbyclient" )
435+ return
436+ }
437+
438+ setOperation ( "continue-unrouted" )
439+ await route . continue ( )
411440}
412441
413442async function fulfillBrowserPreviewRoutedHost ( route : Route , requestUrl : URL , policy : BrowserPreviewNetworkPolicy , localOrigin : URL ) : Promise < void > {
@@ -494,7 +523,9 @@ async function fetchBrowserPreviewRoutedHost(route: Route, requestUrl: URL, poli
494523
495524 let response : Awaited < ReturnType < Route [ "fetch" ] > > | undefined
496525 const resourceType = route . request ( ) . resourceType ( )
497- const maxAttempts = resourceType === "document" ? BROWSER_PREVIEW_ROUTE_DOCUMENT_FETCH_ATTEMPTS : BROWSER_PREVIEW_ROUTE_SUBRESOURCE_FETCH_ATTEMPTS
526+ const method = route . request ( ) . method ( ) . toUpperCase ( )
527+ const methodCanRetry = browserPreviewRouteMethodCanRetry ( method )
528+ const maxAttempts = methodCanRetry ? ( resourceType === "document" ? BROWSER_PREVIEW_ROUTE_DOCUMENT_FETCH_ATTEMPTS : BROWSER_PREVIEW_ROUTE_SUBRESOURCE_FETCH_ATTEMPTS ) : 1
498529 for ( let attempt = 1 ; attempt <= maxAttempts ; attempt += 1 ) {
499530 try {
500531 response = await route . fetch ( {
@@ -530,6 +561,9 @@ async function fetchBrowserPreviewRoutedHost(route: Route, requestUrl: URL, poli
530561 if ( ! response ) {
531562 return undefined
532563 }
564+ if ( ! methodCanRetry ) {
565+ return response
566+ }
533567
534568 const location = response . headers ( ) . location
535569 if ( ! location || response . status ( ) < 300 || response . status ( ) >= 400 ) {
@@ -578,6 +612,35 @@ export function isBrowserPreviewRouteFetchTransientTransportError(error: unknown
578612 return error instanceof Error && / \b (?: E C O N N R E S E T | E C O N N R E F U S E D | E P I P E | E T I M E D O U T | U N D _ E R R _ S O C K E T | s o c k e t (?: h a n g u p | c l o s e d | e n d e d ) | c o n n e c t i o n (?: r e s e t | r e f u s e d | c l o s e d ) | o t h e r s i d e c l o s e d ) \b / i. test ( error . message )
579613}
580614
615+ export function isBrowserPreviewRouteClosedError ( error : unknown ) : boolean {
616+ return error instanceof Error && / (?: R e q u e s t c o n t e x t d i s p o s e d | T a r g e t (?: p a g e , c o n t e x t o r b r o w s e r | p a g e | c o n t e x t | b r o w s e r ) h a s b e e n c l o s e d | B r o w s e r h a s b e e n c l o s e d | c o n t e x t c l o s e d | p a g e c l o s e d ) / i. test ( error . message )
617+ }
618+
619+ function browserPreviewRouteMethodCanRetry ( method : string ) : boolean {
620+ return method === "GET" || method === "HEAD" || method === "OPTIONS"
621+ }
622+
623+ function browserPreviewRouteCallbackError ( route : Route , operation : string , error : unknown ) : Error {
624+ const request = browserPreviewRouteRequestSummary ( route )
625+ const cause = sanitizeBrowserPreviewRouteError ( error ) . message . replace ( / [ \r \n ] + / g, " " )
626+ const diagnostic = new Error ( `wordpress.browser-probe route callback failed: operation=${ operation } method=${ request . method } resourceType=${ request . resourceType } url=${ request . url } cause=${ cause } ` )
627+ diagnostic . name = "BrowserPreviewRouteCallbackError"
628+ return diagnostic
629+ }
630+
631+ function browserPreviewRouteRequestSummary ( route : Route ) : { method : string ; resourceType : string ; url : string } {
632+ try {
633+ const request = route . request ( )
634+ return {
635+ method : request . method ( ) ,
636+ resourceType : request . resourceType ( ) ,
637+ url : redactString ( request . url ( ) , { redactAllUrlQueryValues : true , redactUrlHash : true , redactQueryAssignments : true } ) ,
638+ }
639+ } catch {
640+ return { method : "unknown" , resourceType : "unknown" , url : "[unavailable]" }
641+ }
642+ }
643+
581644function browserPreviewRouteFetchExhaustedError ( route : Route , requestUrl : URL , attempts : number , error : unknown ) : Error {
582645 const method = route . request ( ) . method ( )
583646 const resourceType = route . request ( ) . resourceType ( )
0 commit comments