@@ -53,10 +53,11 @@ export interface BrowserPreviewTopology {
5353export interface BrowserPreviewRouteTracker {
5454 pending : Set < Promise < void > >
5555 errors : unknown [ ]
56+ registrations : number
5657}
5758
5859export function createBrowserPreviewRouteTracker ( ) : BrowserPreviewRouteTracker {
59- return { pending : new Set ( ) , errors : [ ] }
60+ return { pending : new Set ( ) , errors : [ ] , registrations : 0 }
6061}
6162
6263export function browserPreviewRouting ( args : string [ ] , runtimeSpec : RuntimeCreateSpec | undefined , localPreviewOrigin : string ) : BrowserProbePreviewRouting {
@@ -271,19 +272,23 @@ export async function routeBrowserPreviewContextNetwork(context: import("playwri
271272
272273export async function drainBrowserPreviewRouteTracker ( tracker : BrowserPreviewRouteTracker , timeoutMs = BROWSER_PREVIEW_ROUTE_DRAIN_TIMEOUT_MS ) : Promise < void > {
273274 const deadline = Date . now ( ) + timeoutMs
274- while ( tracker . pending . size > 0 ) {
275+ let observedRegistrations = - 1
276+ while ( tracker . pending . size > 0 || tracker . registrations !== observedRegistrations ) {
277+ observedRegistrations = tracker . registrations
275278 const remainingMs = deadline - Date . now ( )
276279 if ( remainingMs <= 0 ) {
277280 throw new Error ( `wordpress.browser-probe route-host timed out waiting for ${ tracker . pending . size } routed request(s) to finish` )
278281 }
279282
283+ const pending = [ ...tracker . pending ]
280284 const result = await Promise . race ( [
281- Promise . allSettled ( [ ... tracker . pending ] ) . then ( ( ) => "drained" as const ) ,
285+ Promise . allSettled ( pending ) . then ( ( ) => "drained" as const ) ,
282286 wait ( remainingMs ) . then ( ( ) => "timeout" as const ) ,
283287 ] )
284288 if ( result === "timeout" ) {
285289 throw new Error ( `wordpress.browser-probe route-host timed out waiting for ${ tracker . pending . size } routed request(s) to finish` )
286290 }
291+ await wait ( 0 )
287292 }
288293
289294 if ( tracker . errors . length > 0 ) {
@@ -307,45 +312,67 @@ async function routeBrowserPreviewNetwork(routePattern: (url: string, handler: (
307312
308313 const origin = new URL ( previewOrigin )
309314 await routePattern ( "**/*" , async ( route ) => {
310- const request = route . request ( )
311- let requestUrl : URL
312- try {
313- requestUrl = new URL ( request . url ( ) )
314- } catch {
315- await route . continue ( )
316- return
317- }
318-
319- const host = normalizeBrowserPreviewHost ( requestUrl . hostname )
320- const stat = browserPreviewNetworkPolicyHostStat ( policy , host )
321- stat . requests += 1
322- stat . external = ! policy . firstPartyHosts . has ( host )
323-
324- if ( policy . blockHosts . has ( host ) || ( policy . mode === "block" && stat . external && ! policy . allowHosts . has ( host ) ) ) {
325- stat . blocked += 1
326- await route . abort ( "blockedbyclient" )
327- return
328- }
329-
330- if ( ! policy . routeHosts . has ( host ) ) {
331- await route . continue ( )
332- return
315+ let operation = "inspect-request"
316+ const task = handleBrowserPreviewRoute ( route , policy , origin , ( nextOperation ) => {
317+ operation = nextOperation
318+ } )
319+ if ( tracker ) {
320+ tracker . registrations += 1
321+ tracker . pending . add ( task )
333322 }
334-
335- stat . routed += 1
336- const task = fulfillBrowserPreviewRoutedHost ( route , requestUrl , policy , origin )
337- tracker ?. pending . add ( task )
338323 try {
339324 await task
340325 } catch ( error ) {
341- tracker ?. errors . push ( sanitizeBrowserPreviewRouteError ( error ) )
342- await route . abort ( "failed" ) . catch ( ( ) => undefined )
326+ if ( ! isBrowserPreviewRouteClosedError ( error ) ) {
327+ tracker ?. errors . push ( browserPreviewRouteCallbackError ( route , operation , error ) )
328+ }
329+ try {
330+ await route . abort ( "failed" )
331+ } catch ( abortError ) {
332+ if ( ! isBrowserPreviewRouteClosedError ( abortError ) ) {
333+ tracker ?. errors . push ( browserPreviewRouteCallbackError ( route , "abort-after-error" , abortError ) )
334+ }
335+ }
343336 } finally {
344337 tracker ?. pending . delete ( task )
345338 }
346339 } )
347340}
348341
342+ async function handleBrowserPreviewRoute ( route : Route , policy : BrowserPreviewNetworkPolicy , origin : URL , setOperation : ( operation : string ) => void ) : Promise < void > {
343+ const request = route . request ( )
344+ let requestUrl : URL
345+ try {
346+ requestUrl = new URL ( request . url ( ) )
347+ } catch {
348+ setOperation ( "continue-invalid-url" )
349+ await route . continue ( )
350+ return
351+ }
352+
353+ const host = normalizeBrowserPreviewHost ( requestUrl . hostname )
354+ const stat = browserPreviewNetworkPolicyHostStat ( policy , host )
355+ stat . requests += 1
356+ stat . external = ! policy . firstPartyHosts . has ( host )
357+
358+ if ( policy . blockHosts . has ( host ) || ( policy . mode === "block" && stat . external && ! policy . allowHosts . has ( host ) ) ) {
359+ stat . blocked += 1
360+ setOperation ( "abort-policy-block" )
361+ await route . abort ( "blockedbyclient" )
362+ return
363+ }
364+
365+ if ( ! policy . routeHosts . has ( host ) ) {
366+ setOperation ( "continue-unrouted" )
367+ await route . continue ( )
368+ return
369+ }
370+
371+ stat . routed += 1
372+ setOperation ( "fulfill-routed-host" )
373+ await fulfillBrowserPreviewRoutedHost ( route , requestUrl , policy , origin )
374+ }
375+
349376async function fulfillBrowserPreviewRoutedHost ( route : Route , requestUrl : URL , policy : BrowserPreviewNetworkPolicy , localOrigin : URL ) : Promise < void > {
350377 const response = await fetchBrowserPreviewRoutedHost ( route , requestUrl , policy , localOrigin )
351378 if ( ! response ) {
@@ -430,7 +457,9 @@ async function fetchBrowserPreviewRoutedHost(route: Route, requestUrl: URL, poli
430457
431458 let response : Awaited < ReturnType < Route [ "fetch" ] > > | undefined
432459 const resourceType = route . request ( ) . resourceType ( )
433- const maxAttempts = resourceType === "document" ? BROWSER_PREVIEW_ROUTE_DOCUMENT_FETCH_ATTEMPTS : BROWSER_PREVIEW_ROUTE_SUBRESOURCE_FETCH_ATTEMPTS
460+ const method = route . request ( ) . method ( ) . toUpperCase ( )
461+ const methodCanRetry = browserPreviewRouteMethodCanRetry ( method )
462+ const maxAttempts = methodCanRetry ? ( resourceType === "document" ? BROWSER_PREVIEW_ROUTE_DOCUMENT_FETCH_ATTEMPTS : BROWSER_PREVIEW_ROUTE_SUBRESOURCE_FETCH_ATTEMPTS ) : 1
434463 for ( let attempt = 1 ; attempt <= maxAttempts ; attempt += 1 ) {
435464 try {
436465 response = await route . fetch ( {
@@ -466,6 +495,9 @@ async function fetchBrowserPreviewRoutedHost(route: Route, requestUrl: URL, poli
466495 if ( ! response ) {
467496 return undefined
468497 }
498+ if ( ! methodCanRetry ) {
499+ return response
500+ }
469501
470502 const location = response . headers ( ) . location
471503 if ( ! location || response . status ( ) < 300 || response . status ( ) >= 400 ) {
@@ -514,6 +546,35 @@ export function isBrowserPreviewRouteFetchTransientTransportError(error: unknown
514546 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 )
515547}
516548
549+ export function isBrowserPreviewRouteClosedError ( error : unknown ) : boolean {
550+ 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 )
551+ }
552+
553+ function browserPreviewRouteMethodCanRetry ( method : string ) : boolean {
554+ return method === "GET" || method === "HEAD" || method === "OPTIONS"
555+ }
556+
557+ function browserPreviewRouteCallbackError ( route : Route , operation : string , error : unknown ) : Error {
558+ const request = browserPreviewRouteRequestSummary ( route )
559+ const cause = sanitizeBrowserPreviewRouteError ( error ) . message . replace ( / [ \r \n ] + / g, " " )
560+ const diagnostic = new Error ( `wordpress.browser-probe route callback failed: operation=${ operation } method=${ request . method } resourceType=${ request . resourceType } url=${ request . url } cause=${ cause } ` )
561+ diagnostic . name = "BrowserPreviewRouteCallbackError"
562+ return diagnostic
563+ }
564+
565+ function browserPreviewRouteRequestSummary ( route : Route ) : { method : string ; resourceType : string ; url : string } {
566+ try {
567+ const request = route . request ( )
568+ return {
569+ method : request . method ( ) ,
570+ resourceType : request . resourceType ( ) ,
571+ url : redactString ( request . url ( ) , { redactAllUrlQueryValues : true , redactUrlHash : true , redactQueryAssignments : true } ) ,
572+ }
573+ } catch {
574+ return { method : "unknown" , resourceType : "unknown" , url : "[unavailable]" }
575+ }
576+ }
577+
517578function browserPreviewRouteFetchExhaustedError ( route : Route , requestUrl : URL , attempts : number , error : unknown ) : Error {
518579 const method = route . request ( ) . method ( )
519580 const resourceType = route . request ( ) . resourceType ( )
0 commit comments