From 218394b641e7434986f642ded9edb1f47a7d3b43 Mon Sep 17 00:00:00 2001 From: David Levin Date: Mon, 17 Aug 2026 17:02:55 -0700 Subject: [PATCH] Fix: keep WireBox mappings registered when first metadata processing fails WireBox removed a mapping when its first mapping.process() call failed. The first caller received the original error. Later lookups failed with Injector.InstanceNotFoundException until the application was reinitialized. This turned a temporary load error into a lasting outage for explicit binder.map().to() mappings because WireBox could not recreate them. WireBox now keeps the failed mapping registered and unprocessed. The next lookup retries processing. The original caller still receives the original error. Retrying is safe because Mapping.process() marks the mapping as discovered only after processing succeeds. An exclusive lock prevents concurrent processing. The dependency injection methods also skip names that are already registered. Updated both places that removed failed mappings: - Injector.getInstance() - Binder.processMappings() Added four specs to InjectorLiveTest.cfc. The full WireBox test suite passes on Adobe ColdFusion 2023, BoxLang 1.15, and Lucee 5.4. --- system/ioc/Injector.cfc | 13 +--- system/ioc/config/Binder.cfc | 5 +- tests/specs/ioc/InjectorLiveTest.cfc | 108 +++++++++++++++++++++++++++ 3 files changed, 113 insertions(+), 13 deletions(-) diff --git a/system/ioc/Injector.cfc b/system/ioc/Injector.cfc index 54f8145ca..e4b3ad3ce 100644 --- a/system/ioc/Injector.cfc +++ b/system/ioc/Injector.cfc @@ -575,16 +575,9 @@ component serializable="false" accessors="true" { // Check if the mapping has been discovered yet, and if it hasn't it must be autowired enabled in order to process. if ( NOT mapping.isDiscovered() ) { - try { - // process inspection of instance - mapping.process( binder = variables.binder, injector = this ); - } catch ( any e ) { - // Remove bad mapping - var mappings = variables.binder.getMappings(); - mappings.delete( name ); - // rethrow - throw( object = e ); - } + // Read the mapped object's metadata. + // Keep the mapping after an error so the next lookup can try again. + mapping.process( binder = variables.binder, injector = this ); } // Request object from scope now, we now have it from the scope created, initialized and wired diff --git a/system/ioc/config/Binder.cfc b/system/ioc/config/Binder.cfc index 855d4b50c..d21e3e351 100644 --- a/system/ioc/config/Binder.cfc +++ b/system/ioc/config/Binder.cfc @@ -1364,11 +1364,10 @@ component accessors="true" { } ) .each( function( key, thisMapping ){ try { - // process the metadata + // Read the mapped object's metadata. arguments.thisMapping.process( binder = this, injector = variables.injector ); } catch ( any e ) { - // Remove bad mapping - variables.mappings.delete( key ); + // Keep the mapping so the next lookup can try again. mappingError = e; } } ); diff --git a/tests/specs/ioc/InjectorLiveTest.cfc b/tests/specs/ioc/InjectorLiveTest.cfc index 1c44cdc0d..eb3b757f3 100644 --- a/tests/specs/ioc/InjectorLiveTest.cfc +++ b/tests/specs/ioc/InjectorLiveTest.cfc @@ -130,6 +130,114 @@ component extends="tests.resources.BaseIntegrationTest" { } ); } ); } ); + + feature( "Keep mappings after a processing error (COLDBOX-1420)", function(){ + beforeEach( function( currentSpec ){ + variables.injector1420 = new coldbox.system.ioc.Injector(); + variables.ghostPath1420 = expandPath( "/tests/resources/Ghost1420.cfc" ); + } ); + + afterEach( function( currentSpec ){ + if ( fileExists( variables.ghostPath1420 ) ) { + fileDelete( variables.ghostPath1420 ); + } + } ); + + story( "Keep explicit mappings after a processing error", function(){ + given( "a mapping for a component that does not exist", function(){ + then( "each lookup reports a component error and keeps the mapping", function(){ + injector1420 + .getBinder() + .map( "ghost1420@demo" ) + .to( "tests.resources.DoesNotExist1420" ); + + var firstErrorType = "NONE"; + try { + injector1420.getInstance( "ghost1420@demo" ); + } catch ( any e ) { + firstErrorType = e.type; + } + expect( firstErrorType ).notToBe( "NONE", "The first lookup should report an error" ); + expect( firstErrorType ).notToBe( "Injector.InstanceNotFoundException" ); + + // The failed lookup must not remove the mapping. + expect( injector1420.getBinder().mappingExists( "ghost1420@demo" ) ).toBeTrue(); + + // The second lookup must try to read the component again. + var secondErrorType = "NONE"; + try { + injector1420.getInstance( "ghost1420@demo" ); + } catch ( any e ) { + secondErrorType = e.type; + } + expect( secondErrorType ).notToBe( "NONE", "The second lookup should report an error" ); + expect( secondErrorType ).notToBe( "Injector.InstanceNotFoundException" ); + } ); + } ); + + given( "a mapped component added after the first lookup", function(){ + then( "the second lookup creates the instance", function(){ + injector1420 + .getBinder() + .map( "ghostFile1420@demo" ) + .to( "tests.resources.Ghost1420" ); + + // The first lookup fails because the component file does not exist. + var firstErrorType = "NONE"; + try { + injector1420.getInstance( "ghostFile1420@demo" ); + } catch ( any e ) { + firstErrorType = e.type; + } + expect( firstErrorType ).notToBe( "NONE", "The first lookup should report an error" ); + expect( injector1420.getBinder().mappingExists( "ghostFile1420@demo" ) ).toBeTrue(); + + // Add the component file and try the same mapping again. + fileWrite( variables.ghostPath1420, "component {}" ); + var instance = injector1420.getInstance( "ghostFile1420@demo" ); + expect( isObject( instance ) ).toBeTrue(); + } ); + } ); + + given( "one missing component mapped under two names", function(){ + then( "a failed lookup keeps both names", function(){ + injector1420 + .getBinder() + .map( [ "aliasA1420", "aliasB1420" ] ) + .to( "tests.resources.DoesNotExist1420" ); + + try { + injector1420.getInstance( "aliasA1420" ); + } catch ( any e ) { + // The missing component error is expected. + } + + expect( injector1420.getBinder().mappingExists( "aliasA1420" ) ).toBeTrue(); + expect( injector1420.getBinder().mappingExists( "aliasB1420" ) ).toBeTrue(); + } ); + } ); + } ); + + story( "Keep failed mappings during processMappings()", function(){ + given( "a mapping for a component that does not exist", function(){ + then( "processMappings() reports the error and keeps the mapping", function(){ + injector1420 + .getBinder() + .map( "bad1420" ) + .to( "tests.resources.DoesNotExist1420" ); + + var errorType = "NONE"; + try { + injector1420.getBinder().processMappings(); + } catch ( any e ) { + errorType = e.type; + } + expect( errorType ).notToBe( "NONE", "processMappings() should report an error" ); + expect( injector1420.getBinder().mappingExists( "bad1420" ) ).toBeTrue(); + } ); + } ); + } ); + } ); } }