Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Turbo Drive treats external URLs redirected from same-origin URLs as same-origin #401

Closed
TastyPi opened this issue Sep 20, 2021 · 19 comments

Comments

@TastyPi
Copy link

TastyPi commented Sep 20, 2021

According to the Turbo Drive docs https://turbo.hotwired.dev/handbook/drive#setting-a-root-location:

By default, Turbo Drive only loads URLs with the same origin—i.e. the same protocol, domain name, and port—as the current document. A visit to any other URL falls back to a full page load.

However, I have encountered two scenarios where this is not the case:

  1. Form POST request to same-origin returns a 303 redirect response to another domain
  2. An anchor with a link to a same-origin URL returns a 303 redirect response to another domain

In both cases, Turbo Drive attempted to load the external URL, which in my particular case causes a CORS error due to cross-origin requests not being allowed by the external page. I've fixed both cases by adding data-turbo=false to the <form> and <a> respectively, but based on the wording of the documentation I would have expected them to both fallback to a full page load.

Is it possible for Turbo Drive to handle redirects in the expected way?

@ghiculescu
Copy link

Turbo Drive tells its underlying fetch request to follow redirects:

redirect: "follow",

I think the problem is that Turbo can't know ahead of time if the page will redirect to a third party URL. So it has no way of knowing it if should send a fetch or not.

https://developer.mozilla.org/en-US/docs/Web/API/fetch suggests there's an alternative option (manual) which I suppose could be supported instead, it'd be a fair bit more code to support redirects rather than letting the browser do it for us.

image

@TastyPi
Copy link
Author

TastyPi commented Sep 21, 2021

Makes sense, I figured it would be something to do with automatically follwing redirects. If someone (maybe me, not sure if I have the time) did contribute a pull request which changed the behaviour, would it be likely to be accepted or would it be rejected due to adding too much complexity for a corner case like this? I'm fine with my solution of disabling Turbo if it wouldn't, but it might be worth at least updating the docs to mention this corner case.

@ghiculescu
Copy link

I think a docs PR is a good start. I’m not a maintainer so can’t answer the other question. My gut feel is this isn’t a very common case, as you say.

@mxlje
Copy link

mxlje commented Sep 25, 2021

I have the same problem. In my case I want to redirect users to an external payment provider during the checkout, and the URL has to be generated on the server.

Disabling Turbo technically works, but then I am also losing the ability to disable the button using the turbo events (to prevent double clicking), for example.

My current workaround is to use Stimulus and a turbo stream:

import { Controller } from 'stimulus'

export default class extends Controller {
  static values = {
    url: String
  }

  connect() {
	// add error checking as you see fit
    window.location.href = this.urlValue
  }
}
<%= turbo_stream.update :payment do %>
  <%= tag.div nil, data: {
    controller: "location-href",
    location_href_url_value: @url
  } %>
<% end %>

The page with the button has an empty <div id="payment">, which then receives the controller through the stream. It’s not ideal but it works and for me is better than disabling turbo.

marienfressinaud added a commit to flusio/Flus that referenced this issue Sep 27, 2021
Requesting access to Pocket account implies a redirection to an external
domain (i.e. getpocket.com). The form was handled by Turbo Drive, which
cannot access this domain because of the `Content-Security-Policy` and
`Access-Control-Allow-Origin` policies. Thus, Turbo Drive needs to be
disabled on this form.

The bug has been introduced by d65c702.

Reference: hotwired/turbo#401
marienfressinaud added a commit to flusio/Flus that referenced this issue Sep 27, 2021
Requesting access to Pocket account implies a redirection to an external
domain (i.e. getpocket.com). The form was handled by Turbo Drive, which
cannot access this domain because of the `Content-Security-Policy` and
`Access-Control-Allow-Origin` policies. Thus, Turbo Drive needs to be
disabled on this form.

The bug has been introduced by d65c702.

Reference: hotwired/turbo#401
@edwinv
Copy link
Contributor

edwinv commented Oct 5, 2021

We also experience this issue. We use data-method="post" for actions that make changes in the database. Some of these changes result in a redirect url which the user needs to visit, like a payment url or a oauth flow. For GET requests, we easily can disabled Turbo with data-turbo="false". For POST requests, there is no alternative because this feature is only available through Turbo. Changing the method to GET is not ideal due to the nature of the requests.

Ideally Turbo handles a redirect to a cross-origin location correctly. This doesn't seem to be that easy because with a fetch you can't handle the redirect yourself. Maybe the service-side could render a same-origin response that Turbo handles to load the new location. But that's not perfect either because Turbo is backend agnostic.

I'd love to hear @dhh his opinion on this. I expect a lot of Rails users upgrading to Turbo going to experience this issue. Maybe we should at least update the documentation for this, but maybe there are other approaches possible?

@TastyPi
Copy link
Author

TastyPi commented Oct 5, 2021

I wanted to leave a more complete workaround for what I'm currently doing, as it sounds like it would be helpful.

We have a POST action that creates a Stripe::Checkout::Session and then respond with a redirect to the session's URL, which is hosted by Stripe and external to us. This sounds similar to what both @mxlje and @edwinv are doing.

The way I've built ours is using a form with Turbo disabled and using 303 See Other as the response code, which redirects POST requests to GET requests. I also had to make sure data-remote was not being set on the form because ujs was also having issues (I forget the exact problem). Code example:

# payments_controller.rb
class PaymentsController
  def create
    session = Stripe::Checkout::Session.create(...)
    redirect_to session.url, status: :see_other
end
<%# views/payments/new.html.erb %>
<%= form_with url: payments_path, method: :post, local: true, data: {turbo: false} do |form| %>
  <%# Any other form fields %>
  <%= form.submit "Checkout" %>
<% end %>

Basically the form is turned into just a standard HTML form since all the magic is disabled. The main issue with this is it isn't possible to update just the form after an error since it's just a regular HTML form.

@boboldehampsink
Copy link

In my case, a form with method POST and an external action is also processed through Turbo. This shouldn't be happening, according to this?:

By default, Turbo Drive only loads URLs with the same origin—i.e. the same protocol, domain name, and port—as the current document. A visit to any other URL falls back to a full page load.

@TastyPi
Copy link
Author

TastyPi commented Nov 4, 2021

@boboldehampsink that sounds like a different issue, this issue is specifically about redirects from same-origin to external. I suggest you open a new issue.

@boboldehampsink
Copy link

@TastyPi indeed, sorry. Opened a new issue for that (#435)

@sagzy
Copy link

sagzy commented Nov 24, 2021

@TastyPi thanks for the workaround! Worked for us

@andrewjtait
Copy link

We also have an issue with this problem.

Similar to others, we have a form which can POST and update data. When there are errors we want Turbo to update the form for the user, but on success we want to follow a returned redirect to an external URL.

So disabling Turbo on this form fixes the redirect, but it does not provide a very good experience when there are form errors.

@TastyPi
Copy link
Author

TastyPi commented Feb 8, 2022

@andrewjtait we have a new solution that allows us to do both. We created a very basic Stimulus controller that uses JavaScript to redirect, and we use Turbo to add an element using that controller to the DOM

export default class extends Controller {
  static values = {url: String}

  def initialize() {
    window.location.href = this.urlValue
  }
}
<%# create.turbo_stream.erb %>
<%= turbo_stream.append "form" do %>
  <%= tag.div data: {controller: "redirect", redirect_url_value: @url} %>
<% end %>

It's not ideal, but it works.

@TastyPi
Copy link
Author

TastyPi commented Feb 8, 2022

I created a small Rails apps with examples of how to set up redirection at https://github.com/TastyPi/turbo-forms

@sedubois
Copy link
Contributor

I ended up here after attempting to change our existing Stripe Checkout client-side redirect (redirectToCheckout, which used to be their only option available) into a server-side redirect (as shown above). Our intention is to avoid the extra render and accelerate the transition to the checkout page. We want to keep XHR in the order form to allow displaying validation errors (so data-turbo should be true), but if validation passes it should redirect to the external Stripe checkout page. As noted here however, we encounter 403 Forbidden, "CORS Missing Allow Origin": Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at https://checkout.stripe.com/pay/cs_test_XXX (Reason: CORS header ‘Access-Control-Allow-Origin’ missing).

@TastyPi
Copy link
Author

TastyPi commented Mar 23, 2022

@sedubois we have the exact same use case. Check out the repo I linked to in #401 (comment).

TL;DR is we fixed this by creating a Stimulus controller that uses JavaScript to redirect to the Stripe URL. You can use Turbo Streams to add a div to the DOM that uses the Stimulus controller to redirect.

You have a form:

<%= form_with ..., id: "my-form" do %>
  ...
<% end %>

On success, render the following:

<%= turbo_stream.append "my-form" do %>
  <div controller="redirect" data-redirect-url="<%= stripe_checkout_url %>"></div>
<% end %>

On error, render the following:

<%= turbo_stream.replace "my-form" do %>
  <%= render "form" %>
<% end %>

You shouldn't use turbo-frame at all, just use turbo-stream to implement the behaviour you want. The Stimulus controller should just do window.location = this.urlValue and that should manually redirect to where you want.

I don't know if the Hotwired team have any plans to support this natively, but it works perfectly for us.

@sedubois
Copy link
Contributor

Yes @TastyPi, thanks for the recap. This looks similar to what we already have: redirect will happen client-side anyway. But it's true that it should allow us to stop relying on the legacy UJS and start using Turbo for the API. So we'll give it another try.

@dhh
Copy link
Member

dhh commented Jun 19, 2022

If anyone wants to summarize these challenges in a doc PR, that would be a good start. It's a basic CORS restriction, so I think treating validation errors with JS and then using vanilla forms for the redirect is a good path to suggest.

@fritzmg
Copy link

fritzmg commented Sep 7, 2023

Couldn't this be automated via error handling within Turbo instead?

@natematykiewicz
Copy link

natematykiewicz commented Jul 17, 2024

A simple solution is to use Turbo Power's redirect_to Stream Action.

respond_to do |format|
  format.html { redirect_to url, allow_other_host: true }
  format.turbo_stream { render turbo_stream: turbo_stream.redirect_to(url) }
end

In our case we have a Stripe Checkout flow that requires you to sign in / sign up before proceeding to Stripe for payment. Redirecting to Stripe caused a CORS error, but we didn't want to have to disable Turbo on every form in the sign in / sign up flow just because it might inevitably redirect to Stripe.

Instead, the action that does redirect to Stripe now returns a Turbo Stream to tell the browser to navigate to the Stripe Checkout URL. Turbo is on the whole time, and no CORS errors happen.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests