CNA-BUG-122: ContentManager::Load<T> leaks non-XNA exception types: std::bad_any_cast for a wrong .xnb type, unwrapped Load<SoundEffect> errors, std::runtime_error when disposed
Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.
ContentManager::Load wraps loose-reader failures in ContentLoadException, but its .xnb tier lets std::bad_any_cast escape, the SoundEffect specialization wraps nothing, and a disposed manager throws std::runtime_error.
- Identifier
CNA-BUG-122- Category
- Bug
- Subsystem
- Content & XNB/CNB/CNJ
- Status
- Open
- Verified against
- CNA
009d40f5(009d40f5dd085c4e674d3479675fac84b12b3e0a) - Severity
- Medium (a triage suggestion, not a project priority)
- Evidence basis
- Source-verified: read at TARGET, not executed
- Tests touching this area
- Yes: see Current tests
- Affected contract
- ContentManager::Load (documented to throw ContentLoadException) and its SoundEffect specialization; XNA ContentManager.Load (ObjectDisposedException, ContentLoadException including BadXnbWrongType)
Expected behaviour
XNA 4.0 throws ObjectDisposedException from Load<T> on a disposed manager, a ContentLoadException (BadXnbWrongType, raised in ContentReader.InvokeReader) when the asset's root object is not a T, and a ContentLoadException for open and read failures. CNA documents Load<T> as throwing ContentLoadException when the asset cannot be loaded, its .cnb tier converts a wrong type into a ContentLoadException naming both types, its loose tier wraps every std::exception, and ReadAsset<T> throws System::ObjectDisposedException.
Actual behaviour at TARGET
- Wrong type on the
.xnbtier.ContentReader::InnerReadObject<T>(ContentReader.hpp) doesstd::any_cast<T>on the root reader's result and nothing between it andLoad<T>catches, soLoad<Texture2D>of a sound.xnbthrowsstd::bad_any_cast. Load<SoundEffect>loose tier. The explicit specialization (ContentManager.cpp) returnsreader.Read(resolvedPath, *this)without the generic template'stry/catch. A missing or undecodable.wavin a sound-enabled build surfaces asSystem::NotSupportedException("Failed to load sound: …") fromSoundEffect::SoundEffect(const std::string&), which derives fromSystem::Exception, not fromContentLoadExceptionorstd::runtime_error.- Disposed manager.
Load<T>, theSoundEffectspecialization andLoadUntypedXnbReferencethrowstd::runtime_error("ContentManager has been disposed.").
XNA-style handlers such as catch (const ContentLoadException&) around an optional or localized asset therefore miss all three.
Source locations
modules/content/include/Microsoft/Xna/Framework/Content/ContentReader.hpp— ContentReader::InnerReadObject - unguarded std::any_cast on the root objectmodules/content/include/Microsoft/Xna/Framework/Content/ContentManager.hpp— ContentManager::Load - disposed check, XNB tier without a wrapper, loose tier with onemodules/content/src/Xna/ContentManager.cpp— ContentManager::Load specialization for Audio::SoundEffect - loose tier without a wrappermodules/audio/src/Xna/SoundEffect.cpp— SoundEffect file-name constructor - throws System::NotSupportedException
Evidence
Checked by reading at 009d40f5; not executed. CNA's own XnbContainerFuzzTests.cpp counts std::bad_any_cast, std::out_of_range, std::length_error and std::invalid_argument escaping Load<T> on mutated .xnb input as "clean rejections", which confirms that the .xnb tier does not normalize exceptions at all; only the wrong-type case is claimed here as a contract violation. XNA behaviour taken from the decompiled XNA 4.0 ContentManager.Load and ContentReader.InvokeReader. The site already warns about each leak (XNB guide, Tutorial 147, content-runtime internals).
Focused reproduction
Illustrative; not compiled.
ContentManager content(&services, "Content"); // Content/click.xnb holds a SoundEffect
try { auto t = content.Load<Graphics::Texture2D>("click"); }
catch (const ContentLoadException&) { /* not reached: std::bad_any_cast */ }
try { auto s = content.Load<Audio::SoundEffect>("missing"); } // no missing.* anywhere
catch (const ContentLoadException&) { /* not reached: System::NotSupportedException */ }
content.Dispose();
try { auto t = content.Load<Graphics::Texture2D>("x"); }
catch (const System::ObjectDisposedException&) { /* not reached: std::runtime_error */ }
Current tests
CnjAssetCacheTypeSafetyTests.cpp covers the cache key and the loose tier; ContentManagerTexture2DXnbTests.cpp pins loose-tier wrapping for Texture2D; ContentRuntimeContractTests.cpp checks ObjectDisposedException for ReadAsset<T> only. No test calls Load<T> with the wrong type for an .xnb, Load<SoundEffect> on a missing file, or Load<T> after Dispose().
Regression test
Three EXPECT_THROWs: Load<Texture2D> of tests/assets/xnb/monogame/windows/uncompressed/audio/tone_mono_44khz_16bit.xnb expects ContentLoadException; Load<SoundEffect>("missing") with SDL's dummy audio driver expects ContentLoadException; Load<T> after Dispose() expects System::ObjectDisposedException. Then narrow the accepted list in XnbContainerFuzzTests.
Blast radius
Callers that rely on ContentLoadException for fallback (optional content, localized-asset probing, mod loaders) and code ported from XNA that catches ObjectDisposedException. The .cnb tier and the generic loose tier already conform. A build without SOUND_ENABLED does not throw from SoundEffect's file constructor at all.
Workaround
Catch std::exception around Load<T> and treat any failure as a load failure.
Related pages
The same subject is explained at several altitudes. These are the neighbouring pages at each one.
- User guide
- XNB guide: diagnosing XNB problems
- Internals
- Content runtime: inconsistencies
- Maintainer workflow
- Modify ContentManager: error contract
- Known issues
- Bug index