{"record":{"id":"e249a03eb8b2b785","repo":"Shopify/liquid","slug":"filter-overrides-registered-public-methods-as-non","errorCode":null,"errorMessage":"Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}","messagePattern":"Filter overrides registered public methods as non public: #(.+?)","errorType":"exception","errorClass":"Liquid::MethodOverrideError","httpStatus":null,"severity":"error","filePath":"lib/liquid/strainer_template.rb","lineNumber":22,"sourceCode":"\nmodule Liquid\n  # StrainerTemplate is the computed class for the filters system.\n  # New filters are mixed into the strainer class which is then instantiated for each liquid template render run.\n  #\n  # The Strainer only allows method calls defined in filters given to it via StrainerFactory.add_global_filter,\n  # Context#add_filters or Template.register_filter\n  class StrainerTemplate\n    def initialize(context)\n      @context = context\n    end\n\n    class << self\n      def add_filter(filter)\n        return if include?(filter)\n\n        invokable_non_public_methods = (filter.private_instance_methods + filter.protected_instance_methods).select { |m| invokable?(m) }\n        if invokable_non_public_methods.any?\n          raise MethodOverrideError, \"Filter overrides registered public methods as non public: #{invokable_non_public_methods.join(', ')}\"\n        end\n\n        include(filter)\n\n        filter_methods.merge(filter.public_instance_methods.map(&:to_s))\n      end\n\n      def invokable?(method)\n        filter_methods.include?(method.to_s)\n      end\n\n      def inherited(subclass)\n        super\n        subclass.instance_variable_set(:@filter_methods, @filter_methods.dup)\n      end\n\n      def filter_method_names\n        filter_methods.map(&:to_s).to_a","sourceCodeStart":4,"sourceCodeEnd":40,"githubUrl":"https://github.com/Shopify/liquid/blob/807d45a6b3d4568e64e86b375e3702df2c7c860c/lib/liquid/strainer_template.rb#L4-L40","documentation":"`StrainerTemplate.add_filter` raises Liquid::MethodOverrideError when a filter module defines a method that collides with an already-registered invokable method but as private/protected instead of public. This protects Liquid's internal invariants: any method invokable from templates must remain public, otherwise filter invocation would break silently.","triggerScenarios":"Registering a filter module with `Liquid::Template.register_filter(MyFilter)` where MyFilter defines a private/protected instance method whose name matches an existing public invokable method (e.g. redefining `size`, `capitalize` or a core filter as private).","commonSituations":"Custom filter modules inheriting from a base class that marks methods private; refactoring a public filter to a private helper without renaming; loading two filter modules where the second narrows visibility of a shared method name; ruby `private` keyword accidentally placed above filter definitions.","solutions":["Rename the private/protected method so it does not collide with registered public methods.","Move the `private` keyword below the invokable filter methods, or make the colliding method public.","Audit custom filter modules with `MyFilter.private_instance_methods & Liquid::StrainerTemplate.methods` before registration.","Split helper logic into a separate non-filter module included only for internal use.","Update the filter registration order or remove duplicate conflicting filter modules."],"exampleFix":"// before\nmodule MyFilter\n  private\n  def truncate(input, n)\n    input[0, n]\n  end\nend\n// after\nmodule MyFilter\n  def truncate(input, n)\n    input[0, n]\n  end\n  private :truncate # only if it is NOT meant to be invokable — otherwise keep public\nend","handlingStrategy":"validation","validationCode":"# before register_filter\nconflicts = (MyFilter.private_instance_methods + MyFilter.protected_instance_methods) & Liquid::StrainerTemplate.instance_methods\nraise \"visibility conflict: #{conflicts}\" unless conflicts.empty?","typeGuard":"def safe_to_register?(filter)\n  (filter.private_instance_methods + filter.protected_instance_methods)\n    .none? { |m| Liquid::StrainerTemplate.method_defined?(m) }\nend","tryCatchPattern":"begin\n  Liquid::Template.register_filter(MyFilter)\nrescue Liquid::MethodOverrideError => e\n  logger.error(\"filter registration failed: #{e.message}\")\nend","preventionTips":["Keep all template-invokable filter methods public","Place `private` keywords carefully in filter modules","Avoid method names colliding with built-in filters (size, capitalize, etc.)","Run a registration smoke test at boot in CI"],"tags":["liquid","filter-registration","method-override","configuration"],"backgroundTag":"invalid-config-value","analyzedSha":"807d45a6b3d4568e64e86b375e3702df2c7c860c","analyzedAt":"2026-09-08T11:31:38.917Z","contentChangedAt":"2026-09-08T11:31:38.917Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}