How can a flash message persist between requests in Rails? - ruby-on-rails

A tutorial I am following has the below code which it mentions is not quite right because the error flash persists one request longer than desired because render doesn't count as a request. The solution is to use flash.now instead.
But how is it even possible for the error flash to persist one extra request? Given that Rails is stateless how is the information of the flash stored for the next request?
class SessionsController < ApplicationController
def new
end
def create
user = User.find_by_email(params[:session][:email].downcase)
if user && user.authenticate(params[:session][:password])
# Sign the user in and redirect to the user's show page.
else
flash[:error] = 'Invalid email/password combination' # Not quite right!
render 'new'
end
end
def destroy
end
end

Use flash.now instead of flash.
The flash variable is intended to be used before a redirect, and it persists on the resulting page for one request. This means that if we do not redirect, and instead simply render a page, the flash message will persist for two requests: it appears on the rendered page but is still waiting for a redirect (i.e., a second request), and thus the message will appear again if you click a link.
To avoid this weird behavior, when rendering rather than redirecting we use flash.now instead of flash.

The flash is stored in the user's Session, which is associated with them on subsequent requests using HTTP cookies. The flash is just one part of the session whose data is automatically flushed on the next request. See the Rails Guide to Action Controller for more details.

Related

How to proceed after send_data() from controller?

I am trying a simple download from a server. This is using Rails 5.01, Ruby 2.24p230.
The view has a link to the controller to download data. It gets there just fine.
The controller method is just this:
def download
send_data("some sample text", filename:sample.txt)
flash[:success] = "it worked"
end
The result is that a file is named sample.txt containing the correct text is downloaded to the client. flash never happens. The view which linked to the controller is still on the screen, without any page refresh. A view called "download.html.erb" is never called.
My questions are:
Is there a simple way to cause some communication with the client following the send_data? It would be nice to tell the person on the client something after a successful download.
After the send_data, what should happen?
Thank you for taking the time to answer this.
To understand what is going on here you have to understand how the session and flashes work.
A flash message is stored in the session and carried on to the next request. When the request is finished flash messages from the previous request are removed from the "flash hash" which is stored in the session.
class TestController
# GET /foo
def foo
flash[:notice] = "Hello"
redirect_to '/bar'
end
# GET /bar
def bar
flash.now[:alert] = " world!"
end
end
So when the user requests /foo they are redirected to /bar and the flash hash will contain:
{
notice: "Hello",
alert: " world!"
}
So how is this relevant? When the client clicks the download button the flash message you set will be seen in the next request they perform. Which is not really that useful. Instead what you want to do is probably just use javascript and display a popup or some sort of message when the user clicks the download link.
When you send data to the client few will actually allow you to set any kind of redirect to be followed or will ignore any headers you send. This is because the huge number of potential annoying or malicious uses.

Devise stored_location_for(resource) returns nil

I read the few posts about troubleshooting stored_location_for here, but can't seem to figure it out and not sure how to troubleshoot.
I tried deleting my custom after_sign_in_path_for, but that didn't work either. My location is never getting saved, although as I understand it after each session/page update it should store the location. Do I need to through that in as a filter manually?
def after_sign_in_path_for(resource)
stored_location_for(resource) ||
if resource.is_a?(Account)
add_quote_to_account(resource)
if resource.applications.any?
edit_application_path(resource.applications(true).last)
else
root_path
end
else
super
end
end
May be you are not storing the location where you want to redirect_to after signing in with devise. Devise provides two methods - store_location_for and stored_location_for
https://github.com/plataformatec/devise/blob/master/lib/devise/controllers/store_location.rb
Suppose your user model is named "user" then
A call to store_location_for(:user, my_desired_path) in your controller will store the url "my_desired_path" in session with key "user_return_to". Basically this method will simply do this - session["user_return_to"] = my_desired_path. Probably you are missing this. I have a booking controller and a "login" action which stores the checkout location for booking in booking controller and then displays a login form for users in rendered view -
def login
my_desired_path = url_for(controller: 'bookings', action: 'checkout')
store_location_for(:user, my_desired_path)
end
Now you can use stored_location_for(:user) to retrieve my_desired_path from your session. So to say, a call to stored_location_for(:user) will return "my_desired_path.
Now if you use stored_location_for in your custom after_sign_in_path_for(:user) then it shall return "my_desired_path".
Additional Point -
A call to stored_location_for(:user) returns session[:user_return_to] but also clears this session after returning the value if your redirect format is navigational format. So a second call to stored_location_for(:user) will not return my_desired_path. And sometimes this is how we want our application to behave. On contrary, if your redirect format is not navigational format then session wont be cleared and a second sign-in will again redirect to same "my_desired_path".
Sometimes you want to redirect to different locations in signing-in from different pages in your application. Suppose, you want to redirect to "\X" on page A and to "\Y" on page B. Now follow these steps -
User in on page A - store_location_for(:user, "\X") is saved in session.
Application provides a sign-in form but User does not sign-in and just browse here and there in your application.
User is on page B and perform a sign-in expecting to land on "\Y" but unexpectedly lands to "\X".
So take care of it in your application. This scenario is more likely to occur in applications which uses AJAX for signing-in.

Flash doesn't clear after being viewed

Here is my create action. It creates a new instance of Message, which is checked with model validations.
Then there is a simple if else loop that sends the message if the model validations were fulfilled and renders the 'new' view if they want to send another. If the model validations weren't fulfilled, it simply renders the 'new' view again.
The flash hash is also populated with relevant information, a success message and an alert message if the message was successful or not.
def create
#message = Message.new(message_params)
if #message.valid?
Contactform.contact(#message.name, #message.town, #message.email, #message.content).deliver
flash[:success] = "Sent message"
render 'new'
else
flash[:alert] = "Alert"
render 'new'
end
end
However, I don't think I'm doing it right, because the messages stay in the flash. They aren't cleared after they've been viewed once.
How should I ensure that the flash is cleared, as it should be?
Update
This is how the error comes about. I send a message successfully, and I'm routed back to the 'new' view and I receive a 'success' flash. I attempt to send an erroneous message, and I'm routed back to the 'new' view and I receive both the 'success' flash from before, and a 'alert' flash. Very confusing for the user!
The second I browse to another page, both flashes disappear, but that's the problem I think. Something to do with coming back to the same page so Rails doesn't know to clear the flash. Can I force clear the flash?
I think reading the section about flash.now in the rails doc will help clear this up for you: http://guides.rubyonrails.org/action_controller_overview.html#flash-now.
Flash sticks around for the next request. In this case, you aren't issuing a new request after your create action - you are just re-rendering the new view. I think flash.now is what you want to be using here.
I would also say that in your create success case, the create action should be doing a redirect rather than just re-rendering the view. Are redirect_to and render exchangeable? explains the difference between redirecting and rendering

rails not picking session data

I am implementing a payment gateway in my app.
Its like this:
The user fills the form with necessary details, along with a field containing return_url(say http://myapp.com/ebs_payment/ebs_response?dr={somedata}) and submit the form to a secure payment site. After the transaction is complete, the secure site puts some encrypted data into my param {dr} and the user is redirected back to the return url. The problem here is, when the user returns to the app with the return_url, the application fails to pick up the session data and returns a nil value.
Before submitting the form, I put the object #fdPaymentDets in to session.
Here is my controller:
class EbsPaymentController < ApplicationController
#before_filter :login_required,:check_user_authenticate
#access_control [:ebs_response] => ('user')
def ebs_response
#fdPaymentDets = session["fd_payment_details"]
#deal = Deal.find(#fdPaymentDets.deal_id)
#categories = find_all_categories
end
private
def find_all_categories
#sp = SitePreference.find(:first)
Category.find(:all,:limit => #sp.categories_display_limit,:order => 'position')
end
end
When the user is redirected to the return url (http://myapp.com/ebs_payment/ebs_response?dr={encrypted_data}) from the secure site, rails is not picking the #fdPaymentDets object from session and making it nil thus resulting in an error when accessing data from the nil object.
The surprising thing is that, when I put the same return_url in my browser by hand, the session data is neatly picked and everything goes well.
Am missing any thing here? What could be the obvious reason?
Any help appreciated.
Cookies and redirects are messy and bug prone (from a browser's implementation perspective).
Take a look at
Safari doesn't set Cookie but IE / FF does
Suggestion would be to change the implementation to set the session first in the show action, and then update the value before the redirect

getting the flash hash to persist through redirects

My basic use case is do some processing, set flash[:notice], and then redirect to a new page. From what I can tell, redirects reset the flash tag (please correct me if I'm wrong). Is there a way to gain persistence? Using sessions isn't an option, and I have hacked around the problem using cookies, but I think there's got to be a better way.
The flash hash persists for exactly one redirect or render. So you should be fine with the default settings.
If you need to keep the flash hash for another request/redirect, you can call flash.keep.
flash.keep # keep the entire flash hash around for an extra request.
flash.keep(:notice) # keep just flash[:notice] for an extra request.
Something to be aware of in at least Rails v3.2.1 is that the flash will persist through a redirect if its not referenced at all through at least 1 redirect and load the same view after. This is a pseudo code of my recent experience:
def some_action
(code that may set a flag to redirect 1 time)
redirect_to action_path if(redirect_flag)
....
end
Running this would result in the flash[:message] being present regardless of the redirect.
def some_action
logger.debug("Flash[:message] #{flash[:message]}")
(code that may set a flag to redirect 1 time)
redirect_to action_path if(redirect_flag)
....
end
During debugging with the logger referencing flash[] it would only show up when the redirect didn't happen. I could see this being problematic if you added a reference to flash before a redirect and lost it down the line for no apparent reason.
See ruby docs here (Instance protected method: Use at the bottom)

Resources