Skip to content

fix: resolve custom filters before their module is loaded - #205

Merged
edgurgel merged 2 commits into
edgurgel:mainfrom
paper-crow:fix/resolve-filters-before-module-load
Jul 19, 2026
Merged

fix: resolve custom filters before their module is loaded#205
edgurgel merged 2 commits into
edgurgel:mainfrom
paper-crow:fix/resolve-filters-before-module-load

Conversation

@samharnack

Copy link
Copy Markdown
Contributor

A filter whose name maps to a function in a custom filter module can be silently skipped, leaving the input value unchanged.

The problem

Filters are resolved by turning the parsed filter name (a string) into an atom with String.to_existing_atom/1, then applying it. to_existing_atom raises if the atom doesn't already exist, and a module's function-name atoms are only interned once that module is loaded. The BEAM loads modules lazily, so the apply that would have loaded the filter module never runs: to_existing_atom raises first. That gets rescued and treated as "filter not found", which with strict_filters off (the default) passes the value straight through.

It needs a custom filter module (custom_filters: SomeModule, not a function callback) that hasn't been loaded yet. When the module is only referenced as a value and never called directly, nothing loads it, so a valid filter gets dropped depending on whether the module happens to be loaded at that point.

Development issue, not production

A Mix release runs in embedded mode and loads every module at boot, so the atoms all exist up front and a released app doesn't hit this. It shows up during development and when running the suite, where modules load lazily: a full compile loads everything, an incremental run may not.

Why fix it in the library

You can work around it by forcing the module to load (Code.ensure_loaded!(MyFilters) at startup) or by passing custom_filters: as a function callback, which skips the to_existing_atom path. But both are easy to miss, and passing a module of filters shouldn't quietly depend on load order. Worth noting the fix is also not just swapping in String.to_atom/1: that would resolve the name but let untrusted template input intern arbitrary atoms, which is why the code reached for to_existing_atom in the first place.

The fix

Look the filter up in the module's exported functions (module.__info__(:functions)), matching on name and arity, instead of going through to_existing_atom. __info__ loads the module if needed and returns real, already-interned atoms, so resolution no longer depends on the atom existing first, and it still never interns atoms from filter names.

The regression test compiles a filter module in a separate process and adds it to the code path without loading it, reproducing the missing-atom state deterministically. It fails on to_existing_atom and passes with the lookup.

String.to_existing_atom/1 raised when the filter's module had not been
loaded yet, so the filter was treated as missing and (with strict_filters
off) the value passed through unchanged. Look the function up in the
module's exported functions instead, which loads the module if needed and
avoids interning atoms from template input.
@edgurgel

Copy link
Copy Markdown
Owner

Hi there I will check it out later this week! Thanks for the contribution

@samharnack

Copy link
Copy Markdown
Contributor Author

Anytime! let me know if you need me to change anything. Happy to help.

Comment thread lib/solid/standard_filter.ex Outdated
… exports

Replace the per-call Enum.find_value scan over the module's exported
functions with Code.ensure_loaded/1 followed by String.to_existing_atom/1.
Loading the module first interns its function-name atoms, so the O(1)
atom-table lookup no longer raises for a valid filter, while still never
interning arbitrary atoms from untrusted template input.
@edgurgel
edgurgel merged commit 1084861 into edgurgel:main Jul 19, 2026
3 checks passed
@edgurgel

Copy link
Copy Markdown
Owner

Thanks will get a new version out soon!

@samharnack
samharnack deleted the fix/resolve-filters-before-module-load branch July 20, 2026 05:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants