Railsでのpreflightリクエストの実装およびテスト例
はじめに
Railsでpreflightリクエストの処理を実装する機会があったのでまとめてみました。
preflightリクエストとは
CORS(Cross-Origin Resource Sharing)という、originをまたいだAjax通信の際に発生するリクエストのことです。
発生条件など、詳しくは以下のサイトで述べられているので、このエントリでは詳しく解説しません。
実装例
RailsでJSON形式のAPIを作成しているとして、/api
以下のアクセスに対して異なるoriginからのアクセスを許可したいとします。
Controller
class Api::BaseController < ApplicationController before_action :validate_http_origin private def validate_http_origin request_origin = request.headers['HTTP_ORIGIN'] return if request_origin.blank? # 通常のリクエストであれば何もしない if request_origin == 'http://localhost:3000' response.headers['Access-Control-Allow-Origin'] = request_origin else render nothing: true, status: :not_acceptable end end end
class Api::OptionsRequestController < Api::BaseController ACCESS_CONTROL_ALLOW_METHODS = %w(GET OPTIONS).freeze ACCESS_CONTROL_ALLOW_HEADERS = %w(Accept Origin Content-Type).freeze ACCESS_CONTROL_MAX_AGE = 86400 def preflight return render nothing: true, status: :not_found if request.headers['HTTP_ORIGIN'].blank? return render nothing: true, status: :not_acceptable unless valid_request? set_preflight_headers! render nothing: true end private def valid_request? request_method = request.headers['Access-Control-Request-Method'] request_headers = request.headers['Access-Control-Request-Headers'].to_s.split(/,\s*/) allow_headers = Regexp.new(Regexp.union(ACCESS_CONTROL_ALLOW_HEADERS).source, Regexp::IGNORECASE) request_method.in?(ACCESS_CONTROL_ALLOW_METHODS) && request_headers.present? && request_headers.all? { |header| header.downcase =~ allow_headers } end def set_preflight_headers! response.headers['Access-Control-Max-Age'] = ACCESS_CONTROL_MAX_AGE response.headers['Access-Control-Allow-Headers'] = ACCESS_CONTROL_ALLOW_HEADERS.join(',') response.headers['Access-Control-Allow-Methods'] = ACCESS_CONTROL_ALLOW_METHODS.join(',') end end
config/routes.rb
Rails.application.routes.draw do namespace :api, defaults: { format: :json } do match '*path' => 'options_request#preflight', via: :options end end
Test (RSpec)
require 'rails_helper' RSpec.describe Api::OptionsRequestController, type: :controller do describe '#preflight' do before do request.headers['HTTP_ORIGIN'] = http_origin request.headers['Access-Control-Request-Method'] = request_method request.headers['Access-Control-Request-Headers'] = request_headers process :preflight, 'OPTIONS', path: :api end context 'HTTP_ORIGINヘッダーがない通常のリクエストの場合' do let(:http_origin) { nil } let(:request_method) { 'GET' } let(:request_headers) { 'accept, origin, content-type' } describe 'response' do subject { response } its(:status) { is_expected.to eq 404 } its(:body) { is_expected.to be_blank } end end context '許可されていないメソッドをリクエストした場合' do let(:http_origin) { 'http://localhost:3000' } let(:request_method) { 'POST' } let(:request_headers) { 'accept, origin, content-type' } describe 'response' do subject { response } its(:status) { is_expected.to eq 406 } its(:body) { is_expected.to be_blank } end end context '許可されていないヘッダーをリクエストした場合' do let(:http_origin) { 'http://localhost:3000' } let(:request_method) { 'GET' } let(:request_headers) { 'hoge' } describe 'response' do subject { response } its(:status) { is_expected.to eq 406 } its(:body) { is_expected.to be_blank } end end context '正常なリクエストの場合' do let(:http_origin) { 'http://localhost:3000' } let(:request_method) { 'GET' } let(:request_headers) { 'accept, origin, content-type' } describe 'response' do subject { response } its(:status) { is_expected.to eq 200 } its(:body) { is_expected.to be_blank } end describe 'headers' do subject { response.headers } its(['Access-Control-Max-Age']) { is_expected.to eq 86400 } its(['Access-Control-Allow-Headers']) { is_expected.to eq 'Accept,Origin,Content-Type' } its(['Access-Control-Allow-Methods']) { is_expected.to eq 'GET,OPTIONS' } end end end end
おわりに
RSpecでテストケースを書くときは、rspec -f d
で実行した時の出力が読みやすいように心がけています。
こうすると、あとで仕様との突き合わせが楽なのでオススメです。