turbo-rails: Turbo new install - forms do not redirect
I just installed Turbo in an existing Rails 6 app and all my existing forms are broken. They submit as expected but the redirect after doesn’t happen. I’m able to interact with Turbo as expected - ie, I have a frame loading correctly, so it appears I loaded Turbo correctly in Webpack.
import "@hotwired/turbo-rails"
And with just a very simple form (using slim and simple_form):
= simple_form_for @comment do |f|
= f.input :text, label: "Comment"
= f.button :submit
My controller performs the redirect (using responders):
class CommentsController < ApplicationController
load_and_authorize_resource
def index
@comment = Comment.new
end
def create
@comment.save
respond_with @comment, location: comments_path
end
private
def resource_params
params.require(:comment).permit(
:text
)
end
end
The comment gets created and the request for the redirect URL happens but the redirect does not. I have to refresh to see the changes.
About this issue
- Original URL
- State: open
- Created 3 years ago
- Reactions: 15
- Comments: 76 (13 by maintainers)
Commits related to this issue
- reproduction for https://github.com/hotwired/turbo-rails/issues/122 — committed to jasonfb/TR001 by jasonfb 3 years ago
Hello everyone,
I opened #152 earlier, but I think this issue is the same. For me it is completely not correct that Turbo prevents plain HTML forms from working with this gem:
<form action="/url" method="POST">...</form>It is old good HTML 1.0 and it gets broken unless you wrap it into
<turbo-frame>and/or make explicitturbo_streamresponse! For me it doesn’t matter what status code Rails return (though correct codes are a good practice), it is just broken HTML standard. Thus Turbo doesn’t do progressive enhancement, it forces you into yet another ecosystem, like React, Angular and other vendor-lock tools.This is not correct. Forms must work the same way as links. We don’t need to opt-out from turbo on every link, so we shouldn’t do it for every form. Links are smart enough to use whole page as response when not wrapped in
turbo_frameand so should forms.I suggest the following logic:
We will get everything working for all existing and all non-Rails code out of the box. It will respect old good HTML basics. And it will be progressive enhancement, not a mandatory Turbo lock-in. It will be effortless magic!
I think it is a more high-level problem of current implementation which will solve original redirect issue, too. It looks more server-side.
Sorry if I’m missing some big idea or technical restriction, but this part is confusing.
To add to what @dmitry-rychkov said, this would also improve the Turbolinks-to-Turbo upgrade path quite a lot.
Most of it was straightforward, but having form submissions suddenly doing nothing (even without
remote: trueand even withstatus: 303) was quite a head-scratcher. The docs also seem to say that you can progressively use just Turbo Drive and later opt into more fanciness with Frames/Streams, but reality currently seems to be that you can’t use just Turbo Drive on its own for form submissions.I got rid of Rails UJS since I was told it would conflict with Turbo and wrapped the form in a
turbo_frame_tag. Displaying form errors works but the browser following the redirect isn’t. The request format is turbostream and the response is in aformat.htmlblock. Like everyone else says, the browser is left hanging on the old page with the submitted form.The really confusing thing to me is that the server renders the redirected page, which means the browser made the request for the redirect but didn’t render it. I setup a bunch of logging on all the Turbo events and found that there’s no render event. Investigating
event.detail.fetchResponse.responseforturbo:submit-endit seems to be perfectly aware that the client should redirect, it just doesn’t.I wrote this SO question and got some insight. Changing
format.html { redirect_to whatever_path }toformat.html { redirect_to whatever_path(format: :html) }does actually change the behaviour despite the common format block. It went from hanging on the previous page with the submitted form to replacing the contents of theturbo_framewith nothing and the console complained about:Response has no matching <turbo-frame id="new_label"> element. I guess it’s passing the format along as turbostream to the redirect despite entering the format.html block. The fallback to handling turbostream requests with html responses seems to put us in a weird place when upgrading.The handbook says:
That just doesn’t seem to be happening. It’s following the redirect, making the request with the turbostream content_type, then not rendering the result.
The confirmed post also shows that this is the convention rails expects.
TL;DR is at the bototm of this post.
@kiddrew yes! Happy to explain. @archonic I agree. This stuff is super hard to debug too because of the massive amounts of indirection in turbo and rails.
Media type registration
So in the new turbo you will find this piece of code:
https://github.com/hotwired/turbo-rails/blob/102e919318ec528b9f5908e10d6427847d7a197b/lib/turbo/engine.rb#L50-L52
This ensures you can call
https://example.org/your/endpoint.turbo_streamto forgo content-negotiation, in the same way:htmlis registered to facilitatehttps://example.org/your/endpoint.html. It also allows you to write code like this:When is the
turbo_streamblock executed? It’s executed when rails understands that it “should”..turbo_stream). This is not content negotiation.Acceptheader contains thetext/vnd.turbo-stream.htmlmedia type and it is more important than anything else acceptable. In the example case, because of.anyany valid media type is acceptable.When a block is executed, it will by default set the content-type to whatever format was accepted. So, if the
turbo_streamis executed, theContent-Typeis set totext/vnd.turbo-stream.htmlif some code requests it.Renderer creation
Often, you will not have any
respond_toblock in your code, but rather you’ll do something like:This internally uses a Rails Renderer instead. So for example, if you want to be able to write
render csv: my_csv_objand that object be turned into csv content by callingto_csvon it, you could do something like this:For turbo, the code is:
https://github.com/hotwired/turbo-rails/blob/102e919318ec528b9f5908e10d6427847d7a197b/lib/turbo/engine.rb#L55-L60
What this shows is: return the content to render verbatim, which means you need to do
render turbo_stream: valid_html. It will, however, set theContent-Typetotext/vnd.turbo-stream.htmlunless it’s already set.Default render
These two things together may help you understand what happens when you do not have
respond_toblocks and do not callrender turbo_stream: ....but rather have the following:Either explicit
rendercall without a renderer option, or no render call at all (implicit render).What happens under the hood is a chain of calls that effectively call
render_to_body, usually onActionView:https://github.com/rails/rails/blob/04972d9b9ef60796dc8f0917817b5392d61fcf09/actionview/lib/action_view/rendering.rb#L108-L124
This in turn finds its template via
context.in_rendering_contextwhich comes fromActionViewas well: https://github.com/rails/rails/blob/04972d9b9ef60796dc8f0917817b5392d61fcf09/actionview/lib/action_view/base.rb#L257-L274…which finally uses the
lookup_contextto see which templates are really available. It will use the provided views_paths (usually a list of all the view paths in your project) to find files and tries to match these things:https://github.com/rails/rails/blob/04972d9b9ef60796dc8f0917817b5392d61fcf09/actionview/lib/action_view/lookup_context.rb#L43-L52
en)html)mobile)haml)We can go deeper, but the important thing to understand is that a file
index.html+mobile.hamlwill be _processed (handled) by haml, only be matchable if the variant requested ismobile, and will outputhtml(because that is the format).The matched template will actually set the format (see
@rendered_formatabove).Turbo, and why
index.hamlfailsOkay. Final step. Turbo doesn’t use native browser navigation but rather a fetch(y) request. It will create a special request. This will happen first when a form is being submitted, first via
requestStartedwhich dispatches theturbo:submit-startevent, and then viastart, which, if allowed (confirm) will call the fetch requestperformfunction.(We’re almost at the point where everything lines up).
This function calls
prepareHeadersForRequeston the form submission which basically checks if the form will understand a Turbo Streams response and if so addstext/vnd.turbo-stream.htmlto theAcceptheader. If it doesn’t, then it only acceptstext/html, application/xhtml+xml.WHY does it accept turbo streams? Because of this line: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/drive/form_submission.ts#L224
Your controller does it’s action and then redirects to a new location by passing the
locationheader. After it has redirected, it renders theindexaction and the response is sent back to the client, right?The code that determines what gets called when it returns can be found here: https://github.com/hotwired/turbo/blob/256418fee0178ee483d82cd9bb579bd5df5a151f/src/core/drive/form_submission.ts#L180-L196
Either it succeeds or it fails, the delegated functions are here: https://github.com/hotwired/turbo/blob/aeeaae8edb5f6ec6ef6b19eaf93dc060a1e67b10/src/core/drive/navigator.ts#L92-L126.
The particular lines that we care about are:
That
responseHTMLproperty is defined here: https://github.com/hotwired/turbo/blob/aeeaae8edb5f6ec6ef6b19eaf93dc060a1e67b10/src/http/fetch_response.ts#L50-L56This basically says: if the content type is
text/htmlor the xhtml equiv, then I know it’s HTML and I can interpret it. In that case only process the response and let turbo visit the new content. It does matchtext/vnd.turbo-stream.htmlas well, (and anything that ends withhtml).Okay. So. Why doesn’t it then?
Accept: text/vnd.turbo-stream.html, text/html, application/xhtml+xml. The order is significant..turbo_streamor.html. So then it tries to look for a less specific thing. It finds a file without format calledindex.haml.rendered_format = rendered_template.format || lookup_context.formats.first)Content-Type: text/vnd.turbo-stream.html.Hope it helps.
TL;DR
Acceptheader.action.handlerinstead ofaction.format.handler).Content-Type) a turbo stream.I had to explicitly use
render ..., status: :unprocessible_entityto make the page show with errors andredirect ..., status: :see_otherto make the page redirect.Perhaps that helps?
Here is a quick fix for those who came there:
config.action_view.form_with_generates_remote_forms = trueat your initializer e.g.config/initializers/new_framework_defaults_6_1.rbapp/controllers/concerns/turbo/redirection.rbIt’s just a copy of the redirection handler from
turbolinks-railsgem 3. Add Turbo to the window at your application.jsThat’s it. Your legacy code works.
@seanpdoyle the problem is when migrating larger apps there is a lot of complex rails UJS code, which is impossible to migrate on
turbo_streamsbecause UJS is more powerful DOM manipulations wise.So to make it work some forms need to be remote with
data-turbo=false. Disabling the turbo enables old UJS code to continue working. But, trurbo-rails gem is missing this piece from the turbolinks, hence redirects are not working from the JS request.This is maybe a different issue from the one @kiddrew posted.
I tried modifying the response to use
:unprocessable_entityand:see_otherand it didn’t fix my issue. The submit and subsequent get request both work as expected but the form render or redirect doesn’t happen.I’ve just gone through every comment and I think my problem differs in a way that my create action was correctly redirecting with turbo-rails 1.3.3 but stopped doing so after upgrading it. After upgrading the gem to 1.4.0 I can still see the 302 response but it’s actually staying in the same view and updating the turbo-frame with “Content missing”. Here’s the code:
As you can see it’s pretty basic and I just have an html response when a message is saved. Of course the form is submitting a POST.
@SleeplessByte That is correct. It’s not well documented yet, but Rails is switching to returning a 422 on fail: https://github.com/rails/rails/pull/41026
Devise will be ready for this change soon: https://github.com/heartcombo/devise/pull/5340
Really excited to start playing with these new toys 🎉
Thanks. Followed your instructions at https://github.com/hotwired/turbo-rails/blob/main/UPGRADING.md and tried so many ways to make it work - it still won’t render the redirects 😦. Only solution for me is to opt out of Turbo for all my forms using
data-turbo='false'. Hoping for a fix in the future, because I like the lazy loading and the general idea.For what it’s worth, the solution in issue 138 in hotwired/turbo worked for me - simply setting
data-turbo-frame = "_top"in the form. Full credit goes to @inopinatus.That enables your form to accept responses which don’t contain a
turbo-frameand properly render the redirect.Is anyone else experiencing an issue where they want to use
turbo-confirmto add a confirmation dialog before archiving a record, while also keepingturbo: falseso they can redirect to a new page if the record is saved? The form is within aturbo_frame_tag.more than a year later and redirection still doesn’t work with form when using turbo by default in rails 7, anyone got a solution?
Yes and no.
Prepending signals “I prefer a turbo stream response” which you do. If you’d append it and the controller can return a html response, it will, despite a turbo stream response being available.
The real issue is that rails is defaulting to turbo stream implicitly. That’s the real bug!
Edit: rails is returning a full document as a stream which it should do imo. Unless you explicitly opt in.
This was actually the problem. Wow 🤯
My editor was saving files with only the
.slimextension. Explicitly using.html.slimfixed my app.@kiddrew is your
simple_form_forinside anyturbo-frame? if so, turbo will load the redirect via ajax and not do any actual redirect orTurbo.visit().@danielricecodes i just tried your scaffold example in a fresh rails 7 app. everything works. like with turbolinks. but there also is no
turbo-frameat play.i was scimming through turbo’s code quickly and from what i’ve seen the following happens:
everything inside a turbo-frame tag is handled through a
FrameController. it’ll submit forms, detect redirects, load the resource redirected to, scan the response for turbo frames and replace those on the page. that’s why you won’t see any other change on the page.now, for a form outside of a turbo-frame, the
Navigationclass will handle the submission and actually do a redirect (or execute aTurbo.visit()).so, there you have it. you cannot break out of a turbo-frame. which is unfortunate i think.
my use case:
@danielricecodes what’s the file names of the views? They are probably .html.erb.
What are your file names?
@SleeplessByte - I really appreciate the thorough response but this issue is maddening. On a brand new Rails 7.0.3.1 app I used
rails generate scaffold quote name:stringwhich generates the following:NONE OF THE SUGGESTIONS ABOVE ARE PRESENT IN THIS FILE, YET THE FORMS AND EVERYTHING WORKS OUT OF THE BOX. I do not need to copy the view template code into this reply because it’s all unedited scaffold. In a Rails 7 scaffold controller, there is no
:see_other, there are norespond_toblocks, and it is using implicit rendering…and yet…scaffold works out of the box while upgraded apps are horked. 😡That said, it is extremely challenging to tell what the problem is with my recently upgraded Rails 7 app when such a basic scaffold works perfectly and without any of the above suggestions. My Rails 7 app has
@rails/ujsremoved and there are no remote forms. Nothing with Rails UJS is interfering here. For all intents and purposes, my Rails 7 app’s forms should be working but they are not.I will keep working on it and post my solution when I find it but good gravy this issue needs to be fixed or the upgrade docs need to be a lot more clearer than they are.
@SleeplessByte thanks for the thorough explanation!
This would explain the trouble I had with Turbo. In an existing app which uses haml, upgrading to Turbo broke all forms which had a redirect response. Using
status: :see_othernever got the response to render. I still just have Turbo disabled on all forms which made me wonder what the point of upgrading from Turbolinks was. That should absolutely be a bug.I had troubles with
status: :unprocessible_entitynot rendering any changes onto the screen (even though the HTML was returned from the server). It turns out turbo-rails require.htmlin the view template name. I had a bunch ofnew.slimoredit.slimwithout the.htmlpart, and that was what was causing trouble.Is this documented anywhere? Couldn’t find anything about the correct template naming.
If you disable turbo, it will be summited in the typical fashion. You can disable turbo on the form instead of removing it completely.
https://turbo.hotwired.dev/handbook/drive#disabling-turbo-drive-on-specific-links-or-forms
I just tried again to upgrade from Turbolinks to Turbo using the concern method that @givemetraffic posted, and all my forms are still broken. Rails version is 6.1.4. Turbo-rails is 0.5.12. Manually adding
data-turbo: falseto each form works.Edit: I noticed that button_to and simple_form_for don’t use form_with, so the action_view config line doesn’t make those remote automatically. I confirmed that forms built with form_with work as expected. Every form I have is generated using simple_form or button_to, so manually updating each appears to be my only option.
A million thanks to @SleeplessByte for thoroughly demystifying this issue that folks have been battling against for—checks date when this issue was created—two years and one day! I wish there was a way to pin your answer to the top, especially since it feels like there’s unlikely to be any remediation (unless I’ve missed something).
(If you haven’t read it, read it.)
It seems like maybe not prepending the Turbo Streams content type would fix it, though, right? Assuming that would not break Turbo Streams, I would like to see that happen, and here’s why.
The reason that so many are reporting that form redirection is broken out of the box is that we are following two longstanding Rails conventions in our new apps:
But because we now want to allow requests for Turbo Streams to work seamlessly, we’ve disrupted this pattern, so that you have to change one of those two conventions in your app just to get form submissions to work.
Wouldn’t it be better to shift the burden onto the newer feature, Turbo Streams, by appending it to the Accept header instead of prepending it?
I’ve stumbled upon this issue yesterday. My App relies on 2 format renderers in update method in controller: turbo_stream and html. In one case I do post form with rails-ujs from haml template (submit button on top of the page, above form) and CTRL instead of html renderer it always used turbo_stream. By some kind of intuition I’ve re-ordered renderers and it worked. And today in the morning I’ve discovered great explanation of why it worked - default renderer. Great job @SleeplessByte ! Thx a ton
FYI the haml / slim issue has already been confirmed here: https://github.com/hotwired/turbo-rails/issues/287.
It was exceedingly difficult to troubleshoot that issue and I only noticed because @SleeplessByte mentioned it on a thread I’ve been following for years.
If what @vikdotdev says is true (I haven’t attempted to reproduce), seems like that should be logged as a bug. A .html.slim file extension isn’t technically correct. The file isn’t HTML, it’s Slim. Slim converts to HTML but it is not HTML. Like coffee is to JS.
The solutions are in this issue.
In short: use
redirect_to x, status: :see_otherwhenever you are redirecting from a form submission, usestatus: :unprocessible_entityto render errors in-line (so without redirecting), or disable turbo on the form.A bit ridiculous that you can create forms now but can’t redirect after submission. Is there a way to get rid of this JS crap completely and do simple html redirects? I’m stuck now right now. I’ve removed
turbo-railsgem from Gemfile and the forms still don’t redirect. What am I doing wrong?I’m running into issues with this too. We have a download button that uses Rails UJS and submits a POST request to the action which redirects to an external URL for download using
status: :see_otherthat I no longer can figure out how to get working.Here’s the step we did for Basecamp 3 to arrive at coexistence for an app built for Turbolinks/UJS: https://world.hey.com/dhh/bringing-hotwire-to-basecamp-91a442d6
I ended up adding
config.action_view.form_with_generates_remote_forms = falseto myapplication.rbwhich prevented me needing to touch all forms by default@jasonfb thank you for creating https://github.com/jasonfb/TR001 to help reproduce the issue. Would it be possible for you to alter the git history so that the changes that are tied directly to reproducing the bug behavior are their own commit? It’s very difficult to read through a commit that has a majority of its changes generated through Devise installation tasks.
After quickly scanning through, I have some high level questions:
form_forcalls generating HTML with[data-remote="true"]?format.turbo_stream { redirect_to "/nowhere" }call intentionally redirecting, or is it to demonstrate something else? Typically,redirect_tois better called from within theformat.html { }block, or in a controller response that omits theformatblocks entirely.link_to "Log out"with abutton_toinstead?Thanks @dixpac. Could you open a separate issue?
@seanpdoyle there is a small issue with redirects. When using Rails UJS, and setting the form as
local: false, data-turbo="false"redirects are not working.