The problem: Turbo just appends the response from server to the end of current page. Not replacing the page like expected.
Recently upgraded from Rails 6 to 7. Also I switched from importmap to esbuild.
I have a form that is making a post to a create method. The controller responds with
if @stamp.save
redirect_to stamps_path, notice: "Input saved"
else
The strange thing that happens is that the response is being appended to the bottom of the source code from the top. So at the end of the original source code a new head-section from the response is appended like
<...> The whole initial html <...>
</body>
<title>Page title</title> <-- The server response gets appended after </body>
<meta .... the whole page gets repeated again
Not really sure what to show you here, been digging through all settings etc. I have never seen Turbo behave like this with a respons.
source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }
ruby "3.1.2"
gem "rails", "~> 7.0.2", ">= 7.0.2.4"
gem "sprockets-rails"
gem "puma", "~> 5.6"
gem "haml-rails", "~> 2.0"
gem "sqlite3", "~> 1.4"
gem 'devise', '~> 4.8'
gem 'devise-i18n', '~> 1.10'
gem "turbo-rails"
gem "jsbundling-rails", "~> 1.0"
gem 'sass-rails'
gem "stimulus-rails"
gem 'rubyXL', '~> 3.3'
gem "jbuilder"
gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ]
gem 'kaminari', '~> 1' # pagination gem
gem "bootsnap", require: false
group :development, :test do
gem "debug", platforms: %i[ mri mingw x64_mingw ]
end
group :development do
gem "web-console"
end
group :test do
gem "capybara"
gem "selenium-webdriver"
gem "webdrivers"
end
{
"private": true,
"license": "ISC",
"name": "name",
"version": "0.1.0",
"scripts": {
"release": "npm run build && npm run publish",
"test": "echo \"Error: no test specified\" && exit 1",
"build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.1.2",
"@fortawesome/free-brands-svg-icons": "^6.2.0",
"@fortawesome/free-solid-svg-icons": "^6.1.2",
"@hotwired/stimulus": "^3.1.0",
"@hotwired/turbo-rails": "^7.2.5",
"@popperjs/core": "^2.11.5",
"@rails/activestorage": "^6.0.0",
"bootstrap": "^5.2.0",
"esbuild": "^0.17.7"
}
}
I've been trying to replicate another rails app I have using Turbo. Been looking into all settings etc. Other pages can take a redirect_to and replacing the content as expected. I'm using haml so the HTML-structure should be fine.
I can only think of a one way this could happen: you have show.erb
template. Just rename it to show.html.erb
(or .haml, .slim, it doesn't matter).
By default, the forms are submitted as TURBO_STREAM, it's what rails shows in the logs, however, this is not the whole picture. The form submit request sets Accept
header:
Accept: text/vnd.turbo-stream.html, text/html, application/xhtml+xml
# ^
# here is where `TURBO_STREAM` comes from
These are the response Content-Type
s that turbo expects to receive. When you use respond_to
block in controller actions, Accept
header is what determines which format
block to call:
respond_to do |format|
if @model.save(model_params)
# if you have turbo_stream, it will run first
# format.turbo_stream { render turbo_stream: turbo_stream.replace(model) }
# if you don't have turbo_stream, next format in `Accept` header is html
format.html { redirect_to model_url(@model) }
format.json { render :show, status: :ok, location: @model }
# if nothing matches, you get `ActionController::UnknownFormat` error.
# the one you get when trying to render `format.js`, but Turbo
# doesn't handle js format, so it doesn't send `Accept: text/javascript`
else
format.html { render :edit, status: :unprocessable_entity }
format.json { render json: @model.errors, status: :unprocessable_entity }
end
end
When you use respond_to
block, rails will automatically set some render options, one of which is :content_type
which in turn sets Content-Type
response header. So if format.html
block runs it sets Content-Type: text/html
which turbo knows how to handle by redirecting or replacing the page content.
The important part is that we went from turbo_stream format to html. Needless to say that html needs to render html looking response and turbo_stream has to be a <turbo-stream>
tag.
What happens when you don't use respond_to
block. Rails will try to render a template that matches the format. For example, turbo_stream request will look for show.turbo_stream.erb
then show.html.erb
then show.erb
, like this:
render :show, formats: [:turbo_stream, :html]
Let's say it finds show.html.erb
, because there is an html
extension present, rails will set response Content-Type to text/html
and everything works as expected.
However, if it finds show.erb
, there is nothing to tell rails that it is in html format, we didn't use respond_to
block so response Content-Type is not set explicitly, the only option left is to fallback to the first Accept type, which is turbo_stream. Now you're rendering an html template but response type is turbo_stream, and there is no <turbo-stream>
tag.
When turbo sees a turbo_stream response it appends it to the document, which is what you're seeing. Since there are no <turbo-stream> instructions, it just stays untouched.
Long story short, something along the way needs to set the correct content type.
Either use respond_to
block which will set necessary render options. Or set render options yourself: content_type: "text/html"
. Or let rails know the content type by using html.erb
and turbo_stream.erb
extensions.
Note, when you redirect, turbo_stream content type stays, because turbo handles the redirect on the front end. If there is no html.erb
extension and no respond_to
block in the show action, you get a content type mismatch.