Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Gracefully handling InvalidAuthenticityToken exceptions in Rails 4

I've just upgraded an app from Rails 3 to Rails 4, and I'm seeing a bunch of InvalidAuthenticityToken exceptions popping up. Digging in it looks like it is fairly common for our users to have multiple long-lived tabs open on our site. So I think what's happening is something like this: user Alice has three tabs open and her session expires. She logs back in on one of the tabs, which updates the authenticity token stored in her session. Then she returns to one of the other open tabs and tries to submit data, but she gets a 500 error from the InvalidAuthenticityToken error we raised.

It would clearly be nice to do some error handling for Alice so she doesn't get a 500 error. I'm wondering about best practices for this kind of situation. What would be a nice way to handle Alice's submission from the expired tab? I don't want to expire the current session, because that would be super annoying from the user's perspective ("I just logged in, you dolt!"). Ideally, I just want the user to reload the page, which would result in the correct authenticity token being present in the form. Or should I be doing something different so that the long-lived tabs that are open notice that the session has expired and force a reload? This would probably be sub-optimal from the user's point of view, because they liked having that page ready and easily accessible to keep referencing, which is why they left it open in the tab in the first place.

like image 278
Bee Avatar asked Apr 08 '16 21:04

Bee


2 Answers

In my rails 4.1.1 app I had the same problem. I solved it by adding this code to ApplicationController. I found this solution here.

rescue_from ActionController::InvalidAuthenticityToken, with: :redirect_to_referer_or_path

def redirect_to_referer_or_path
  flash[:notice] = "Please try again."
  redirect_to request.referer
end

This way any controller that inherits from ApplicationController will handle the error with a redirect to the page the form was submitted from with a flash message to give the user some indication of what went wrong. Note this uses the hash syntax introduced in Ruby 1.9. For older versions of Ruby you will need to use :with => :redirect_to_referer_or_path

like image 60
Durrell Chamorro Avatar answered Nov 18 '22 02:11

Durrell Chamorro


The solution to this problem can be divided into 2 phases. Phase 1 addresses the issue of ActionController::InvalidAuthenticityToken error and phase 2 deals with the issue of long tabs waiting idly.

Phase 1(1st variation)

One way to go about is redirect the user back to their location before the error. For ex. if Alice has 3 tabs open, the first one expires and Alice logs in again in it because she was browsing on it. But when she moves to tab 3 which has URL 'http://example.com/ex' and submits a form. Now instead of displaying her an error we can redirect her back to 'http://example.com/ex' with her submitted form values already pre-filled in the form for easy use.

This can be achieved by following this approach:

1) ApplicationController - Add this function:

def handle_unverified_request
    flash[:error] = 'Kindly retry.' # show this error in your layout
    referrer_url = URI.parse(request.referrer) rescue URI.parse(some_default_url)
    # need to have a default in case referrer is not given

    # append the query string to the  referrer url
    referrer_url.query = Rack::Utils.parse_nested_query('').
        merge(params[params.keys[2]]). # this may be different for you
        to_query

    # redirect to the referrer url with the modified query string
    redirect_to referrer_url.to_s
end

2) You need to include a default value for all your form fields. It will be the name of that field.

...
<% f.text_field, name: 'email', placeholder: 'Email', value: params[:email] %>
...

This way whenever Alice will submit a form with wrong authenticity_token she will be redirected back to her form with the original values she submitted and she will be shown a flash message that kindly retry your request.

Phase 1(2nd variation)

Another way to go about is just redirect Alice back to the form which she submitted without any pre-filled values.

This approach can be achieved by:

1) ApplicationController - Add this function:

def handle_unverified_request
    flash[:error] = 'Kindly retry.'
    redirect_to :back
end

Phase 2

To tackle the problem of long awaited tabs you can take the help of SSEs. Rails 4 has ActionController::Live for handling SSEs.

1) Add this to any controller:

include ActionController::Live
...
def sse
    response.headers['Content-Type'] = 'text/event-stream'
    sse = SSE.new(response.stream, retry: 2000, event: 'refresh') # change the time interval to your suiting
    if user_signed_in? # check if user is signed-in or not
        sse.write('none')
    else
        sse.write('refresh')
    end
ensure
    sse.close
end

2) Give the above function a GET route in your routes file. Lets call this route '/sse'

3) Add this in your layout:

<% if user_signed_in? %> # check if user is signed-in or not
    <script>
        var evtSource = new EventSource("/sse");

        evtSource.addEventListener('refresh', function(e){
            if(e.data == 'refresh'){
                window.location = window.location.href;
            }
        });
    </script>
<% end %>

Note: using EventSource is not supported by all browsers. Please check out the Browser compatibility section.

Source: rails 4 redirect back with new params & MDN: Using server-sent events

like image 15
Jagjot Avatar answered Nov 18 '22 04:11

Jagjot