Troubleshooting

This page collects surprising behaviors you may run into with Loofah, why they happen (traced to the source code), and what to do about them. Each entry follows the pattern symptom → cause → fix.

Setup and Parsing

NotImplementedError: Loofah::HTML5 is not supported by your version of Nokogiri

Cause: Loofah.html5_support? is false: Nokogiri is 1.14.0 or older, or you are on JRuby, where Nokogiri has no Gumbo parser. Note that exactly 1.14.0 counts as unsupported.

Fix: Upgrade Nokogiri on CRuby. On JRuby, use the html4_* methods. For code that must run on both, branch on Loofah.html5_support?.

My output is wrapped in <html><head></head><body>

Cause: You parsed a snippet with html5_document / html4_document, which always builds a full document, and then called to_s on the document.

Fix: Use html5_fragment for snippets. If you really need a document, serialize just the body: doc.at_css("body").inner_html.

ArgumentError when passing options to a fragment method

Cause: Loofah replaces the fragment classes' parse with parse(tags, encoding = nil) for HTML and parse(tags) for XML. Nokogiri's extra fragment arguments (context, parse options, block) are not accepted.

Fix: Pass only the markup and, for HTML, an encoding name. If you need Nokogiri's fragment options, parse with Nokogiri and copy the nodes into a Loofah document.

ArgumentError: unexpected class: MyFragment

Cause: You subclassed Loofah::HTML5::DocumentFragment or Loofah::HTML4::DocumentFragment. The fragment parse method picks the matching document class by comparing self with exactly those two classes, and raises for anything else.

Fix: Do not subclass the fragment classes; wrap them or add behavior with modules instead.

undefined method 'scrub!' on a node

Cause: The node came from a plain Nokogiri document. scrub! is added to nodes only through the decorators of a Loofah document.

Fix: Parse with a Loofah.*_document or Loofah.*_fragment method.

Sanitizer Output

Script source shows up as text after sanitizing

Cause: :strip (and Loofah::Helpers.sanitize, which uses it) removes unsafe tags but keeps their content. <script>alert('evil')</script> becomes the text alert('evil').

Fix: Use :prune, which removes unsafe elements together with their content.

There are three possible causes:

  1. The URI was rejected. allowed_uri? removes href values with schemes outside the protocol list (javascript:, vbscript:, file:, custom app schemes, feed: since 2.25.2) and data: URIs with disallowed media types. Check with Loofah::HTML5::Scrub.allowed_uri?(url).
  2. You used :whitewash, which removes every attribute. Use :prune or :strip instead.
  3. SVG references. On SVG elements such as use, href and xlink:href must point inside the document (#id).

style attributes disappear or are shorter than expected

Cause: scrub_css drops declarations with url(...), disallowed properties (position, z-index, transform, opacity, ...), disallowed functions, and, for background*, border*, margin*, padding*, keywords that are not on the keyword or color list. If nothing is left, the empty style attribute is removed.

Fix: Test the declaration with Loofah::HTML5::Scrub.scrub_css(...) to see what survives. If you need more CSS, sanitize styles yourself in a custom scrubber, or use a sanitizer that lets you configure allowed properties. See URI and CSS Sanitization.

CSS output has no spaces: color:red;font-size:12px;

Cause: scrub_css re-serializes each declaration as name:value; with no space after the colon or between declarations. Whitespace inside values is preserved (collapsed to single spaces).

Fix: None needed; browsers parse it identically. Do not compare sanitized CSS against hand-formatted strings in tests.

Boolean attributes like disabled or checked vanish

Cause: After attribute checks, any remaining attribute (except data-*) whose value is empty or whitespace-only is removed. A boolean attribute written without a value is reported by the parser with an empty value, so it is dropped. This follows from the implementation of scrub_attributes.

Fix: If you need these attributes, write them with a value (disabled="disabled"), or restore them with a custom scrubber.

Forms survive Loofah::Helpers.sanitize

Cause: form, input, button, select, and textarea are on the safelist. The helper removes only <form> elements at the top level of the fragment (XPath ./form), so a form nested in a <div> is kept.

Fix: Remove form elements with a custom pruning scrubber (recipe).

HTML comments become visible text

Cause: With :escape, any node that is not an allowed element, text, or CDATA is replaced by its serialized markup as text. Comments therefore show up as <!-- ... --> text on the page. (With :strip, :prune, and :whitewash, comments are removed.)

Fix: Run :prune first or remove comments with a custom scrubber before :escape.

data-* attributes are kept with dangerous-looking values

Cause: Attributes matching /\Adata-[\w-]+\z/ are kept with no value check, by design.

Fix: Treat data-* values as untrusted in your JavaScript, or remove them with a custom scrubber.

Special characters inside <style> are escaped

Cause: With the HTML4 parser on CRuby, <style> and <script> content becomes CDATA, and Loofah escapes <, >, and & in CDATA to prevent HTML4/HTML5 parser-mismatch attacks. The project's decision record accepts this side effect.

Fix: Use the HTML5 parser, where the issue does not arise in the same way.

Results differ between machines

Cause: Parsing depends on Nokogiri's parser, which differs between CRuby (libxml2 or Gumbo) and JRuby, and between libxml2 versions. The project's tests contain several version-specific expectations, for example for namespaced tags under :whitewash and for comments inside attributes.

Fix: Prefer the HTML5 parser and pin Nokogiri in your lockfile. In tests, compare parsed structures rather than exact strings when possible.

href values have %20 and %22 in them

Cause: On CRuby, after scrubbing, spaces and double quotes in href, action, src, and name (on <a>) are percent-encoded to work around a libxml2 serialization issue (CVE-2018-8048).

Fix: This is intentional. Decode the URL if you need the original form for display.

Built-in Transformations

scrub!("prune") raises Loofah::ScrubberNotFound

Cause: Built-in scrubbers are looked up by symbol in Loofah::Scrubbers::MAP.

Fix: Use scrub!(:prune), or convert with name.to_sym after checking Loofah::Scrubbers.scrubber_symbols.include?(name.to_sym).

:unprintable did not remove a control character

Cause: It removes only U+2028 and U+2029, and only from text and CDATA nodes.

Fix: Write a custom scrubber with your own character class for other characters.

Cause: Links without href, and links whose href starts with #, are intentionally skipped (since 2.24.0). Existing target values on other links are replaced.

:double_breakpoint did nothing on my fragment

Cause: In 2.25.2 and earlier, :double_breakpoint left document fragments unchanged. The fix is recorded in the CHANGELOG's unreleased section.

Fix: Upgrade to a release that includes the fix, or parse a document and scrub it.

:double_breakpoint removed my paragraph's class

Cause: New paragraphs are created without attributes. This also happens to paragraphs that are only rebuilt (two non-adjacent <br> children) without being split.

Fix: Copy attributes in your own scrubber, or run :double_breakpoint before adding classes.

Text Output

text returns &amp; and &lt;

Cause: text escapes special characters by default so the result is HTML-safe.

Fix: For non-HTML destinations, use text(encode_special_chars: false) or to_text(encode_special_chars: false). Loofah::Helpers.strip_tags always escapes.

"prefix " + fragment gives text instead of HTML

Cause: Loofah HTML documents and fragments define to_str as an alias of text, so Ruby converts them implicitly to their escaped text.

Fix: Call to_s explicitly: "prefix " + fragment.to_s.

text on a document ignores the <title>

Cause: Document text reads from /html/body only.

Fix: Read doc.at_css("title")&.text separately.

to_text puts table cells on separate lines

Cause: td, th, and tr are in Loofah::Elements::LOOSE_BLOCK_LEVEL, so each becomes its own line.

Fix: For tabular text, walk the table with Nokogiri yourself.

Getting Help

See Also