CNA-BUG-122: ContentManager::Load<T> leaks non-XNA exception types: unwrapped Load<SoundEffect> errors and 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 c1c316b9; 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 the SoundEffect specialization lets System::NotSupportedException escape from its loose tier, and a disposed manager throws std::runtime_error instead of ObjectDisposedException.
- Identifier
CNA-BUG-122- Category
- Bug
- Subsystem
- Content & XNB/CNB/CNJ
- Status
- Narrowed (partially fixed; describes only what survives)
- Verified against
- CNA
c1c316b9(c1c316b9c7a846ce8002809c151fcd1af14942c9) - 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
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 both. Since KF-13 (2d85a276f) ContentLoadException derives from System::Exception, so a catch (System::Exception) now covers the SoundEffect case; only the disposed manager's std::runtime_error escapes it.
Fixed since first recorded: a reader that produces another type than requested is a ContentLoadException naming both types (CastReadResult in ContentReader.hpp, KF-9, 0af45cbd4).
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 c1c316b9; not executed. CNA's own XnbContainerFuzzTests.cpp counts std::out_of_range, std::length_error and std::invalid_argument escaping Load<T> on mutated .xnb input as "clean rejections"; those corrupt-input cases are not 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).
Re-verified at b0e97bb1: every any_cast in ContentReader.hpp goes through CastReadResult (KF-9); the SoundEffect specialization still returns reader.Read(...) unwrapped, and Load<T> still throws std::runtime_error("ContentManager has been disposed."). Not executed.
Focused reproduction
Illustrative; not compiled.
ContentManager content(&services, "Content");
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
CustomContentTypeReaderTests.cpp pins the fixed wrong-type case (LoadingAsAnotherTypeThrowsContentLoadExceptionNotBadAnyCast). ContentRuntimeContractTests.cpp checks ObjectDisposedException for ReadAsset<T> only. No test calls Load<SoundEffect> on a missing file or Load<T> after Dispose().
Regression test
Two EXPECT_THROWs: Load<SoundEffect>("missing") with SDL's dummy audio driver expects ContentLoadException; Load<T> after Dispose() expects System::ObjectDisposedException.
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 .xnb wrong-type case, 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