这是indexloc提供的服务,不要输入任何密码
Skip to content

TypeError: element.hasAttribute is not a function in HeadSnapshot during prefetch #1472

@oakbow

Description

@oakbow

Bug Description

When hovering over links with Turbo prefetch enabled, a TypeError occurs in HeadSnapshot.detailsByOuterHTML when processing the <head> element.

Error Message

TypeError: e.hasAttribute is not a function
  at elementWithoutNonce (turbo.es2017-esm.js:3763)
  at Array.map (<anonymous>)
  at HeadSnapshot.detailsByOuterHTML (turbo.es2017-esm.js:3660)

Turbo Version

  • v8.0.20 (latest, released 3 weeks ago)

Environment

  • Browser: Chrome 142.0.7444.176
  • Framework: Ruby on Rails 7.1 with Turbo 8.0.20
  • Occurs in all environments: production, staging, development, and test
  • Particularly problematic in test environment where it causes flaky tests

Steps to Reproduce

  1. Create a page with a <head> element containing HTML comments or ERB comments:
<head>
  <script>...</script>
  <!-- Some HTML comment -->
  <%= content %>
</head>
  1. Enable Turbo prefetch on navigation links
  2. Hover over a link to trigger prefetch
  3. Check browser console for the TypeError

Root Cause

The bug is in src/core/drive/head_snapshot.js:

Problem Location 1: detailsByOuterHTML property (L3-22)

detailsByOuterHTML = this.children
  .filter((element) => !elementIsNoscript(element))
  .map((element) => elementWithoutNonce(element))  // ← TypeError occurs here
  .reduce((result, element) => {
    // ...
  }, {})

Problem Location 2: elementWithoutNonce function (L107-113)

function elementWithoutNonce(element) {
  if (element.hasAttribute("nonce")) {  // ← Assumes element is always an Element
    element.setAttribute("nonce", "")
  }
  return element
}

Why it happens:

  1. this.children can include non-Element nodes (text nodes, comment nodes)
  2. The .filter() only removes <noscript> elements, not other node types
  3. Text nodes and comment nodes don't have hasAttribute() method
  4. Particularly, HTML comments (<!-- -->) and ERB comments (<%# %>) in <head> trigger this error

Expected Behavior

HeadSnapshot should safely handle all node types in <head> element without throwing errors.

Proposed Fix

Add a type check before calling elementWithoutNonce:

detailsByOuterHTML = this.children
  .filter((element) => !elementIsNoscript(element))
  .filter((element) => element instanceof Element)  // ← Add this line
  .map((element) => elementWithoutNonce(element))
  .reduce((result, element) => {
    // ...
  }, {})

Or alternatively, add defensive check in elementWithoutNonce:

function elementWithoutNonce(element) {
  if (element instanceof Element && element.hasAttribute("nonce")) {
    element.setAttribute("nonce", "")
  }
  return element
}

Workaround

We've implemented a client-side patch that removes non-Element nodes from <head> before Turbo processes it:

const cleanupHeadElement = () => {
  if (!document.head) return
  
  const nodesToRemove = []
  Array.from(document.head.childNodes).forEach(node => {
    if (node.nodeType !== Node.ELEMENT_NODE) {
      nodesToRemove.push(node)
    }
  })
  
  nodesToRemove.forEach(node => {
    node.parentNode.removeChild(node)
  })
}

// Call before Turbo events
document.addEventListener('turbo:before-visit', cleanupHeadElement)
document.addEventListener('turbo:before-fetch-request', cleanupHeadElement)

Additional Context

  • This appears to be an unreported bug - we couldn't find similar issues in the repository
  • Causes flaky tests in system/feature tests that use Selenium/Capybara with Turbo
  • Affects users across all environments (production, staging, development, test)
  • Error is caught by Sentry in production
  • The error doesn't break navigation completely, but appears in error monitoring and test failures

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions