Demistifying Rails default_url_options

The way default_url_options work is not very intuitive, here are my findings for Rails versions 7.0 and above.

I believe that a refactoring would be in order (I might as well try to make a PR one day) and that they should be centralized, favouring dynamic over static options.

url_options vs default_url_options

A URL is essentialy built by merging

explicit args  >  default_url_options  >  request-derived values

You should only deal with default_url_options. Do not touch url_options, it is an internal method used to build URLs.

What can you put in there?

┌─────────────────┬───────────────────────────────────────────────────────────────────────┐
│ :host           │ The hostname (required for _url helpers unless derived from request)  │
│ :port           │ Port number, e.g. 3000                                                │
│ :protocol       │ e.g. "http" or "https"                                                │
│ :subdomain      │ Subdomain portion; false strips all subdomains                        │
│ :domain         │ Domain portion, used with :tld_length                                 │
│ :tld_length     │ Number of TLD labels, defaults to 1 (used with :subdomain/:domain)    │
│ :anchor         │ Fragment appended to URL, e.g. "section-1"                            │
│ :params         │ Query string parameters hash                                          │
│ :path_params    │ Parameters only used for named dynamic segments; extras are discarded │
│ :trailing_slash │ If true, adds a trailing slash                                        │
│ :script_name    │ Path prefix relative to domain root (for mounted engines)             │
│ :only_path      │ If true, returns relative URL (no host/protocol)                      │
└─────────────────┴───────────────────────────────────────────────────────────────────────┘

Plus any route segment parameters (like :locale) which are merged into the path if the route defines them, or appended as query params if it doesn’t.

Relationships between the various items

Rails.application.default_url_options is an alias for Rails.application.routes.default_url_options, both reading and writing.

ActionMailer::Base.default_url_options is a separate object. It is populated dynamically from config.action_mailer.default_url_options when the railtie is initialized in a configuration block.

I’ve found some references to config.action_controller.default_url_options but AFAICT it doesn’t exist.

The various contexts

Keeping present that explicit args always have the priority, this is where default_url_options come from

Controllers

By default the controller will add to the url_options (not default!) host, port and proto.

You can define a default_url_options controller instance method, and this becomes the priority chain:

explicit args 
  > controller's default_url_options 
    > request-derived values (host, port, protocol)
      > Rails.application.routes.default_url_options

The options are merged, not replaced.

Mailers

Mailers can have their own separate default_url_options.

They can be an instance method on the mailer class.

You can also define them globally as ActionMailer::Base.default_url_options. The railtie initializer actually copies whatever you set as config.action_mailer.default_url_options in a rails configuration block to an ActionMailer::Base.default_url_options class attribute. If you try to set a value after the initialization is complete you must act directly on ActionMailer::Base.default_url_options.

explicit args
    > mailer's default_url_options (instance method, if defined)
      > ActionMailer::Base.default_url_options
        > Rails.application.routes.default_url_options

Important thing to keep in mind: the instance method will override the class attribute (if defined) unless it calls super.

Direct access

You might want to use URL helpers in a different context (models, jobs, service objects, presenters).

You can call the methods on the url helpers like this

Rails.application.routes.url_helpers.posts_path

In this case there will be no default_url_options instance method available, so it will just pick up the global Rails.application.routes.default_url_options.

You can otherwise include the helpers in your class (importing a lot of methods) with

include Rails.application.routes.url_helpers

And then you’ll be free to implement your own default_url_options method.

My recommendations

Try to keep the configuration simple. Define the static options in the config/environment/*.rb files. Make sure you are aware of any default_url_options instance method defined in controllers or mailers. Watchout for URL helper calls in other contexts. Avoid the draper gem, that makes it even more complicated.