@@ -372,8 +372,8 @@ export class ContainerProxy extends WorkerEntrypoint<Cloudflare.Env, ContainerPr
372372 }
373373
374374 // In per-host mode, only specific hosts were intercepted.
375- // No catch-all or enableInternet fallback applies — if no handler
376- // matched above, the host was intercepted for allow/deny only .
375+ // If no handler matched above, fall back to direct internet access only
376+ // when the container already allows it .
377377 if ( ! interceptAll ) {
378378 if ( allowedHosts || enableInternet ) {
379379 return fetch ( request ) ;
@@ -635,7 +635,6 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
635635 async removeOutboundByHost ( hostname : string ) : Promise < void > {
636636 delete this . outboundByHostOverrides [ hostname ] ;
637637 await this . refreshOutboundInterception ( ) ;
638- await this . cleanupRemovedHostInterception ( [ hostname ] ) ;
639638 }
640639
641640 /**
@@ -653,18 +652,13 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
653652 this . validateOutboundHandlerMethodName ( methodName ) ;
654653 }
655654
656- const removedHosts = Object . keys ( this . outboundByHostOverrides ) . filter (
657- hostname => ! ( hostname in handlers )
658- ) ;
659-
660655 this . outboundByHostOverrides = Object . fromEntries (
661656 Object . entries ( handlers ) . map ( ( [ hostname , handler ] ) => [
662657 hostname ,
663658 typeof handler === 'string' ? { method : handler } : handler ,
664659 ] )
665660 ) ;
666661 await this . refreshOutboundInterception ( ) ;
667- await this . cleanupRemovedHostInterception ( removedHosts ) ;
668662 }
669663
670664 // ====================================
@@ -678,11 +672,9 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
678672 * @param hosts - Array of hostnames to allow (e.g. `['api.stripe.com', 'example.com']`)
679673 */
680674 async setAllowedHosts ( hosts : string [ ] ) : Promise < void > {
681- const removedHosts = ( this . effectiveAllowedHosts ?? [ ] ) . filter ( host => ! hosts . includes ( host ) ) ;
682675 this . allowedHostsOverride = [ ...hosts ] ;
683676 this . usingInterception = true ;
684677 await this . refreshOutboundInterception ( ) ;
685- await this . cleanupRemovedHostInterception ( removedHosts ) ;
686678 }
687679
688680 /**
@@ -693,11 +685,9 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
693685 * @param hosts - Array of hostnames to deny (e.g. `['evil.com', 'blocked.org']`)
694686 */
695687 async setDeniedHosts ( hosts : string [ ] ) : Promise < void > {
696- const removedHosts = ( this . effectiveDeniedHosts ?? [ ] ) . filter ( host => ! hosts . includes ( host ) ) ;
697688 this . deniedHostsOverride = [ ...hosts ] ;
698689 this . usingInterception = true ;
699690 await this . refreshOutboundInterception ( ) ;
700- await this . cleanupRemovedHostInterception ( removedHosts ) ;
701691 }
702692
703693 /**
@@ -736,7 +726,6 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
736726 async removeAllowedHost ( hostname : string ) : Promise < void > {
737727 this . allowedHostsOverride = ( this . effectiveAllowedHosts ?? [ ] ) . filter ( h => h !== hostname ) ;
738728 await this . refreshOutboundInterception ( ) ;
739- await this . cleanupRemovedHostInterception ( [ hostname ] ) ;
740729 }
741730
742731 /**
@@ -747,7 +736,6 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
747736 async removeDeniedHost ( hostname : string ) : Promise < void > {
748737 this . deniedHostsOverride = ( this . effectiveDeniedHosts ?? [ ] ) . filter ( h => h !== hostname ) ;
749738 await this . refreshOutboundInterception ( ) ;
750- await this . cleanupRemovedHostInterception ( [ hostname ] ) ;
751739 }
752740
753741 // ==========================
@@ -1353,6 +1341,10 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
13531341 private allowedHostsOverride ?: string [ ] ;
13541342 private deniedHostsOverride ?: string [ ] ;
13551343
1344+ // The runtime does not expose a way to remove outbound interceptions yet, so
1345+ // once we promote an instance to intercept-all we must keep using it.
1346+ private hasInterceptAllRegistration = false ;
1347+
13561348 // ==========================
13571349 // GENERAL HELPERS
13581350 // ==========================
@@ -1451,13 +1443,33 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
14511443 return ctor . outbound !== undefined || this . outboundHandlerOverride !== undefined ;
14521444 }
14531445
1446+ private hasMutableOutboundConfiguration ( ) : boolean {
1447+ return (
1448+ Object . keys ( this . outboundByHostOverrides ) . length > 0 ||
1449+ this . allowedHostsOverride !== undefined ||
1450+ this . deniedHostsOverride !== undefined
1451+ ) ;
1452+ }
1453+
1454+ private shouldInterceptAllOutbound ( ) : boolean {
1455+ return (
1456+ this . hasInterceptAllRegistration ||
1457+ this . needsCatchAllInterception ( ) ||
1458+ this . effectiveAllowedHosts !== undefined ||
1459+ this . effectiveDeniedHosts !== undefined ||
1460+ this . hasMutableOutboundConfiguration ( )
1461+ ) ;
1462+ }
1463+
1464+ private getStaticOutboundByHostKeys ( ) : string [ ] {
1465+ const ctor = this . constructor as typeof Container ;
1466+ return ctor . outboundByHost ? Object . keys ( ctor . outboundByHost ) : [ ] ;
1467+ }
1468+
14541469 /**
14551470 * Collects all hostnames that need per-host outbound interception.
1456- * This is the union of:
1457- * - Static `outboundByHost` keys
1458- * - Runtime `outboundByHostOverrides` keys
1459- * - `allowedHosts`
1460- * - `deniedHosts`
1471+ * This path is only used for the narrow optimized case where outbound
1472+ * handling is static and host-specific.
14611473 */
14621474 private getHostsToIntercept ( ) : string [ ] {
14631475 const hosts = new Set < string > ( ) ;
@@ -1473,14 +1485,6 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
14731485 hosts . add ( hostname ) ;
14741486 }
14751487
1476- for ( const hostname of this . effectiveAllowedHosts ?? [ ] ) {
1477- hosts . add ( hostname ) ;
1478- }
1479-
1480- for ( const hostname of this . effectiveDeniedHosts ?? [ ] ) {
1481- hosts . add ( hostname ) ;
1482- }
1483-
14841488 return [ ...hosts ] ;
14851489 }
14861490
@@ -1493,51 +1497,17 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
14931497 await this . applyOutboundInterceptionPromise ;
14941498 }
14951499
1496- private async cleanupRemovedHostInterception ( hosts : string [ ] ) : Promise < void > {
1497- if ( hosts . length === 0 ) {
1498- return ;
1499- }
1500-
1501- const ctx = this . ctx as unknown as {
1502- exports ?: { ContainerProxy ?: ( params : { props : { } } ) => Fetcher } ;
1503- } ;
1504- if ( ctx . exports ?. ContainerProxy === undefined ) {
1505- return ;
1506- }
1507-
1508- const outboundConfiguration = this . getOutboundConfiguration ( ) ;
1509- const fetcher = ctx . exports . ContainerProxy ( {
1510- props : {
1511- enableInternet : this . enableInternet ,
1512- containerId : this . ctx . id . toString ( ) ,
1513- className : this . constructor . name ,
1514- outboundByHostOverrides : outboundConfiguration . outboundByHostOverrides ,
1515- outboundHandlerOverride : outboundConfiguration . outboundHandlerOverride ,
1516- allowedHosts : outboundConfiguration . allowedHosts ,
1517- deniedHosts : outboundConfiguration . deniedHosts ,
1518- interceptAll : true ,
1519- } ,
1520- } ) ;
1521-
1522- for ( const host of new Set ( hosts ) ) {
1523- await this . container . interceptOutboundHttp ( host , fetcher ) ;
1524-
1525- if ( this . interceptHttps ) {
1526- await this . container . interceptOutboundHttps ( host , fetcher ) ;
1527- }
1528- }
1529- }
1530-
15311500 /**
15321501 * Applies (or re-applies) outbound HTTP interception with the current
15331502 * default registries + runtime overrides passed through ContainerProxy props.
15341503 *
1535- * Uses `interceptAllOutboundHttp` when a catch-all outbound handler is
1536- * configured. Otherwise, sets up per-host interception for known hosts only,
1537- * avoiding unnecessary overhead for unintercepted traffic.
1504+ * Uses per-host interception only for static host-specific outbound handlers.
1505+ * As soon as the config needs to evaluate all hosts (catch-all outbound,
1506+ * allow/deny lists, or runtime-mutated outbound config), we promote the
1507+ * container to intercept-all and keep it there until the instance restarts.
15381508 *
15391509 * When `interceptHttps` is enabled, also applies HTTPS interception:
1540- * - Catch -all mode: `interceptOutboundHttps('*', ...)` for all HTTPS traffic
1510+ * - Intercept -all mode: `interceptOutboundHttps('*', ...)` for all HTTPS traffic
15411511 * - Per-host mode: `interceptOutboundHttps(host, ...)` for each known host
15421512 */
15431513 private async applyOutboundInterception ( ) : Promise < void > {
@@ -1559,7 +1529,7 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
15591529 const outboundConfiguration = this . getOutboundConfiguration ( ) ;
15601530 this . persistOutboundConfiguration ( outboundConfiguration ) ;
15611531
1562- const needsCatchAll = this . needsCatchAllInterception ( ) ;
1532+ const interceptAll = this . shouldInterceptAllOutbound ( ) ;
15631533 const hosts = this . getHostsToIntercept ( ) ;
15641534
15651535 const fetcher = ctx . exports . ContainerProxy ( {
@@ -1571,13 +1541,24 @@ export class Container<Env = Cloudflare.Env> extends DurableObject<Env> {
15711541 outboundHandlerOverride : outboundConfiguration . outboundHandlerOverride ,
15721542 allowedHosts : outboundConfiguration . allowedHosts ,
15731543 deniedHosts : outboundConfiguration . deniedHosts ,
1574- interceptAll : needsCatchAll ,
1544+ interceptAll,
15751545 } ,
15761546 } ) ;
15771547
1578- if ( needsCatchAll ) {
1579- // Catch -all: intercept all outbound HTTP traffic
1548+ if ( interceptAll ) {
1549+ // Intercept -all: intercept all outbound HTTP traffic
15801550 await this . container . interceptAllOutboundHttp ( fetcher ) ;
1551+ this . hasInterceptAllRegistration = true ;
1552+
1553+ // If we previously installed static per-host interceptors, refresh them
1554+ // with the current fetcher so they follow the latest config too.
1555+ for ( const host of this . getStaticOutboundByHostKeys ( ) ) {
1556+ await this . container . interceptOutboundHttp ( host , fetcher ) ;
1557+
1558+ if ( this . interceptHttps ) {
1559+ await this . container . interceptOutboundHttps ( host , fetcher ) ;
1560+ }
1561+ }
15811562
15821563 // If HTTPS interception is enabled, intercept all HTTPS traffic too
15831564 if ( this . interceptHttps ) {
0 commit comments