Custom Topic Filters: URL Params Not Reaching `TopicQuery.options`

While building a plugin with custom topic list filters (e.g., by price, location) using URL query parameters. The URL updates correctly (e.g., ...?market_item_statuses=Available), but the parameters aren’t appearing in topic_query.options on the server.

Setup:

  1. Client-Side Param Registration (tecenc-discovery-params.js):

    // tecenc-discovery-params.js
    import { apiInitializer } from "discourse/lib/api";
    
    export default apiInitializer("1.37.3", (api) => {
      const MARKET_PARAMS_KEYS = [
        "market_price_min", "market_price_max", "market_location",
        "market_conditions", "market_warranty", "market_item_statuses"
      ];
      MARKET_PARAMS_KEYS.forEach(paramKey => {
        api.addDiscoveryQueryParam(paramKey, { replace: true, refreshModel: true });
      });
    });
    
  2. Server-Side Whitelisting Attempt (plugin.rb):

    # plugin.rb
    module ::Tecenc
      MARKET_PARAMS_KEYS = [
        :market_price_min, :market_price_max, :market_location,
        :market_conditions, :market_warranty, :market_item_statuses
      ].freeze
    end
    
    # after_initialize do
      if SiteSetting.tecenc_enabled?
        if defined?(::TopicQuery.add_custom_param_handler)
          ::Tecenc::MARKET_PARAMS_KEYS.each do |param_key|
            ::TopicQuery.add_custom_param_handler(param_key) { |value| value } # Simplified for brevity
          end
          Rails.logger.info "[Tecenc] Registered with add_custom_param_handler."
        elsif defined?(::TopicQuery) && ::TopicQuery.methods.include?(:extra_options_whitelist)
          current_whitelist = ::TopicQuery.extra_options_whitelist || []
          new_whitelist = (current_whitelist + ::Tecenc::MARKET_PARAMS_KEYS).uniq
          ::TopicQuery.extra_options_whitelist(*new_whitelist)
          Rails.logger.info "[Tecenc] Extended extra_options_whitelist."
        else
          Rails.logger.warn "[Tecenc] PLUGIN_WARN: Could not find method to whitelist params for TopicQuery."
        end
    
  3. Server-Side Filtering Logic (plugin.rb):

    # plugin.rb (inside after_initialize / if enabled)
    ::TopicQuery.add_custom_filter(:"tecenc_filters") do |topics, topic_query|
      opts = topic_query.options
      Rails.logger.info "[Tecenc_TopicQuery] Opts: #{opts.inspect}" # CRITICAL LOG
    
      # market_params_present = ::Tecenc::MARKET_PARAMS_KEYS.any? { |p| opts[p].present? }
      # if market_params_present
      #   # ... filtering logic using opts[key] ...
      # end
      topics # or modified_topics
    end
    

The Problem (Logs):

  1. Parameter whitelisting fails:

    [Tecenc] PLUGIN_WARN: Could not find a suitable method (add_custom_param_handler or extra_options_whitelist) to whitelist custom params for TopicQuery.
    
  2. opts in TopicQuery.add_custom_filter is missing our custom params:
    When URL is ...?market_item_statuses=Available, log shows:

    [Tecenc_TopicQuery] Opts: {:category=>5, :filter=>"default", :topic_ids=>nil, :category_id=>5}
    

    Our market_item_statuses (and other custom params) are not present.

Our Environment:

Questions:

  1. What’s the current best practice for ensuring custom URL query params reach topic_query.options in recent Discourse versions?
  2. Why might our attempts to use add_custom_param_handler or extra_options_whitelist be failing with the “Could not find a suitable method” warning?
  3. Is there an alternative approach for parameter registration with TopicQuery we should use?

Any help would be much appreciated!

Just noticed that add_custom_param_handler is not even available as a method on TopicQuery. Is there another way to build custom filters for topics in newer discourse versions?

TopicQuery singleton_methods: [:add_custom_filter, :apply_custom_filters, :new_filter, :public_valid_options, :remove_custom_filter, :remove_muted_tags, :results_filter_callbacks, :results_filter_callbacks=, :tracked_filter, :unread_filter, :valid_options, :validate?, :validators, :yaml_tag]

Progress Report so far:

  1. Confirmed that static TopicQuery whitelisting methods like add_custom_param_handler or extra_options_whitelist are not available as class methods in my Discourse version, so those approaches were abandoned.

  2. Implemented a patch for ListController#build_topic_list_options to inject my custom URL parameters (e.g., market_item_statuses, market_price_min) into the opts hash before TopicQuery.new is called.

  3. This part is now working! When I make a request like /c/market/5/l/latest.json?filter=default&market_item_statuses=Available, my server logs confirm the injection:

    [TecencMarket_ListControllerExt_BuildOptsV3] Injected into opts: market_item_statuses = Available
    
    

    So, TopicQuery.new(user, opts) is now receiving opts containing my custom parameters.

Current Sticking Point: 500 Error & Filter Block Not Reached

Despite the parameters now being correctly passed to TopicQuery’s initializer, I still get a 500 Internal Server Error when the request includes these custom market parameters.

To isolate this, I simplified my TopicQuery.add_custom_filter(:"tecenc_market_filters") block to the absolute minimum.

# plugin.rb - Current simplified custom filter block
if ::TopicQuery.respond_to?(:add_custom_filter)
  ::TopicQuery.add_custom_filter(:"tecenc_market_filters") do |topics, topic_query|
    original_topics_relation = topics 
    opts = topic_query.options
    log_prefix_query = "[TecencMarket_SimplifiedFilter_V1.1_Test]" # My debug prefix

    Rails.logger.info "#{log_prefix_query} Opts received by TopicQuery: #{opts.inspect}"

    if opts[:market_item_statuses].present?
      Rails.logger.info "#{log_prefix_query} 'market_item_statuses' IS PRESENT in opts: #{opts[:market_item_statuses]}"
    else
      Rails.logger.info "#{log_prefix_query} 'market_item_statuses' IS NOT PRESENT in opts."
    end
    
    Rails.logger.info "#{log_prefix_query} Returning original topics relation."
    original_topics_relation # Implicit return
  end
  Rails.logger.info "[TecencMarket] Applied SIMPLIFIED custom filter (V1.1_Test)."
end

Observations with this Simplified Filter:

  • Requests without my custom filter parameter (e.g., just /c/market/5/l/latest.json?filter=default):

    • The simplified filter block logs correctly:

      [TecencMarket_SimplifiedFilter_V1.1_Test] Opts received by TopicQuery: {:category=>5, :filter=>"default", ...}
      [TecencMarket_SimplifiedFilter_V1.1_Test] 'market_item_statuses' IS NOT PRESENT in opts.
      [TecencMarket_SimplifiedFilter_V1.1_Test] Returning original topics relation.
      
      
    • The page loads with a 200 OK.

  • Requests with my custom filter parameter (e.g., /c/market/5/l/latest.json?filter=default&market_item_statuses=Available):

    • The ListController patch log appears: [TecencMarket_ListControllerExt_BuildOptsV3] Injected into opts: market_item_statuses = Available

    • A Completed 500 Internal Server Error in ...ms occurs immediately after.

    • Crucially, NO logs from inside the simplified filter block (no [TecencMarket_SimplifiedFilter_V1.1_Test]... lines) appear for this failing request.

This indicates the 500 error happens after TopicQuery is initialized with the opts hash (which now contains my custom parameters), but before or right as Discourse’s core TopicQuery#apply_custom_filters mechanism tries to execute my registered (and now extremely simple) filter block.

I am having difficulty isolating the specific Ruby exception and full backtrace from development.log that immediately precedes the “Completed 500…” line for the failing requests (the log snippets I’ve gathered show the 500 line itself, but not the detailed error message just before it).

Follow-up Question:

Given that:

  1. Custom parameters are now being successfully injected into TopicQuery.options by the ListController patch.

  2. A 500 error occurs when these custom parameters are present in opts.

  3. This 500 error happens before even an extremely simplified custom filter block (that only logs and returns the original relation) gets to execute its first line of code for the request with custom parameters.

  4. This is on Discourse 3.5.0.beta3-dev.

What could be causing TopicQuery itself, or its apply_custom_filters method, to error out before invoking a registered custom filter block, specifically when the options hash contains these plugin-specific keys? Could the previous LocalJumpError we suspected still be relevant at a lower level in how apply_custom_filters handles the iteration or calling of filter blocks, even if my simplified block is just an implicit return?

Any guidance on what to check next in TopicQuery’s behavior with these kinds of custom options, or advice on how to robustly get the full backtrace for the 500 in this scenario, would be immensely helpful.