Configuring for CORS and Cookies with React and Ruby on Rails

Chris Li
6 min readDec 6, 2022

--

You’re working on your first full stack application. You’re building your front-end using React and Javascript and your backend API is built using Ruby on Rails.

You have an idea, you’ve drawn your components, your database structure, you’ve run your migrations and seeded your database. The moment of truth, you want to connect the frontend and backend. You race through the installation bundle install… npm install. You run rails s to start your sever, you run npm start to run your frontend. You run your first fetch from your frontend to your backend and the API call fails… and staring at you is the dreaded CORS error.

What is CORS?

CORS stands for Cross-Origin Resource Sharing and simplistically, it essentially is a system that allows information to be shared across domains. In your fullstack application, your backend API may be hosted on http://localhost:3000, but your frontend react application is running on http://localhost:4000. You can’t run both on the same localhost given the PORT is already taken.

How to configure your Rails backend application for CORS?

Your Ruby application does not permit by default, the ability for CORS. This means, in the above example, any request received from a domain other than http://localhost:3000 will be rejected and you will see a CORS error in the console as per the error message above.

To fix this, you can enable a gem called ‘rack-cors’ . You can either do this by running ‘gem install rack-cors’, or include the gem “rack-cors” in your gemfile, then run bundle install in your terminal.

# in root/Gemfile
# ... other gems

gem 'rack-cors'

Next, add the following code in config/initializer/cors.rb. This will allow the rack-cors gem to permit requests received from the origin that you are running your fetch request from. In the example above, our React frontend is running on localhost:4000.

# in config/initializers/cors.rb

Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'http://localhost:4000'

resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end

You can be more prescriptive by specifying any headers or methods. However for the purposes of this blog, I have left the default options. Whenever you update configuration files, you need to restart your Rails server in order for these changes to take effect.

Now when you run your fetch request from the front end, the CORS error should no longer be there!

But what about Cookies and Sessions?

One of the next things I would like to do is implement user authentication using sessions. Upon a user logging in, I would like to store a a cookie ‘sessions[:user_id]’ in the browser so that requests from the frontend can be authorized.

With default React and Rails settings, this will not currently work. In our frontend App.js, below is the API fetch:

function App() {
useEffect(() => {
fetch(`http://localhost:3000/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user: 1 })
})
.then(r => r.json())
.then(console.log)
}, [])
}

The first thing that will hit is our routes, which sends the request to the create method in our sessions controller.

# config/routes.rb
Rails.application.routes.draw do
post '/login', to: "sessions#create"
end

In your sessions_controller, I have the following code:

# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController
# POST /login
def create
user = User.find(params[:user])
session[:user_id] ||= user.id
render json: user
end
end

If I put a byebug just after the session[:user_id] is set, and refresh my frontend application so the fetch runs, I am able to hit my byebug in my Rails terminal and I can access the session that I have created and session[:user_id] is equal to 1, as expected.

Exiting byebug, I can see that the method was successfully executed and an HTTP status of 200 OK was sent back to our frontend application. In our frontend, looking in DevTools and in the Network tab, you can see that the response was well received and a cookie was set along with the session_id:

However, looking at the Application tab, this cookie is not being stored in our browser… very strange?

Going back to our backend, if we send another request that hits a different route in our sessions_controller, are we able to access the session[:user_id] that we had previously instantiated?

class SessionsController < ApplicationController

def create
user = User.find(params[:user])
session[:user_id] = user.id
render json: user
end

def show
byebug
user = User.find(session[:user_id])
render json: user
end
end

Then update the routes:

Rails.application.routes.draw do
post '/login', to: "sessions#create"
get '/login', to: "sessions#show"
end

Now in my console in the browser, I’m going to send a GET request to my backend and see if I can access the session[:user_id].

fetch("http://localhost:3000/login")

Back in rails terminal, I hit my byebug, but I am unable to access the session or the session[:user_id]. What is going on?

Proxy

Your browser will only store cookies/sessions if the domain making the request and receiving the request are the same. While enabling CORS allows you to receive requests from other origins, it means that while the cookies/sessions are being generated by your backend and sent to your frontend, your browser is not actually storing these values.

One way to fix this is to update your proxy in your frontend application by adding one line to your package.json file: “proxy”: “http://localhost:3000”

Make sure the there is no “/” at the end, then run npm install and npm start to restart your frontend application.

// client/package.json

{
"name": "my-app",
"version": "0.1.0",
"proxy": "http://localhost:3000",
// ... rest of the file
}

Make sure to update all of your fetch requests to remove the “http://localhost:3000” given your updated proxy. For example, in App.js, the updated fetch should look like:

useEffect(() => {
fetch(`/login`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ user: 1 })
})

Now, when you you should see that the session[:user_id] is now persisting to your application.

I appreciate this is a very simple fix and a convoluted explanation and debugging process. I personally spent several hours trying to debug this and the above shows part of my debug process. I tried several methods, starting a new application from scratch, manually setting sessionsStorage in the front-end and trying to disable same-site strict settings (which you should never do!). None of them really gave me what I wanted.

If you have another or better/way to address this issue, I would be all ears!

References:

--

--