fix: resolve custom filters before their module is loaded - #205
Merged
edgurgel merged 2 commits intoJul 19, 2026
Merged
Conversation
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.
Owner
|
Hi there I will check it out later this week! Thanks for the contribution |
Contributor
Author
|
Anytime! let me know if you need me to change anything. Happy to help. |
edgurgel
reviewed
Jul 3, 2026
… 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
approved these changes
Jul 19, 2026
Owner
|
Thanks will get a new version out soon! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_atomraises 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 theapplythat would have loaded the filter module never runs:to_existing_atomraises first. That gets rescued and treated as "filter not found", which withstrict_filtersoff (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 passingcustom_filters:as a function callback, which skips theto_existing_atompath. 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 inString.to_atom/1: that would resolve the name but let untrusted template input intern arbitrary atoms, which is why the code reached forto_existing_atomin 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 throughto_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_atomand passes with the lookup.