From ec64359f32a3b1262dca4513a7ff8ed490f7acbd Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 11 Jun 2026 22:57:38 +0200 Subject: [PATCH] Fix MissingTemplate on Rails >= 8.0 with prepended controller modules Rails 8.0 moved render option normalization out of _normalize_render (which ran before custom renderers) into render_to_body (which runs after). Until then the renderer's own template/prefixes defaulting was dead code: Rails had already set options[:template] to the action name and options[:prefixes] to the controller's _prefixes by the time the :xlsx renderer block executed. On Rails 8.0 that defaulting is exercised for the first time, and computing prefixes via self.class.ancestors.take_while { |a| a.respond_to?(:controller_path) } returns [] whenever a module is prepended to the controller (memo_wise, instrumentation gems, ...): the prepended module is the first ancestor and does not respond to controller_path, so take_while stops at once. The bare template name is then looked up at the view-paths root and raises ActionView::MissingTemplate (e.g. "Missing template /show"). Use the controller's _prefixes instead - it is built from the superclass chain (local_prefixes + superclass._prefixes), immune to prepended modules, and exactly the value Rails itself injected before 8.0 and still uses for a bare render :action. --- CHANGELOG.md | 1 + lib/axlsx_rails/action_controller.rb | 11 ++++++++--- .../examples/prepended_module_controller.rb | 17 +++++++++++++++++ .../examples/prepended_module/show.xlsx.axlsx | 6 ++++++ spec/rails_app/config/routes.rb | 1 + .../prepended_module_controller_spec.rb | 18 ++++++++++++++++++ 6 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 spec/rails_app/app/controllers/examples/prepended_module_controller.rb create mode 100644 spec/rails_app/app/views/examples/prepended_module/show.xlsx.axlsx create mode 100644 spec/rails_app/spec/requests/prepended_module_controller_spec.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fee4a0..30279fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ **Unreleased** +- Fix `ActionView::MissingTemplate` on Rails >= 8.0 when a module is prepended to the controller: derive template lookup prefixes from the controller's `_prefixes` instead of `ancestors.take_while` **April 1, 2026**: 0.7.1 release diff --git a/lib/axlsx_rails/action_controller.rb b/lib/axlsx_rails/action_controller.rb index 09bcf42..7cf67c2 100644 --- a/lib/axlsx_rails/action_controller.rb +++ b/lib/axlsx_rails/action_controller.rb @@ -22,9 +22,14 @@ # if options[:template].nil? options[:template] ||= action_name - options[:prefixes] ||= self.class.ancestors - .take_while { |a| a.respond_to?(:controller_path) } - .map(&:controller_path) + # Use the controller's _prefixes (built from the superclass chain) instead of + # deriving prefixes from `ancestors`: a module prepended to the controller + # (memo_wise, instrumentation gems, ...) is the first ancestor and stops a + # take_while immediately, yielding no prefixes at all. Up to Rails 7.2 this + # branch was dead code because render option normalization ran before custom + # renderers and had already defaulted :template and :prefixes; Rails 8.0 + # moved that normalization after the renderer, exposing the bug. + options[:prefixes] ||= _prefixes end options[:template] = filename.gsub(%r{^.*/}, '') if options[:template] == action_name diff --git a/spec/rails_app/app/controllers/examples/prepended_module_controller.rb b/spec/rails_app/app/controllers/examples/prepended_module_controller.rb new file mode 100644 index 0000000..b13cc0c --- /dev/null +++ b/spec/rails_app/app/controllers/examples/prepended_module_controller.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module Examples + class PrependedModuleController < ApplicationController + # Gems like memo_wise or instrumentation libraries prepend modules into + # controllers. A prepended module becomes the first entry in `ancestors` + # and does not respond to `controller_path`, so a prefix computation based + # on `ancestors.take_while { |a| a.respond_to?(:controller_path) }` + # returns an empty array for this controller. + module Prepended; end + prepend Prepended + + def show + render xlsx: 'show' + end + end +end diff --git a/spec/rails_app/app/views/examples/prepended_module/show.xlsx.axlsx b/spec/rails_app/app/views/examples/prepended_module/show.xlsx.axlsx new file mode 100644 index 0000000..a9e974f --- /dev/null +++ b/spec/rails_app/app/views/examples/prepended_module/show.xlsx.axlsx @@ -0,0 +1,6 @@ +wb = xlsx_package.workbook + +wb.add_worksheet(name: 'Test') do |sheet| + sheet.add_row ['one', 'two', 'three'] + sheet.add_row ['a', 'b', 'c'] +end diff --git a/spec/rails_app/config/routes.rb b/spec/rails_app/config/routes.rb index 9d477a2..c30fdbb 100644 --- a/spec/rails_app/config/routes.rb +++ b/spec/rails_app/config/routes.rb @@ -7,5 +7,6 @@ resources :render_template, only: :show resource :respond_to, only: :show, controller: :respond_to resource :respond_with, only: :show, controller: :respond_with + resource :prepended_module, only: :show, controller: :prepended_module end end diff --git a/spec/rails_app/spec/requests/prepended_module_controller_spec.rb b/spec/rails_app/spec/requests/prepended_module_controller_spec.rb new file mode 100644 index 0000000..007f488 --- /dev/null +++ b/spec/rails_app/spec/requests/prepended_module_controller_spec.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Regression test: on Rails >= 8.0, render option normalization runs AFTER +# custom renderers (rails/rails moved it from _normalize_render into +# render_to_body), so the renderer's own template/prefixes defaulting is +# actually exercised. Computing prefixes from `ancestors.take_while` yields [] +# when a module is prepended to the controller, so the template was looked up +# at the view-paths root and raised ActionView::MissingTemplate. +describe Examples::PrependedModuleController do + it 'resolves the template although a module is prepended to the controller' do + visit '/examples/prepended_module.xlsx' + + expect(page.response_headers['Content-Type']).to start_with 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' + validate_xlsx_file(page.source) + end +end