Apartment::TenantNotFoundをrescueするElevatorを使う

井原(@ihara2525)です。

apartmentを使ってサブドメインでテナントを切り替える場合、存在しないテナントのサブドメインにアクセスするとApartment::TenantNotFoundが投げられるのですが、これはApplicationController等のレベルではrescueできません。

というわけで、Rackのミドルウェアを追加してそこで吸収するのですが、apartmentでいうところのElevatorを継承して、Apartment::Elevators::Subdomainを親とするミドルウェアをつくってみます。

lib/middlewares/rescued_tenant_elevator.rb

require 'apartment/elevators/subdomain'

class RescuedTenantElevator < ::Apartment::Elevators::Subdomain
  def call(env)
    super
  rescue Apartment::TenantNotFound
    request = Rack::Request.new(env)
    Rails.logger.error "Apartment Tenant not found: #{subdomain(request.host)}"
    [302, { 'Content-Type' => 'text/hml', 'Location' => Rails.application.routes.url_helpers.root_url }, self]
  end

  # needed to work
  def each
  end
end

こんな感じで、superを呼び出してまずApartment::Elevators::Subdomainに処理を行ってもらい、例外が発生した場合は上記の例だとトップページに飛ばす、みたいな対応をしています。

さらに、これのテストも書いたので、それも載せておきます。

spec/lib/middlewares/rescued_tenant_elevator_spec.rb

require 'rails_helper'

RSpec.describe RescuedTenantElevator do
  include Rack::Test::Methods

  let(:test_app) { -> (env) { [200, env, 'app'] } }
  let(:app) { described_class.new(test_app) }

  context 'when the tenant is public' do
    it 'returns 200' do
      get('http://example.org')
      expect(last_response.status).to eq 200
    end
  end

  context 'when the tenant exists' do
    it 'returns 200' do
      get("http://#{SUBDOMAIN}.example.org")
      expect(last_response.status).to eq 200
    end
  end

  context 'when the tenant does not exist' do
    it 'redirects to service top page' do
      get('http://foo.example.org')
      expect(last_response.status).to eq 302
      expect(last_response.header['Location']).to eq 'http://localhost:3000/'
    end
  end
end

テナントがない(サブドメインがない)場合や、テナントがある場合は200を返しますが、テナントがない場合は上記のミドルウェアの実装に従って302を返し、所定のページにリダイレクトされることをテストしています。

何かのご参考になれば幸いです!