RobertJoseph
RobertJoseph

Reputation: 8158

Rails and CORS: why am I only seeing the OPTIONS request?

I am having a horrible time trying to get CORS working properly in my JSON api Rails app.

I have an Event model and associated controller. Events belong to RegisteredApplications.

# Routes.rb
Rails.application.routes.draw do

  root 'registered_applications#index'

  resources :registered_applications

  namespace :api, defaults: { format: :json } do
    # OPTION is an HTTP verb
    match '/events', to: 'events#create', via: [:options]
    resources :events, only: [:create]
  end

  devise_for :users

end

# events_controller.rb
class API::EventsController < ApplicationController

  skip_before_filter :verify_authenticity_token
  before_filter :cors_preflight_check
  after_filter :cors_set_access_control_headers

  # If this is a preflight OPTIONS request, then short-circuit the request,
  # return only the necessary headers and return an empty text/plain.
  def cors_preflight_check
    if request.method == :options
      headers['Access-Control-Allow-Origin'] = '*'
      headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS'
      headers['Access-Control-Allow-Headers'] = 'X-Requested-With, X-Prototype-Version'
      headers['Access-Control-Max-Age'] = '1728000'
      render :text => '', :content_type => 'text/plain'
    end
  end

  # For all responses in this controller, return the CORS access control headers.
  def cors_set_access_control_headers
    headers['Access-Control-Allow-Origin'] = '*'
    headers['Access-Control-Allow-Methods'] = 'POST, GET, OPTIONS'
    headers['Access-Control-Max-Age'] = "1728000"
  end

  def create
    registered_application = RegisteredApplication.find_by(url: request.env['HTTP_ORIGIN'])

    if registered_application.nil?
      render json: "Unregistered application", status: :unprocessable_entity
    else
      event = registered_application.events.new(event_params)

      if event.save
        render json: @event, status: :created
      else
        render @event.errors, status: :unprocessable_entity
      end
    end
  end

private

  def event_params
    params.permit(:name)
  end

end

The javascript I'm using to send new click events to my Rails app:

$( document ).ready(function() {
  $( window ).click( function() {
      metrics.report("click");
  });
});

var metrics = {};

metrics.report = function(eventName) {
  var event = { name: eventName };
  var request = new XMLHttpRequest();

  request.open("POST", "http://localhost:3000/api/events", true);
  request.setRequestHeader('Content-Type', 'application/json');

  request.send(JSON.stringify(event));
}

There are two issues:

1) When I make the request via the above javascript (from within another web page) I get a XMLHttpRequest cannot load http://localhost:3000/api/events. Request header field Content-Type is not allowed by Access-Control-Allow-Headers error. My Rails api requires the request to be in JSON so I don't know what I am supposed to do about that. If I comment out the setRequestHeader() line then the error goes away but then my Rails app doesn't receive the initial HTTP OPTIONS request.

2) On the Rails app side I see an OPTIONS request but the second (POST) request never occurs. This is the only thing in my log:

Started OPTIONS "/api/events" for ::1 at 2015-08-30 12:54:17 -0400
Processing by API::EventsController#create as JSON
  RegisteredApplication Load (0.2ms)  SELECT  "registered_applications".* FROM "registered_applications" WHERE "registered_applications"."url" = $1  ORDER BY created_at DESC LIMIT 1  [["url", "http://localhost:2222"]]
Unpermitted parameter: format
   (0.1ms)  BEGIN
  SQL (0.4ms)  INSERT INTO "events" ("registered_application_id", "created_at", "updated_at") VALUES ($1, $2, $3) RETURNING "id"  [["registered_application_id", 21], ["created_at", "2015-08-30 16:54:17.751257"], ["updated_at", "2015-08-30 16:54:17.751257"]] (0.4ms)  COMMIT
Completed 201 Created in 11ms (Views: 0.1ms | ActiveRecord: 1.1ms)

Am I missing something obvious?

Upvotes: 2

Views: 1630

Answers (1)

sideshowbarker
sideshowbarker

Reputation: 88408

I think you need to add headers['Access-Control-Allow-Headers'] = "Content-Type" in your cors_set_access_control_headers function in your server-side code.

My Rails api requires the request to be in JSON so I don't know what I am supposed to do about that. If I comment out the setRequestHeader() line then the error goes away but then my Rails app doesn't receive the initial HTTP OPTIONS request.

Per the CORS spec, if you send a Content-Type request header with a value that’s anything other than application/x-www-form-urlencoded, multipart/form-data, or text/plain, then a CORS preflight OPTIONS request is sent.

So, when you send a Content-Type header with the value application/json, it triggers the OPTIONS request. But when you drop the setRequestHeader() line, then no custom Content-Type header is sent (and no other custom “author” headers), so in the case, no CORS preflight is required, so no OPTIONS request is made.

Upvotes: 3

Related Questions