Using URL helpers in Rails ActiveJob background jobs

inopinatus

I’ve just moved some of our code to run asynchronously and found that URL helpers aren’t available inside ActiveJob jobs; at least, not the way they are within Rails views, controllers and mailers. We can fix this; read on for how.

I wanted to simply write:

class NotificationJob < ActiveJob::Base
  def perform(object, message)
    NotificationService.send(url_for(object), message)
  end
end

… but invocation threw a NoMethodError because jobs don’t have url_for available.

At first I tried to just invoke the helper directly:

class NotificationJob < ActiveJob::Base
  def perform(object, message)
    NotificationService.send(Rails.application.routes.url_helpers.url_for(object), message)
  end
end

but this url_for threw a TypeError message, unable to handle being passed a model object!

A bit of studying the Rails source followed and it transpires - to my surprise at least - that the url_for reached when invoking url_helpers as a singleton is a different method to the url_for included as an instance method in views!

Solution

To pull in the method we want, we have to include the url_helpers module instead of referring to it as a singleton. Unusually, this module is not reachable directly as a constant name, but as an anonymous module via the Rails application object.

We also have to recognise that outside of a controller, there’s no request object to derive a base URL from, so as with ActionMailer we’ll need to configure the domain/port/protocol at runtime via a standard url_options hash.

So to make a slightly more reusable solution, I’ve created an application-specific base class in app/jobs/application_job.rb to inherit my jobs from:

class ApplicationJob < ActiveJob::Base
  include Rails.application.routes.url_helpers

  private
    def default_url_options
      Rails.application.config.active_job.default_url_options
    end
end

added the necessary config to config/application.rb:

config.active_job.default_url_options = { host: "example.com", protocol: "https" }

and changed my job class to inherit from this new base, thus:

class NotificationJob < ApplicationJob
  def perform(object, message)
    NotificationService.send(url_for(object), message)
  end
end

I don’t know if this is the best solution, but it helped me. If you come across this post with the same issue, I hope it helps you too!