Summary
With config.intercept_erb = true, ReactionView registers Herb as the :erb handler for every template, including third-party gem views. Herb's engine cannot compile some gem templates that erubi compiles fine — and the failure is silent and destructive. Depending on validation_mode, the page renders empty (:none), shows the parser-error overlay (:overlay — the default outside test, so it hits production), or raises (:raise). No value of validation_mode renders the template; it only selects the failure mode.
I originally filed this as a validation_mode scoping issue — that was wrong. Setting :none doesn't render the template, it just produces a blank page. The real problem is interception (below).
Evidence (rails_pulse 0.3.3 dashboard layout)
Compiling rails_pulse's app/views/layouts/rails_pulse/application.html.erb through each path:
| handler / mode |
compiled .src |
renders? |
Herb :none |
15 chars |
❌ empty |
Herb :overlay |
14,740 chars |
❌ (overlay HTML) |
Herb :raise |
— |
❌ Herb::Engine::CompilationError |
| erubi |
5,130 chars |
✅ yes |
Herb cannot produce working output for this template; erubi can. The template ships and works in every non-ReactionView app — Herb's stricter HTML parser rejects it (a <div> it considers unclosed across an <% if %> scope).
The problem
intercept_erb applies Herb to templates the host app doesn't own and can't fix. An app can lint/format its own templates, but it has no control over a gem's. So any gem view Herb can't compile takes the whole page down — in production, with the default validation_mode. Interception should be limited to the app's own templates and fall back to erubi for everything else.
Suggested fix
Gate interception on whether the template belongs to the host app, falling back to the stock erubi handler otherwise:
def call(template, source)
if template.format == :html && ReActionView.config.intercept_erb && app_owned?(template)
::ReActionView::Template::Handlers::Herb.call(template, source)
else
super # erubi
end
end
Caveat — the existing local_template? is NOT a correct predicate for this. It uses:
template.identifier.start_with?(Rails.root.to_s)
That misclassifies vendored gems. In CI, and in any deployment build that bundles to vendor/bundle (or bundle --path), gems live under Rails.root, so their views pass start_with?(Rails.root) and are still sent to Herb — reintroducing the exact failure. (I hit this directly: a fix using start_with?(Rails.root) passed locally, where gems sit outside Rails.root, then broke in CI where they're vendored under it.)
The predicate must exclude gem/engine paths. Two robust options:
- Scope to the app's own view roots:
identifier.start_with?("#{Rails.root}/app/") — vendored gems under vendor/ are excluded.
- Or exclude anything under a gem path:
Gem.loaded_specs.values.none? { |s| identifier.start_with?(s.full_gem_path) }.
local_template? is also used to gate the DebugVisitor / dev-tools markup, so it has the same vendored-bundle blind spot there (benign, but worth fixing in one place).
Workaround (host app)
Reopen ReActionView::Template::Handlers::ERB and gate #call on an app-owned check that excludes vendored gems:
ReActionView::Template::Handlers::ERB.class_eval do
def call(template, source)
identifier = template.identifier.to_s if template.respond_to?(:identifier)
local = identifier&.start_with?("#{Rails.root}/app/")
if template.format == :html && ReActionView.config.intercept_erb && local
::ReActionView::Template::Handlers::Herb.call(template, source)
else
super # stock erubi handler
end
end
end
Environment
reactionview 0.3.0 · herb 0.10.1 · Rails 8.1.3 · Ruby 3.4.7
Summary
With
config.intercept_erb = true, ReactionView registers Herb as the:erbhandler for every template, including third-party gem views. Herb's engine cannot compile some gem templates that erubi compiles fine — and the failure is silent and destructive. Depending onvalidation_mode, the page renders empty (:none), shows the parser-error overlay (:overlay— the default outsidetest, so it hits production), or raises (:raise). No value ofvalidation_moderenders the template; it only selects the failure mode.Evidence (rails_pulse 0.3.3 dashboard layout)
Compiling
rails_pulse'sapp/views/layouts/rails_pulse/application.html.erbthrough each path:.src:none:overlay:raiseHerb::Engine::CompilationErrorHerb cannot produce working output for this template; erubi can. The template ships and works in every non-ReactionView app — Herb's stricter HTML parser rejects it (a
<div>it considers unclosed across an<% if %>scope).The problem
intercept_erbapplies Herb to templates the host app doesn't own and can't fix. An app can lint/format its own templates, but it has no control over a gem's. So any gem view Herb can't compile takes the whole page down — in production, with the defaultvalidation_mode. Interception should be limited to the app's own templates and fall back to erubi for everything else.Suggested fix
Gate interception on whether the template belongs to the host app, falling back to the stock erubi handler otherwise:
Caveat — the existing
local_template?is NOT a correct predicate for this. It uses:That misclassifies vendored gems. In CI, and in any deployment build that bundles to
vendor/bundle(orbundle --path), gems live underRails.root, so their views passstart_with?(Rails.root)and are still sent to Herb — reintroducing the exact failure. (I hit this directly: a fix usingstart_with?(Rails.root)passed locally, where gems sit outsideRails.root, then broke in CI where they're vendored under it.)The predicate must exclude gem/engine paths. Two robust options:
identifier.start_with?("#{Rails.root}/app/")— vendored gems undervendor/are excluded.Gem.loaded_specs.values.none? { |s| identifier.start_with?(s.full_gem_path) }.local_template?is also used to gate theDebugVisitor/ dev-tools markup, so it has the same vendored-bundle blind spot there (benign, but worth fixing in one place).Workaround (host app)
Reopen
ReActionView::Template::Handlers::ERBand gate#callon an app-owned check that excludes vendored gems:Environment
reactionview 0.3.0 · herb 0.10.1 · Rails 8.1.3 · Ruby 3.4.7