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!