Forms without redirect

Short question: Is there any way to have a form without a redirect and without triggering this error “Form responses must redirect to another location”?

Long question: In my app I am using this gem https://github.com/jorgemanrubia/turbolinks_render, essentially it replaces the whole body with the html returned by the server. So with this gem form validation errors are non issue. (this is useful for other things too, not only validation) I can even make it work for devise auth controllers without actually editing any devise code. Without this gem, it seems like I would have to add respond_to turbo_stream in every form, which would be a lot of work. I thought I could create a stimulus controller that does a similar thing, since we now have form submission events. But without redirect, we get the error “Form responses must redirect to another location”. Is there any workaround to this?

4 Likes

Stucked with devise controllers too… Didn’t dig too much on turbo yet. May be will find answer myself or will ask a question too ). Anyway if somebody already knows how to make works devise + turbo + flash messages. Give me to know. Thanks

1 Like

I’ve got the same devise issue. Devise uses the responders gem which returns a 200 even when the response is invalid.

From reading the source of Turbo, it only requires a redirect on success, so if devise sent a 422 it would work just fine. However, responders only seems to send 422 for non-HTML formats.

There’s been an issue opened for a while now that I’ve commented on. Perhaps this can be changed now: https://github.com/heartcombo/responders/issues/159#issuecomment-751334253

In the meantime, replacing the create and update methods by overriding the controllers to send 422 should work.

2 Likes

I might be missing something (just started playing around with this Turbo stuff today), but I tried to verify this:

From reading the source of Turbo, it only requires a redirect on success

I have a form, it submits and the server responds with 422 and HTML. I still see the same error that Turbo requires a redirect.

In this example the POST to /applications is the endpoint for the form submission:

I’ve just watched the go rails episode on Hotwire and realised it works differently.

Definitely useful and now I understand how it’s supposed to work.

1 Like

I’m still in the exploration phase of Turbo myself, but I managed to make Devise work with redirects. The problem lies somewhere between Devise & Warden, because they respond with a 401 and Turbo seems to expect either a 200 or a redirect (as mentioned by others).

# config/initializers/devise.rb

...

config.navigational_formats = ['*/*', :html, :turbo_stream]

config.warden do |manager|
  manager.failure_app = CustomFailure
end

...

Then add the Custom Failure App

# lib/custom_failure.rb

class CustomFailure < Devise::FailureApp
  def redirect_url
    request.referrer
  end

  def respond
    if http_auth?
      http_auth
    else
      redirect
    end
  end
end

Also don’t forget to add lib into the autoload_paths (in config/application.rb) if you haven’t already so the custom FailureApp is available.

Now Devise/Warden will redirect on failure and Turbo will replace the body. I would think that following the “Turbo” way of doing things for everything else would be best, this is just one workaround for Devise integration (seems like other workarounds need to be in place for other routes as well).

2 Likes

Hmm, I still get some wonkiness with this method, but it feels closer.

If I make the form element wrapped in a turbo frame, it seems to render form errors correctly on forms like sign up or forgot password, but it won’t redirect on successful submit. If I add target: '_top', it will redirect correctly, but not render the form errors.

Is there some way to get both to function?

3 Likes

I have exactly the same question. It looks like the data-turbo-frame tag can somehow help here?

1 Like

I’ve just got error messages rendering in the right place. I subclassed the Devise::RegistrationsController as per normal, having added :turbo_stream to the navigational_formats (thanks @fdoxyz) to the initializer, and overwrote the entire create method with (note the turbostream response format):

  def create
    build_resource(sign_up_params)

    resource.save
    yield resource if block_given?
    if resource.persisted?
      if resource.active_for_authentication?
        set_flash_message! :notice, :signed_up
        sign_up(resource_name, resource)
        respond_with resource, location: after_sign_up_path_for(resource)
      else
        set_flash_message! :notice, :"signed_up_but_#{resource.inactive_message}"
        expire_data_after_sign_in!
        respond_with resource, location: after_inactive_sign_up_path_for(resource)
      end
    else
      clean_up_passwords resource
      set_minimum_password_length
      respond_to do |format|
        format.turbo_stream do
          render turbo_stream: turbo_stream.replace(resource, partial: 'users/registrations/new_form',
                                                              locals: { resource: resource })
        end
        format.html
      end
    end
  end

I then moved the form into a _new_form.html.erb partial, and changed the new.html.erb file to:

<h2>Sign up</h2>
<%= turbo_frame_tag "#{resource_name}_registration_form" do %>
  <%= render 'new_form', resource: resource, resource_name: resource_name %>
<% end %>
<%= render "users/shared/links" %>

That’s resulted in having just the form be replaced, so I’m happy. I’ve not been able to replicate the success that @fdoxyz had with the custom failure app.

1 Like

Oh. Very hackish :frowning: Does anyone know is there an issue in Devise about this problem? Could somebody create it if not? I would do it but English is not my first language. And there are maybe miscommunication :slight_smile:

They recently merged the fact that 4XX or 5XX form responses will be displayed without the need for a redirect.

Not sure what status code devise returns but it should be easily overloadable if necessary

I’ve got everything working with the latest Turbo and recorded a screencast on how to set it up. Nice part is we don’t have to change any Devise controllers or views. :+1:

Code for it:

# config/initializers/devise.rb
# frozen_string_literal: true

class TurboFailureApp < Devise::FailureApp
  def respond
    if request_format == :turbo_stream
      redirect
    else
      super
    end
  end

  def skip_format?
    %w(html turbo_stream */*).include? request_format.to_s
  end
end

class TurboController < ApplicationController
  class Responder < ActionController::Responder
    def to_turbo_stream
      controller.render(options.merge(formats: :html))
    rescue ActionView::MissingTemplate => error
      if get?
        raise error
      elsif has_errors? && default_action
        render rendering_options.merge(formats: :html, status: :unprocessable_entity)
      else
        redirect_to navigation_location
      end
    end
  end

  self.responder = Responder
  respond_to :html, :turbo_stream
end

Devise.setup do |config|

  # ==> Controller configuration
  # Configure the parent class to the devise controllers.
  config.parent_controller = 'TurboController'

  # ==> Warden configuration
  config.warden do |manager|
    manager.failure_app = TurboFailureApp
  end
end
5 Likes

@excid3 @woto just done some more playing around and removing my controller method override and just including the turbo_frame_tag call in the view, into which goes the form seems to have done the trick.

@excid3 I like your approach too - we now have a choice as to which suits best :slight_smile:

1 Like

I didn’t want to have to modify any views or controllers, so this route worked pretty well. My original solution was to add turbo_stream.erb templates. Always good to have a few options. :+1:

Working on this issue adding a helped, then I just need to disable caching I think (because my auth controller is redirecting to the same page on error). I’ll write what happens when I get it working.

Beta 2 is out, addressing the issue of 4xx and 5xx HTML error responses.

I’ve fixed it with a combination of @excid3 and data: {turbo: false}:
my inititalizer

BTW @excid3 you’re missing
config.navigational_formats = ["*/*", :html, :turbo_stream]
change in the Code block. You do it in the video, just not in the text.

Lol, yeah this might fix my problem entirely, but I fixed it using turob streams which allowed me to set a response that would replace the form with the errors (non websockets, just HTTP).

I ended up with this code based on @excid3 example:

class TurboFailureApp < Devise::FailureApp
  def respond
    if request_format == :turbo_stream
      recall
      # turbo wants a 422 https://turbo.hotwire.dev/handbook/drive#redirecting-after-a-form-submission
      response[0] = 422
    else
      super
    end
  end

  def skip_format?
    %w[html turbo_stream */*].include? request_format.to_s
  end
end

Didn’t need to do any of the parent controller stuff. Has been working well in production with our turbo-ios/turbo-android apps too.

Okay I lied a bit. That code was enough to get a login form (SessionsController) working with native Turbo.

For other forms (eg. RegistrationsController) I’d suggest using Respond with 422 Unprocessable Entity for non-GET HTML/JS requests with errors by carlosantoniodasilva · Pull Request #223 · heartcombo/responders · GitHub over adding a new TurboController.

Hey all, this is Carlos, developer maintaining devise/responders (and the heartcombo libs). I appreciate you all get things working here with Hotwire, I haven’t been able to follow along on the first few weeks but I’d be happy to get things working more out of the box with responders first, & devise second in the coming weeks.

@ghiculescu thanks for linking to that PR! Please let me know if you (or anyone else really) are able to test it out and if you bump into any issues, before I move on with it and release a new version in the upcoming days. It’s a breaking change on responders so it will likely result in a major bump.

That should also help make Devise work mostly out of the box as well with Hotwire, at least for most of its form interactions, I think. I’ll have to circle back on it and see what’s necessary to change for turbo streams, like the changes that @excid3 showed above (thanks for that!). To be honest I haven’t been able to test things out with Hotwire myself yet, so it’s been helpful to read through your conversation here, and any help you can provide with testing is appreciated.

Thanks all.

3 Likes