Skip to content

Escape unsafe OutputBuffer appends without the super dispatch - #59570

Open
thomasklemm wants to merge 1 commit into
rails:mainfrom
thomasklemm:thomasklemm/output-buffer-escape
Open

thomasklemm wants to merge 1 commit into
rails:mainfrom
thomasklemm:thomasklemm/output-buffer-escape

Conversation

@thomasklemm

@thomasklemm thomasklemm commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Every <%= %> appends through ActionView::OutputBuffer#<<. For a value that isn't html_safe, it calls to_s and html_safe?, then ERB::Util.unwrapped_html_escape, which reaches ERB::Escape.html_escape through an alias and a super on ERB::Util's singleton class. YJIT can't specialize that super, so each append is a dynamic send.

This PR runs the same steps inline: to_s on the converted value, html_safe? again, then ERB::Escape.html_escape, the method super reaches. Every to_s and html_safe? call stays.

There is one difference. Template appends no longer go through ERB::Util, so a module prepended to its singleton class that overrides unwrapped_html_escape, or html_escape before Active Support loads, no longer sees them. Every other caller still goes through it. unwrapped_html_escape is :nodoc:, and #46573 chose super so that such prepends keep working; this keeps that everywhere except template appends.

On Ruby 4.0.7, a <%= %> of a String that isn't html_safe takes 110 ns instead of 141 ns on main in the interpreter, and 52 ns instead of 68 ns with YJIT.

On ruby-bench's erubi-rails this runs 5.8% fewer instructions per iteration in the interpreter and 5–6% fewer with YJIT. A page in Fizzy or Lobsters makes 130 to 920 <%= %> appends.

OutputBuffer#<< calls ERB::Util.unwrapped_html_escape for every value that
isn't html_safe, which reaches ERB::Escape.html_escape through an alias and
super. Every <%= %> in an ERB template appends this way.

Run the same steps inline instead: call to_s on the converted value, check
html_safe? again, and otherwise call ERB::Escape.html_escape, the method
that super reaches. Every to_s and html_safe? call stays as it was.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant