Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
11 changes: 8 additions & 3 deletions lib/axlsx_rails/action_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions spec/rails_app/config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
18 changes: 18 additions & 0 deletions spec/rails_app/spec/requests/prepended_module_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -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