Rails CORS Middleware For Multiple Resources

Published 04 Jun 2021 · 2 min read
Add multiple blocks to Rails CORS middleware to support multiple endpoints.

A short post for today on a usage of CORS Middleware for Rails (well any Rack application) that wasn't obvious from the docs - how to specify multiple endpoints, or resources?

If you want Javascript from a web page that is hosted on a different domain than your Rails app (or any app actually) to make HTTP API calls to the Rails app, it will require adding CORS support (Cross-Origin Resource Sharing) on the app server. Otherwise the request will fail due to the Same-origin policy.

In my case, I have a Rails server that provides some back end services for this blog (which is statically hosted on Github Pages). So the blog and Rails server live on different domains. One of the services is privacy focused analytics. Visits to blog pages are recorded via a POST {{my-rails-server}}/visits with some information about the page being visited and user agent (but no cookie is used for further tracking, hence the privacy focus).

In order for the POST from the blog hosted on Github Pages to be allowed through to the Rails server, the Rails server needs to have CORS middleware configured as follows:

# Gemfile
gem 'rack-cors'
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'https://danielabaron.me'
    resource '/visits', headers: :any, methods: %i[post]
  end
end

This allows the POST HTTP method on the /visits resource, only from the origin https://danielabaron.me.

Multiple Resources

This has been working well, but recently I added a second service to the Rails server which also needed to be accessible to the blog with CORS. This is a search service, backed by Postgres full-text search. The API is available via a GET {{my-rails-server}}/search?q=whatever.

Looking at the Configuration Reference for the Rails/Rack CORS middleware gem, I couldn't determine how to add a second resource to the cors middleware configuration. It says:

A Resource path can be specified as exact string match (/path/to/file.txt) or with a * wildcard (/all/files/in/*)

I already had an exact string match /visits, and only wanted to allow exactly one more /search. A wildcard wouldn't work for this.

Solution

Turns out, this middleware supports multiple blocks. To allow CORS for two different, exactly specified resources, the following works:

# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'https://danielabaron.me'
    resource '/visits', headers: :any, methods: %i[post]
  end

  allow do
    origins 'https://danielabaron.me'
    resource '/search', headers: :any, methods: %i[get]
  end
end

Local Development

One final tweak to support local development. When developing on this blog (or whatever web app you're working on), it will be running on localhost, for example http://localhost:8000, and the Rails server also runs locally at http://localhost:3000. In this case, the Rails server should accept CORS requests from http://localhost:8000. To get this flexibility, use an environment variable ALLOWED_ORIGIN as follows:

# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins ENV['ALLOWED_ORIGIN'] || 'http://localhost:8000'
    resource '/visits', headers: :any, methods: %i[post]
  end

  allow do
    origins ENV['ALLOWED_ORIGIN'] || 'http://localhost:8000'
    resource '/search', headers: :any, methods: %i[get]
  end
end

Rails.logger.info("Cors Configured to allow origin: #{ENV['ALLOWED_ORIGIN'] || 'http://localhost:8000'}")

When the Rails server runs locally, don't specify any environment variable and it defaults to allowing localhost requests. For production (or any other environment), run the server with the ALLOWED_ORIGIN environment variable set to the origin that requires access.