Saying stuff about stuff.

Dynamically setting Rails default_url_options in Heroku review apps

I love Heroku review apps but not when URLs in emails point to the parent app — and if you have a hypermedia API its _links can end up totally broken. Why does this happen and how can it be fixed?

My usual approach used to be to configure Rails.application.routes.default_url_options[:host] from a DEFAULT_URL_HOST environment variable, but if the review app inherits this variable then all of its URLs generated outside the controller/view request/response cycle — including URLs in emails and serializers such as ActiveModel::Serializers — incorrectly point to the parent app. In the past I’ve been forced to manually update a review app’s config but that isn’t a solution.

It isn’t possible to dynamically set an environment variable but you can detect a review app at run-time by enabling the HEROKU_APP_NAME environment variable and then use it to generate the correct host. To do this in Rails create an initializer config/initializers/default_url_options.rb with the following contents:

# If a default host is specifically defined then it's used otherwise the app is
# assumed to be a Heroku review app. Note that `Hash#fetch` is used defensively
# so the app will blow up at boot-time if both `DEFAULT_URL_HOST` and
# `HEROKU_APP_NAME` aren't defined.
host = ENV['DEFAULT_URL_HOST'] ||
  "#{ENV.fetch('HEROKU_APP_NAME')}.herokuapp.com"

# Set the correct protocol as SSL isn't configured in development or test.
protocol = Rails.application.config.force_ssl ? 'https' : 'http'

Rails.application.routes.default_url_options.merge!(
  host: host,
  protocol: protocol,
)

In staging and production apps you should set the DEFAULT_URL_HOST using heroku config:set, but in review apps tell Heroku to pick up the HEROKU_APP_NAME variable by adding it to the app’s app.json (and remove references to DEFAULT_URL_HOST):

{
  "env": {
    "HEROKU_APP_NAME": {
      "required": true
    }
  }
}

Note that this also means in development/test/CI you’ll need to define the DEFAULT_URL_HOST environment variable or the app won’t boot — on my development machine I use direnv, dotenv is also popular.

And that’s it, now all URLs generated in emails and serializers will correctly point to the review app’s host.