Skip to content

intercept_erb runs gem templates through Herb (can't compile some) → blank page; scope interception to local templates #107

Description

@AliOsm

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions