Four endpoints accept unauthenticated POSTs and create records / send email. None are throttled — the only defence is a hidden honeypot field, which stops naive bots but nothing deliberate.
Endpoints
| Endpoint |
Controller |
Creates |
POST /f/:slug |
PublicFormsController#create |
Person, FormSubmission, 2 emails |
POST /events/:event_id/public_registrations |
Events::PublicRegistrationsController#create |
Person, EventRegistration, FormSubmission, emails |
POST /events/:event_id/bulk_payment_form_submissions |
Events::BulkPaymentFormSubmissionsController#create |
Person, FormSubmission, emails |
POST /contact_us |
ContactUsController#create |
email |
/f/:slug is the most exposed: it is always open, whereas the event endpoints are gated behind ensure_registerable.
Approach
Rails 8.1 ships ActionController::RateLimiting, so no new gem:
rate_limit to: 5, within: 1.minute, only: :create,
with: -> { redirect_to public_form_path(@form.slug), alert: "Too many submissions — please wait a minute." }
Keys on request.remote_ip by default and scopes per controller.
Storage
Uses config.cache_store, which is already suitable in both real environments:
- production —
:solid_cache_store (DB-backed, so the counter is shared across processes)
- development —
:memory_store
Decided: no spec for the rate limit
Test env is :null_store, so increment no-ops and a limit never trips — it can't be spec'd without first changing the test cache store (stub Rails.cache, pass an explicit store:, or switch the env to :memory_store).
We are deliberately not doing that. Writing the spec means solving the cache-store question and adds roughly 15 minutes to every suite run for a one-line, framework-provided guard. Not worth it. Ship the rate_limit calls unspec'd and verify by hand in development (where :memory_store makes the limit live).
Revisit only if a future limit carries real logic in its by: or with: lambda.
Notes
Four endpoints accept unauthenticated POSTs and create records / send email. None are throttled — the only defence is a hidden honeypot field, which stops naive bots but nothing deliberate.
Endpoints
POST /f/:slugPublicFormsController#createPerson,FormSubmission, 2 emailsPOST /events/:event_id/public_registrationsEvents::PublicRegistrationsController#createPerson,EventRegistration,FormSubmission, emailsPOST /events/:event_id/bulk_payment_form_submissionsEvents::BulkPaymentFormSubmissionsController#createPerson,FormSubmission, emailsPOST /contact_usContactUsController#create/f/:slugis the most exposed: it is always open, whereas the event endpoints are gated behindensure_registerable.Approach
Rails 8.1 ships
ActionController::RateLimiting, so no new gem:Keys on
request.remote_ipby default and scopes per controller.Storage
Uses
config.cache_store, which is already suitable in both real environments::solid_cache_store(DB-backed, so the counter is shared across processes):memory_storeDecided: no spec for the rate limit
Test env is
:null_store, soincrementno-ops and a limit never trips — it can't be spec'd without first changing the test cache store (stubRails.cache, pass an explicitstore:, or switch the env to:memory_store).We are deliberately not doing that. Writing the spec means solving the cache-store question and adds roughly 15 minutes to every suite run for a one-line, framework-provided guard. Not worth it. Ship the
rate_limitcalls unspec'd and verify by hand in development (where:memory_storemakes the limit live).Revisit only if a future limit carries real logic in its
by:orwith:lambda.Notes
request.remote_ipresolves correctly behind the production proxy before trusting per-IP keys.