turbo-rails: Turbo stream partial with button_to tag throws ActionView::Template::Error

after_create_commit with broadcast_append_to throws this error: ActionView::Template::Error (Request forgery protection requires a working session store but your application has sessions disabled. You need to either disable request forgery protection, or configure a working session store.) (seems to happen to all broadcasts with partials)

Log trace:

10:22:02 web.1  | ActionView::Template::Error (Request forgery protection requires a working session store but your application has sessions disabled. You need to either disable request forgery protection, or configure a working session store.):
10:22:02 web.1  |     13:     <div class="flex">
10:22:02 web.1  |     14: 
10:22:02 web.1  |     15:       <div class="up">
10:22:02 web.1  |     16:         <%= button_to increment_question_group_path(question_group), method: :patch do %>
10:22:02 web.1  |     17:           <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-gray-500 font-light" viewBox="0 0 20 20" fill="currentColor">
10:22:02 web.1  |     18:             <path fill-rule="evenodd" d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L11 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z" clip-rule="evenodd" />
10:22:02 web.1  |     19:           </svg>
10:22:02 web.1  |   
10:22:02 web.1  | app/views/question_groups/_question_group.html.erb:16
10:22:02 web.1  | app/models/question_group.rb:16:in `append_to_list'

Rails 7 alpha2, turbo-rails 0.7.14

About this issue

  • Original URL
  • State: closed
  • Created 3 years ago
  • Reactions: 4
  • Comments: 46 (8 by maintainers)

Commits related to this issue

Most upvoted comments

I believe this needs to be reopened.

Rails 7 alpha raises an error when sessions are disabled, which appears to break model broadcasts.

A minimal application reproducing the error can be found here: https://github.com/DavidColby/turbo_rails_7_bug

This app includes an after_create_commit broadcast:

class Post < ApplicationRecord
  after_create_commit -> { broadcast_prepend_to 'posts' }
end

And it includes a button_to in the post partial:

<%= button_to "Destroy this post", post_path(post), method: :delete %>

Note that a form_with will fail too, rendering any form appears to cause the error.

To reproduce on this app, boot up the app, head to /posts/new and see that ActionController::RequestForgeryProtection::DisabledSessionError in Posts#create is raised when you attempt to submit the form.

Uncomment this line and reboot the app and see that post submission works as expected.

The same is happening with rich_text_field tags used in forms (just to be clear, the form needs to appear in the Turbo broadcast). I found this error in our application to be raised after the changes introduced in rails/rails #38957: Your application has sessions disabled. To write to the session you must first configure a session store.

after_create_commit with broadcast_append_later_to enqueues a Turbo::Streams::ActionBroadcastJob for the prepend. The newest changes for the rich_text_area_tag helper now checks for a Rails session. The problem with that is a background job won’t have a rails session. In our broadcast_append_later_to we send a partial that includes a form and a rich text field.

https://github.com/DmitryTsepelev/rails/blob/193289dbbe146c56ec16faf8dd1a2c88611feb83/actiontext/app/helpers/action_text/tag_helper.rb#L37

Here is an example of the stack trace, Screen Shot 2021-12-20 at 1 51 08 PM

Solutions for getting the ‘authenticity_token’ when a form is rendered via a broadcast from @aantix [1] and @mhenrixon [2] or [2] may work (I’m using the @aantix one, as of now) but IMHO the issue should be handled at a global level.

Hello, just another update on the problem @aantix seems the only solution through turbo atm when triggering the broadcast from a worker. But in the case of displaying a collection that relies on this broadcast you are then forced to setup an n+x situation. So it can be a solution, but you are quickly forced to use another system whenever the n+x is too much of trouble when you can’t do without authentication.

@tbcooney @AhmedNadar Broadcast from your background job a lazy-loaded turbo frame.

That lazy-loaded frame will then in turn load your form with a second request, using the current user’s session.

E.g.

# notes/app/jobs/bookmark_to_note_job.rb
class BookmarkToNoteJob < ApplicationJob
  queue_as :default

  def perform(bookmark)
    ....
    broadcast_prepend_to("#{bookmark.notebook.id}_notes",
                         target: 'notes',
                         partial: 'notes/note_frame',
                         object: bookmark.note,
                         as: :note, locals: { note_active: true })

  end
end
<!-- notes/app/views/notes/_note_frame.html.erb -->
<%= turbo_frame_tag note, src: edit_note_path(note), loading: :lazy do %>
  <!-- The notes/edit form will render here -->
<% end %>
<!-- notes/app/views/notes/edit.html.erb -->
<turbo-frame id="<%= dom_id(@note) %>">
  <%= form_with(model: @note) do |form| %>
    ....
    <%= form.rich_text_area :details %>
  <% end %>
</turbo-frame>

My solution was a little different:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    let form = this.element

    if (this.element.type === "submit" || this.element.type === "button") {
      form = this.element.closest("form")
    }

    if (form.querySelector("input[name='authenticity_token']") == null) {
      form.appendChild(this.authenticityToken)
    }
  }

  get authenticityToken() {
    const input = document.createElement("input")

    input.type = "hidden"
    input.name = "authenticity_token"
    input.autocomplete = "off"
    input.value = window.mrujs.csrfToken()

    return input
  }
}

I use mrujs to set an authenticity token dynamically based on the one on the top level.

Are you sure you have a working session store, as the error mentions? Can you create a boiled down app that replicates this? Not seeing it on my end.

Stumbled upon this today. rails (7.0.4) turbo-rails (1.3.2)

a button_to is used in a partial that is broadcasted. Clicking on it results in “ActionController::InvalidAuthenticityToken (Can’t verify CSRF token authenticity.)”

It just seems surprising that it does not work out of the box

When I was using rails v-7.0.0.alpha2 with turbo-rails v-0.9.0 I solved it with form_with

<%= form_with(model: post, method: :delete, authenticity_token: false) do |form| %>
  <%= form.button :submit, class: "..." do %>
    <svg ...>
    </svg>
  <% end %>
<% end %>

But now, using rails v-7.0.0.rc3 with turbo-rails v-1.0.0 that it is working with button_to and method: :delete

<%= button_to post_path(post), method: :delete,  class: "..." do  %>
   <svg ...>
   </svg>
<% end %>

Then when the form is submitted without a token, the token from the <head> gets used instead?

That’s what I’d expect yes.

If that’s the case we’d need to let Action View know that it shouldn’t generate a CSRF token in that form.

It looks like this already happens. Using the example above, when a form is rendered via a broadcast the HTML looks like this:

<form class="button_to" method="post" action="/posts/33">
  <input type="hidden" name="_method" value="delete">
  <button data-confirm="Are you sure?" type="submit">Destroy this post</button>
</form>

The same form rendered during a normal page turn looks like this:

<form class="button_to" method="post" action="/posts/33">
  <input type="hidden" name="_method" value="delete">
  <button data-confirm="Are you sure?" type="submit">Destroy this post</button>
  <input type="hidden" name="authenticity_token" value="somesecuretoken">
</form>

I suppose this means there must be some path that bypasses token generation when the session is unwriteable. Then when the form is submitted without a token, the token from the <head> gets used instead?

I was able to address this be changing from button_to to link_to since link_to will get a CSRF token on the fly.

Looking at this further, it may be more appropriate to raise this issue against Rails since the change to default to raising errors when the session is disabled causes rendering outside of the controller context to fail and turbo-rails isn’t the only library that expects to be able to safely render partials from models, background jobs, etc.

I’ll leave this here for now and see what others think. IMO, a default in Rails 7 that explicitly breaks rendering from models seems like an incorrect default given the reliance on this pattern in a number of libraries.