TIL: Turbo Stream broadcasting needs default_url_options to be set

We've been using Turbo Streams in some of our recent prototypes, which makes it really easy and fun to get responsive and fun interactions set up. However, we kept having issues with images sent in a turbo stream response.

If the response was delivered by a normal controller render, e.g.

class ThingController < ApplicationController
def update
@thing = Thing.find(params[:id])
if @thing.update(thing_params)
respond_to do |format|
format.turbo_stream # renders `update.turbo_stream.erb`
end
end
end
end

... then any images included in that template would render as you'd expect.

However, any rendering that was triggered by one of the broadcast_... methods from turbo-rails — which is how you get content to update in "real time" across many clients — would break. So changing the controller above to something more like this:

class ThingController < ApplicationController
def update
@thing = Thing.find(params[:id])
if @thing.update(thing_params)
@thing.broadcast_to
respond_to do |format|
format.turbo_stream { @thing.broadcast_replace }
end
end
end
end

... would result in broken images, and if Action Text was being used, broken attachments too. When we reloaded the page, however, the images would be fixed.

It turns out this is due to how the rendering system determines the host to be used for any URLs it is generating.

In the first code example, the turbo stream template is being rendered by the controller so it can use the context of the request, including the host and port of the incoming request URL, to figure out what to use for any URLs it needs to generate.

However, when we switch to calling broadcast_..., or the background-job-rendered versions like broadcast_..._later, the rendering happens outside the context of a request, and so Rails needs to be told what host to use.

The way to do this is by setting config.action_controller.default_url_options in the config/environments/ configuration files, e.g.

config.action_controller.default_url_options = { host: 'localhost': port: 3000 }

Despite this being an issue I would've expected to be extremely common ( and indeed here's a very clear issue, a fix, and even a proposed top-level Rails.application.url), it doesn't seem to be included in the turbo-rails documentation.

If you've hit that issue and found this post, hopefully setting default_url_options will fix things for you too.