Skip to content

Diagnose a failed asset lookup

Reads in this crate fail quietly by design: a missing file, an unreadable file, a rejected path and a layer that was never registered all produce the same None. That makes for a clean overlay and a frustrating half-hour, so start here.

Assets implements Debug and prints its layer labels in priority order, lowest first:

println!("{assets:?}");
// Assets { layers: ["defaults", "system", "user"] }

This answers two questions at once: whether the layer you expected is registered at all, and whether it is where you thought in the order. A surprising number of lookups fail because the answer is "no" to one of those.

If a label looks like a filesystem path — Assets { layers: ["/etc/mytool"] } — the arguments to .directory() are the wrong way round. It takes the root first and the label second, unlike .memory().

open returns None and the file is definitely there

Work down this list; it is roughly ordered by how often each one is the answer.

The path is keyed differently from how you asked. Embedded and in-memory layers compare keys literally. ./config.yaml, /config.yaml and config.yaml are three different keys, and only the last matches an embed of config.yaml. A DirectorySource is more forgiving — it ignores a leading ./ — so a path with ./ on the front can work in development and fail once it falls through to the embedded layer.

The path is a directory. Directories are listable, never readable. exists returns false for them too, even when list_dir on the parent shows the name.

The path contains ... DirectorySource rejects .. anywhere, even where it would not escape the root: sub/../file.txt is refused. So is any absolute path.

The DirectorySource root does not exist. A missing root is not an error — the layer simply contributes nothing, forever. Check the path you passed actually exists, and that it is the directory rather than a file inside it.

The file cannot be read. Permission denied is indistinguishable from absent. If it is readable by you but not by the process, this is the one.

The embedded folder did not include it. If your embed type carries #[include] or #[exclude] attributes, exclude beats include. Iterate E::iter() directly to see what actually got embedded.

The embedded asset was edited but not rebuilt. This crate enables rust-embed's debug-embed, so there is no read-from-disk behaviour in debug builds. The binary holds whatever was there at compile time.

The right file is found but the wrong bytes come back

Byte reads take the last registered layer that has the path, not the first. If the wrong version is winning, a layer you meant to be a fallback is registered after the one you meant to win. Reorder the builder calls; there is no way to reorder after build().

list_dir misses entries, or shows things I cannot read

Listings are unioned across every layer, deduplicated, and byte-order sorted — so uppercase names sort before lowercase ones.

Entries include subdirectory names with nothing to mark them as directories, and exists on a directory name returns false. If you are iterating a listing and reading each name, expect the directories to come back empty and skip them.

Nested paths are not flattened: d/sub/b.txt contributes sub to list_dir("d"). There is no recursive listing.

A merged config has a key missing entirely

Almost always RFC 7396's deletion rule: a higher layer set the key to null, which removes it rather than setting it to null. Search the override files for null, ~, and bare keys with nothing after the colon — key: with an empty value parses as null too.

A merged config came back empty, or failed with "invalid type: null"

A higher layer's file is empty or contains only comments. An empty YAML document parses to null, and a null patch replaces everything below it. Put {} in the file, or delete it.

A merged array lost the defaults

Arrays replace wholesale. A user override listing one plugin replaces the shipped list entirely rather than appending to it. This is RFC 7396 and there is no option to change it — see Merge semantics for the alternative schema shape.

A number came out as null

Check for .inf, -.inf or .nan in the YAML. The YAML-to-JSON conversion turns all three into null with no warning.

A parse error names a file I did not expect

The path in an AssetError::Parse reads cfg.yaml (layeruser) when a single layer failed to parse, and the bare cfg.yaml when the merged document did not fit the target type. The second form means every individual file is well-formed and it is their combination that does not fit — usually a required field deleted by a null, or a value out of range for its type.

A malformed file in any contributing layer aborts the whole load. There is no fallback to the defaults, so a broken user override is loud rather than silent.