0

I have a button_to tag, with data: {turbo_method: :post}

This works.

When I change it to link_to it breaks with Can't verify CSRF token authenticity.

Is this a bug? It seems that it should work regardless according to the documentation.

In case you are wondering why I care: I am retooling an legacy app to use Turbo Rails / Hotwire, and the style sheets are already set up for this to be an anchor tag. Otherwise it wouldn't be a problem.

----- I was asked for these details -------

            <%= button_to(update_header_admin_settings_printers_path, data: { turbo_method: :post }, 
                        params: @params.merge({hide_filters: @params[:hide_filters]=="true" ? "false" : "true" }),
                        class: "cp-btn cp-btn--small tp-filter") do %>
                <i class="material-icons cp-btn__icon">filter_list</i>
                <span><%= @params[:hide_filters]=="true" ? "Show " : "Hide " %> Button Filters</span>
            <% end %>
            <%= link_to(update_header_admin_settings_printers_path, data: { turbo_method: :post }, 
                        params: @params.merge({hide_filters: @params[:hide_filters]=="true" ? "false" : "true" }),
                        class: "cp-btn cp-btn--small tp-filter") do %>
                <i class="material-icons cp-btn__icon">filter_list</i>
                <span><%= @params[:hide_filters]=="true" ? "Show " : "Hide " %> Link Filters</span>
            <% end %>

Request Header For Button:

accept
text/vnd.turbo-stream.html, text/html, application/xhtml+xml
accept-encoding
gzip, deflate, br, zstd
accept-language
en-US,en;q=0.9
connection
keep-alive
content-length
123
content-type
application/x-www-form-urlencoded;charset=UTF-8
cookie
_quote_editor_session=3ffUr0BL0QvgoKcfZ5c7MupwCX3kQOx9xgTGSEDfoi%2FB7QM3gQpYtSp0I%2FPu1%2Bf8Q19j9vf2KcTJzRExh62RwIoYIGzVWNGNpCQMHQC%2FEO%2BrsxA4HH6Gsj%2BeE5wO3l6guNCI3dIiYsxnQg%2BNHOFQcJImvP45sH4sFBNEO%2FUR3uzdJnGscEnyHXvU591baeh6kwg5nWUqf%2BFM8ImspKlJaZUWxZDsjwvMrjI4GbW5opqNXC6DRpxHc%2FEr%2FY2LsIjobdHE88AuRGKuCqZTutQRgatkS%2BUKAjI%2FZP%2FpKEU%3D--syKk0hnBUMK4fFg2--IMn4hfIICS9NkjONyq0V2g%3D%3D; _catprint_on_rails_session=s3Heh%2BRJtJTyHk8VOSUkYk%2F%2BpQ%2BtzzD6wq%2Fi9GeuDfsoZbSToggOBkg8rGk9zRjR6D6d3uC9rS%2FOdX6G8uy8u21fqQ%2Bg%2FObEtKyTJN1XhKpqpMYSA2FxeEt%2B3P4o0DtclIUPZgmu9MEpL%2FRGrSVn2zwhcq829ND6HO3Z0rbJRGa77DK3PlewLm3GLRLXgoLI21rOALfDxiAM9F0Umrcmoly1UoK%2Bq58rfd3zIOZfg90yebxTm4Bzbfvt1memyv%2FwzdRRiHExSmbQo0dO6gebH7R36KjT7iLs2pBbyHHHSQs7qxkOMgyQrhqTyLctCLOWp0Gmog0wic1oymUy%2FP%2FOB%2B%2Bv3%2BjNsEid2YX5gDrW0dk%2B%2F8Nk9ghPEshpdnwE1WELCoLqourkwfGMx4jOgEpZjX1wkSk34HxzEwRYKha4VbPyS8%2Fb7mT%2FiQz8dc46ZWzhQS8pIVj1jtCGkXi%2FZ68%3D--RpRDmpX%2FQ452aPrM--TmsoFtYmpd05Wh0EVSl94Q%3D%3D
host
localhost:5000
origin
http://localhost:5000
referer
http://localhost:5000/admin/settings/printers
sec-ch-ua
"Google Chrome";v="143", "Chromium";v="143", "Not A(Brand";v="24"
sec-ch-ua-mobile
?0
sec-ch-ua-platform
"macOS"
sec-fetch-dest
empty
sec-fetch-mode
cors
sec-fetch-site
same-origin
turbo-frame
header
user-agent
Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36
x-csrf-token
GvuIphHnU//L9qhzs5RSZpmQGXbYkRFGbckAaKIkBWl6kUe0IeGpJ9H8CqEFxajRaOMC9Ypz3CX80MDYVlUjRA==
x-turbo-request-id
41c1c4d0-7b0c-4813-985d-b88c22646d25

Server Log for Button:

Started POST "/admin/settings/printers/update_header" for ::1 at 2025-11-13 13:36:45 -0500
Processing by Admin::Settings::PrintersController#update_header as TURBO_STREAM
  Parameters: {"authenticity_token"=>"[FILTERED]", "hide_filters"=>"true"}
  Rendered admin/settings/printers/_filters.html.erb (Duration: 0.2ms | GC: 0.0ms)
  Rendered admin/settings/printers/_header.html.erb (Duration: 1.1ms | GC: 0.0ms)
Completed 200 OK in 5ms (Views: 1.4ms | ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms)

Request Header for Link is exactly the same EXCEPT

Content-Length 0

Server Log for Link

Started POST "/admin/settings/printers/update_header" for ::1 at 2025-11-13 13:38:54 -0500
Processing by Admin::Settings::PrintersController#update_header as TURBO_STREAM
Can't verify CSRF token authenticity.
Completed 422 Unprocessable Content in 6ms (ActiveRecord: 0.0ms (0 queries, 0 cached) | GC: 0.0ms)


  
ActionController::InvalidAuthenticityToken (Can't verify CSRF token authenticity.):
  
actionpack (8.1.0) lib/action_controller/metal/request_forgery_protection.rb:321:in `handle_unverified_request'
actionpack (8.1.0) lib/action_controller/metal/request_forgery_protection.rb:415:in `handle_unverified_request'
actionpack (8.1.0) lib/action_controller/metal/request_forgery_protection.rb:404:in `verify_authenticity_token'
activesupport (8.1.0) lib/active_support/callbacks.rb:362:in `block in make_lambda'
activesupport (8.1.0) lib/active_support/callbacks.rb:179:in `block in call'
actionpack (8.1.0) lib/abstract_controller/callbacks.rb:34:in `block (2 levels) in <module:Callbacks>'
activesupport (8.1.0) lib/active_support/callbacks.rb:180:in `call'
activesupport (8.1.0) lib/active_support/callbacks.rb:560:in `block in invoke_before'
activesupport (8.1.0) lib/active_support/callbacks.rb:560:in `each'
activesupport (8.1.0) lib/active_support/callbacks.rb:560:in `invoke_before'
activesupport (8.1.0) lib/active_support/callbacks.rb:119:in `block in run_callbacks'
activesupport (8.1.0) lib/active_support/callbacks.rb:141:in `run_callbacks'
actionpack (8.1.0) lib/abstract_controller/callbacks.rb:260:in `process_action'
actionpack (8.1.0) lib/action_controller/metal/rescue.rb:36:in `process_action'
actionpack (8.1.0) lib/action_controller/metal/instrumentation.rb:76:in `block in process_action'
activesupport (8.1.0) lib/active_support/notifications.rb:210:in `block in instrument'
activesupport (8.1.0) lib/active_support/notifications/instrumenter.rb:58:in `instrument'
activesupport (8.1.0) lib/active_support/notifications.rb:210:in `instrument'
actionpack (8.1.0) lib/action_controller/metal/instrumentation.rb:75:in `process_action'
actionpack (8.1.0) lib/action_controller/metal/params_wrapper.rb:259:in `process_action'
activerecord (8.1.0) lib/active_record/railties/controller_runtime.rb:39:in `process_action'
actionpack (8.1.0) lib/abstract_controller/base.rb:157:in `process'
actionview (8.1.0) lib/action_view/rendering.rb:40:in `process'
actionpack (8.1.0) lib/action_controller/metal.rb:252:in `dispatch'
actionpack (8.1.0) lib/action_controller/metal.rb:335:in `dispatch'
actionpack (8.1.0) lib/action_dispatch/routing/route_set.rb:65:in `dispatch'
actionpack (8.1.0) lib/action_dispatch/routing/route_set.rb:50:in `serve'
actionpack (8.1.0) lib/action_dispatch/journey/router.rb:35:in `block in serve'
actionpack (8.1.0) lib/action_dispatch/journey/router.rb:86:in `block in recognize'
actionpack (8.1.0) lib/action_dispatch/journey/router.rb:66:in `each'
actionpack (8.1.0) lib/action_dispatch/journey/router.rb:66:in `recognize'
actionpack (8.1.0) lib/action_dispatch/journey/router.rb:31:in `serve'
actionpack (8.1.0) lib/action_dispatch/routing/route_set.rb:906:in `call'
railties (8.1.0) lib/rails/engine/lazy_route_set.rb:60:in `call'
rack (3.2.3) lib/rack/tempfile_reaper.rb:20:in `call'
rack (3.2.3) lib/rack/etag.rb:29:in `call'
rack (3.2.3) lib/rack/conditional_get.rb:44:in `call'
rack (3.2.3) lib/rack/head.rb:15:in `call'
actionpack (8.1.0) lib/action_dispatch/http/content_security_policy.rb:38:in `call'
rack-session (2.1.1) lib/rack/session/abstract/id.rb:274:in `context'
rack-session (2.1.1) lib/rack/session/abstract/id.rb:268:in `call'
actionpack (8.1.0) lib/action_dispatch/middleware/cookies.rb:708:in `call'
activerecord (8.1.0) lib/active_record/migration.rb:671:in `call'
actionpack (8.1.0) lib/action_dispatch/middleware/callbacks.rb:31:in `block in call'
activesupport (8.1.0) lib/active_support/callbacks.rb:101:in `run_callbacks'
actionpack (8.1.0) lib/action_dispatch/middleware/callbacks.rb:30:in `call'
actionpack (8.1.0) lib/action_dispatch/middleware/executor.rb:20:in `call'
actionpack (8.1.0) lib/action_dispatch/middleware/actionable_exceptions.rb:18:in `call'
actionpack (8.1.0) lib/action_dispatch/middleware/debug_exceptions.rb:31:in `call'
web-console (4.2.1) lib/web_console/middleware.rb:132:in `call_app'
web-console (4.2.1) lib/web_console/middleware.rb:28:in `block in call'
web-console (4.2.1) lib/web_console/middleware.rb:17:in `catch'
web-console (4.2.1) lib/web_console/middleware.rb:17:in `call'
actionpack (8.1.0) lib/action_dispatch/middleware/show_exceptions.rb:32:in `call'
railties (8.1.0) lib/rails/rack/logger.rb:41:in `call_app'
railties (8.1.0) lib/rails/rack/logger.rb:29:in `call'
propshaft (1.3.1) lib/propshaft/quiet_assets.rb:11:in `call'
actionpack (8.1.0) lib/action_dispatch/middleware/remote_ip.rb:98:in `call'
actionpack (8.1.0) lib/action_dispatch/middleware/request_id.rb:34:in `call'
rack (3.2.3) lib/rack/method_override.rb:28:in `call'
rack (3.2.3) lib/rack/runtime.rb:24:in `call'
actionpack (8.1.0) lib/action_dispatch/middleware/server_timing.rb:61:in `block in call'
actionpack (8.1.0) lib/action_dispatch/middleware/server_timing.rb:26:in `collect_events'
actionpack (8.1.0) lib/action_dispatch/middleware/server_timing.rb:60:in `call'
actionpack (8.1.0) lib/action_dispatch/middleware/executor.rb:20:in `call'
propshaft (1.3.1) lib/propshaft/server.rb:37:in `call'
actionpack (8.1.0) lib/action_dispatch/middleware/static.rb:27:in `call'
rack (3.2.3) lib/rack/sendfile.rb:131:in `call'
actionpack (8.1.0) lib/action_dispatch/middleware/host_authorization.rb:143:in `call'
railties (8.1.0) lib/rails/engine.rb:534:in `call'
puma (7.1.0) lib/puma/configuration.rb:300:in `call'
puma (7.1.0) lib/puma/request.rb:101:in `block in handle_request'
puma (7.1.0) lib/puma/thread_pool.rb:355:in `with_force_shutdown'
puma (7.1.0) lib/puma/request.rb:100:in `handle_request'
puma (7.1.0) lib/puma/server.rb:503:in `process_client'
puma (7.1.0) lib/puma/server.rb:262:in `block in run'
puma (7.1.0) lib/puma/thread_pool.rb:182:in `block in spawn_thread'

2
  • done - see above Commented Nov 13 at 18:44
  • do you have <%= csrf_meta_tags %> in your layout? Commented Nov 14 at 0:08

1 Answer 1

0

link_to does not support the params option.

You will notice that in your logs there are no parameters passed when using link_to which is why this is failing.

The reason this works for button_to is that this method actually creates a form, so the params, including the authenticity token, are embedded in that form and posted.

Most values in html_options are passed through to the button element, but there are a few special options:

...

:params - Hash of parameters to be rendered as hidden fields within the form.

Using the example from the Docs:

<%= button_to "New", new_article_path, params: { time: Time.now  } %>
# => "<form method="post" action="/articles/new" class="button_to">
#      <button type="submit">New</button>
#      <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
#      <input type="hidden" name="time" value="2021-04-08 14:06:09 -0500" autocomplete="off">
#    </form>"

link_to does not have this same functionality.

You can however add query parameters to the link itself which will be treated as parameters on the backend:

<%= link_to(
  update_header_admin_settings_printers_path(
    @params.merge({
      hide_filters: @params[:hide_filters]=="true" ? "false" : "true",
      authenticity_token: form_authenticity_token})
  ), 
  data: { turbo_method: :post }, 
  class: "cp-btn cp-btn--small tp-filter") do %>
     <i class="material-icons cp-btn__icon">filter_list</i>
     <span><%= @params[:hide_filters]=="true" ? "Show " : "Hide " %> Link Filters</span>
<% end %>

With a bare bones setup I show the following in the console logs

Started POST "/posted?authenticity_token=[FILTERED]&hide_filters=true" for XXX.XXX.XXX.XXX at 2025-11-14 20:39:36 +0000
Processing by HomeController#posted as TURBO_STREAM
  Parameters: {"authenticity_token"=>"[FILTERED]", "hide_filters"=>"true"}
Sign up to request clarification or add additional context in comments.

8 Comments

Sorry but it makes no difference if I put the params in the URL or not. I still get the can't verify CSRF token error. This is clearly because (looking at the logs I gave) Rails is not including it the params when it converts. Turbo-Rails is supposed to be able to convert the link_to to a form, which it does, but it is not adding any of the params including the CSRF token.
When I add the params to a link_to as query params they show up as parameters in the logs. Can you please try to utilize the code in my answer and provide the logs showing the parameters do not appear?
sorry I accidentally clicked down arrow. Not sure how to clear it to zero.
Just a note that the doc clearly says that TurboRails intercepts the click on link_to and converts it to a post request. This much is working. It does indeed send a post request. Hence it needs the CSRF token.
Also note the Docs state: "Forms are recommended when performing non-GET requests."
Yes. But this should work correct?
When rails generate form it adds CSRF token in the form, but turbo would (probably) have to read it from HTML meta tag. Can you check if you have it? But, unless you have a hard requirement for it to be <a> tag - I'd just go with <button> that you can just style to look like a link.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.