turbo: HTTP 303 Redirects with anchor not respected

Steps to Reproduce:

  • Redirect after a form submission (via HTTP 303) to a location that contains an anchor (e.g. Location: /path#anchor).

Observed Behavior:

  • The redirection occurs, but strips the anchor from the resulting page (e.g. /path instead of /path#anchor)
  • This results in any page rendering dependent upon this anchor to fail.

Expected Behavior

  • Redirect and expose the full path with the anchor in the resulting page’s address bar.

Workaround

  • Disable turbo on that particular form submission.

Wild Guess Hypothesis I suspect that the subsequent redirection fetch (from Turbo) strips the anchor tag from the underlying HTTP request (as expected), but when the page is re-hydrated, the address bar URL (pushstate) is not updated to reflect the original anchor.

About this issue

  • Original URL
  • State: open
  • Created 3 years ago
  • Reactions: 31
  • Comments: 17 (1 by maintainers)

Commits related to this issue

Most upvoted comments

I can confirm that this is an issue and I find it quite an unexpected behavior and would love to use my anchors. Adding a tailing /does not solve the issue for me when doing redirects in the controller:

redirect_to '.../posts/433545#comment_221760128', status: :see_other
# gets redirected to /posts/433545

redirect_to '.../posts/433545/#comment_221760128', status: :see_other
# gets redirected to /posts/433545/

In both cases the anchor is lost. Using 301, 302, 303 makes no difference. The only solution right now is to set data-turbo=falseon the form.

Any ideas how to fix this?

I’m seeing this behavior when performing any type of redirect after a successful form submission and the problem goes away if I disable Turbo. I’m also not using Rails so this is isolated to the JS library itself. I’m using beta 7 and I’m also not using frames or streams here. This is a basic full blown HTTP redirect.

Reproduceable steps:

  1. import * as Turbo from "@hotwired/turbo";

  2. Create a form where after the POST comes in, redirect to /#foo

  3. The #foo will get stripped out from the redirect

  4. Comment out the line from step 1 and do the same thing and now the anchor is kept in tact.

It’s worth pointing out params are kept in tact in either case. I’m only losing the anchor with Turbo.

While it may not be possible to fix this, at a minimum it would be helpful if Rails were to emit a log message whenever a redirect with an anchor response is produced in response to a request that is being processed as TURBO_STREAM indicating that the anchor will effectively be ignored, and perhaps suggesting data: {turbo: false} as a means to honor this parameter.

Unfortunately, looking at the current code, architecturally this is awkward. actionpack/lib/action_dispatch/http/url.rb is what processes the anchor option and it is entirely unaware of the request.

actionpack/lib/action_controller/metal/redirecting.rb could look at the location header that is produced and is aware of the request, but Rails to date literally has no code that is turbo specific.

Ideally, this would be a part of the hotwired turbo-rails gem, but as near as I can tell, absolutely none of server side logic which produces the redirect response goes through any code that is included in this gem. Perhaps turbo-rails could add some middleware to the processing, but that seems like a heavyweight solution that would penalize all applications in order to handle this one edge case.

If my humble ramblings above spark any sort of ideas on how this situation can be improved, I’ll be glad to develop or contribute a pull request to address this.

For those trying to execute the Stripe redirect, as mentioned by @rubys, using data: { turbo: false } is a solution that works. Considering Stripe checkout is redirecting to an outside site anyway, not using turbo in this one case is probably not a huge deal. So for those looking to make their Stripe redirect work, try this:

(In this example, we’re using a PaymentMethodsController)

<%# new.html.erb, the key here being data: { turbo: false } %>
<%= link_to "Add credit card", new_payment_method_path, class: 'btn btn-primary', data: { turbo: false } %>
class PaymentMethodsController < ApplicationController
  def new
    session_options = {...}
    session = Stripe::Checkout::Session.create(session_options)
    redirect_to session.url, allow_other_host: true
  end
end

Works well for me.

I was implementing a notification sidebar and I got into this problem.

In my implementation some notifications have an anchor in the redirect url so once the browser loads it can scroll to the correct element.

Here is my workaround for anyone that might find it useful.

I had to change the status code from the redirect_to so it doesn’t return a 30x code and it returns a 200 instead, so I can workaround the fetch atomic http redirect handling

class RecipientNotificationsController < ApplicationController
  def show
    @recipient_notification.read!
    # Workaround this sends status :ok because of opaque redirect in fetch API
    redirect_to notification_origin_path(anchor: ...), status: :ok
  end
end

Then on the notification card with the link I added a stimulus controller that handles the anchor link

<%= turbo_frame_tag dom_id(recipient_notification) do %>
  <%= link_to recipient_notification_path(recipient_notification), data: {
              turbo_frame: '_top',
              controller: 'redirect-preserving-url-anchor',
              action: 'redirect-preserving-url-anchor#redirect',
              'turbo-prefetch': false
            }, class: 'notification-link' do %>
            
            <% # ... %>
   <% end %>  
<% end %>

And here is the Stimulus contorller redirect_preserving_url_anchor_controller.js

import { Turbo } from '@hotwired/turbo-rails';
import { get } from '@rails/request.js';
import ApplicationController from './application_controller';

export default class extends ApplicationController {
  async redirect(event) {
    event.preventDefault();

    const response = await get(this.element.href);

    if (response.ok) {
      const location = response.headers.get('Location');
      Turbo.visit(location, { action: 'replace' });
    }
  }
}

Ran into this again. Given this code:

=link_to "Upgrade now →", account_payment_path(@account), "data-turbo-method": "post", class: "btn btn-primary is-medium"

Here’s the work around I went with this time:

= form_tag account_payment_path(@account), method: :post, "data-turbo": false do
  = button_tag "Upgrade now →", class: "btn btn-primary btn-lg w-full"

I believe this issue occurs for parameters as well. When I redirect with params in the same controller, the params are lost. If I redirect to a different controller, then the params are kept. Very strange!