ruby-on-railsherokucross-domainheroku-api

Managing multiple domains in a rails application error: "NameError (uninitialized constant #<Class:0x0000563d7bf62250>::Heroku):"


In my rails app, I'm trying to create a booking form external parties(parks) can point their customers to to make a booking for the respective park. The booking form works with the url/route with the subdomain book

https://book.myapp.com/en/parks/:park_id/park_availability

Goal

I would like substitute my domain (myapp.com) with the website of the park, such that I get

https://book.parkapp.com/en/park_availability

Unfortunately I get the error message(s) upon creation park

NameError (uninitialized constant #<Class:0x0000563d7bf62250>::Heroku):

when using an existing park

{park.website}'s server IP address could not be found.

Outline attempted approach

  1. Park has a website column. In routes.rb I tried setting up the constraints and apply them on the park_availability action.
  2. In the Park model I attempted to add the domain (Park.website) to my Heroku application, once the park is saved.
  3. In my Park controller I try to find the @park, before the park_availability action.

Code

routes.rb

class CustomDomainConstraint
  # Implement the .matches? method and pass in the request object
  def self.matches? request
    matching_site?(request)
  end

  def self.matching_site? request
    # handle the case of the user's domain being either www. or a root domain with one query
    if request.subdomain == 'www'
      req = request.host[4..-1]
    else
      req = request.host
    end

    # first test if there exists a Site with a domain which matches the request,
    # if not, check the subdomain. If none are found, the the 'match' will not match anything
    Park.where(:website => req).any?
  end
end

Rails.application.routes.draw do
  resources :parks do
     match ':website/park_availability' =>  'parks#park_availability', on: :member, :constraints => CustomDomainConstraint, via: :all
end
end

park.rb

class Park < ApplicationRecord
  after_save do |park|
  heroku_environments = %w(production staging)
  if park.website && (heroku_environments.include? Rails.env)
    added = false
    heroku = Heroku::API.new(api_key: ENV['HEROKU_API_KEY'])
    heroku.get_domains(ENV['APP_NAME']).data[:body].each do |domain|
      added = true if domain['domain'] == park.website
    end

    unless added
      heroku.post_domain(ENV['APP_NAME'], park.website)
      heroku.post_domain(ENV['APP_NAME'], "www.#{park.website}")
    end
  end
end

parks_controller.rb

class ParksController < ApplicationController
before_action :find_park, only:[:park_availability]

def park_availability
  #working code...
end

private
def find_park
     # generalise away the potential www. or root variants of the domain name
    if request.subdomain == 'www'
      req = request.host[4..-1]
    else
      req = request.host
    end

    # test if there exists a Park with the requested domain,
    @park = Park.find_by(website: req)

    # if a matching site wasn't found, redirect the user to the www.<website>
    redirect_to :back
  end

end

Solution

  • To get around error in question you can try writing classname as ::Heroku, that will make ruby to look for it in correct scope.

    But Heroku legacy api has been disabled, so you should use their new platform-api:

    heroku = PlatformAPI.connect(ENV['HEROKU_API_KEY']) # note that you may also have to use a new key
    domains = heroku.domain.list(ENV['APP_NAME'])
    ...
    heroku.domain.create(ENV['APP_NAME'], ...)
    

    also to check for domain presence you can use domain.info api method instead of fetching all domains that are not needed.

    Keep in mind that callbacks are not the best place to make any external calls: if the api call fails for some reason (temporary network problems, api outage, server restart, etc) - whole transaction will be rolled back, item will not be saved and you can loose data. Better way is to enqueue a background job there, which will handle domains later, can retry if needed and so on.