Bot Challenge Page
This page documents the implementation of bot_challenge_page in GeoDiscovery, including rationale, configuration, and usage details.
Overview
bot_challenge_page provides Turnstile-based bot protection with session awareness. It was chosen to replace Rack::Attack
due to its greater resilience against evasive bot tactics (e.g., IP rotation).
Our deployment uses Cloudflare Turnstile to issue JavaScript-based challenges and track exemption status via session cookies.
Why We Removed Rack::Attack
The previous setup used Rack::Attack to rate-limit suspicious requests. However, this approach proved ineffective against rotating IPs and occasionally blocked legitimate traffic.
Rack::Attack remains in the Gemfile but is not initialized.
Dependencies
# Gemfile
gem "bot_challenge_page", "~> 0.4.0"
gem "rack-attack", "~> 6.7" # present but unused
ApplicationController Integration
The Turnstile challenge is enforced in ApplicationController
via a before_action
:
# app/controllers/application_controller.rb
before_action do |controller|
BotChallengePage::BotChallengePageController.bot_challenge_enforce_filter(controller, immediate: true)
end
This filter checks the session and the exempt logic to determine whether a Turnstile challenge is required.
Configuration
All challenge behavior is configured in:
config/initializers/bot_challenge_page.rb
Key settings include:
config.enabled
— UsesSettings.turnstile.enabled
(disabled in test env)config.cf_turnstile_sitekey
and..._secret_key
— Stored inSettings.turnstile
config.redirect_for_challenge = false
— Render challenge inlineconfig.rate_limited_locations = []
— No rate-limiting paths configured
Session Exemptions
We track exemptions using session state. If a user passes a Turnstile challenge, they are considered exempt for the rest of the session.
Additional exemption logic is defined in the allow_exempt
lambda:
config.allow_exempt = lambda do |controller, _|
exempt =
(controller.is_a?(CatalogController) &&
controller.params[:action].in?(%w[facet]) &&
controller.request.headers["sec-fetch-dest"] == "empty") ||
ip_safelist.map { |cidr| IPAddr.new(cidr) }.any? { |range| range.include?(controller.request.remote_ip) }
Rails.logger.warn "[Turnstile‑EXEMPT] IP: #{controller.request.remote_ip}, Exempt: #{exempt}"
exempt
end
This ensures:
- Search interface components (like facets) are not interrupted
- Known IP ranges (set via
TURNSTILE_IP_SAFELIST
) are exempt
Testing and Local Development
See Cloudflare’s testing guide for keys that always pass, challenge, or fail.
In development
and test
, the feature is typically disabled:
config.enabled = !Rails.env.test? && Settings.turnstile.enabled
Related Links
- Production Turnstile Widget Dashboard
- Uptime Robot status page
- bot_challenge_page GitHub
- Cloudflare Turnstile Docs
- GeoDiscovery GitHub