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:
-
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 });
});
});
-
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
-
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):
-
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.
-
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:
- What’s the current best practice for ensuring custom URL query params reach
topic_query.options
in recent Discourse versions?
- 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?
- 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:
-
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.
-
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.
-
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:
-
Custom parameters are now being successfully injected into TopicQuery.options
by the ListController
patch.
-
A 500 error occurs when these custom parameters are present in opts
.
-
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.
-
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.