CNA-BUG-122: ContentManager::Load<T> leaks non-XNA exception types: unwrapped Load<SoundEffect> errors and std::runtime_error when disposed

CNA snapshot c1c316b9  ·  Known Issues › Current bugs  ·  source links pinned to c1c316b9

✓

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) returns reader.Read(resolvedPath, *this) without the generic template's try/catch. A missing or undecodable .wav in a sound-enabled build surfaces as System::NotSupportedException ("Failed to load sound: …") from SoundEffect::SoundEffect(const std::string&), which derives from System::Exception, not from ContentLoadException or std::runtime_error.
  • Disposed manager. Load<T>, the SoundEffect specialization and LoadUntypedXnbReference throw std::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

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.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Known issues
Bug index