jdx/mise · error

Tag closing loop reached loop limit (%d). Consider either in

Error message

Tag closing loop reached loop limit (%d). Consider either increasing it or checking HTML-code for syntax errors

What it means

This error is thrown by the embedded htmlparser Lua library (used by vfox plugins that parse HTML, e.g. to scrape version lists from a website) when its TagCloseLoop, the loop that matches closing tags back to their opened counterparts, iterates `limit` times without finishing. It is a safety valve against runaway parsing caused by malformed or pathological HTML: unclosed tags, mismatched close names, or documents far larger than the loop budget allow. The parser aborts rather than looping forever, so any plugin call that triggers an HTML parse of such input fails with this message.

Source

Thrown at crates/vfox/lua/htmlparser.lua:228

		end
		-- }}}
		if voidelements[tag.name:lower()] then -- {{{
			descend = false
			tag:close()
		else
			descend = true
			opentags[tag.name] = opentags[tag.name] or {}
			table.insert(opentags[tag.name], tag)
		end
		-- }}}
		local closeend = tpos
		local closingloop
		while true do -- TagCloseLoop {{{
			-- Can't remember why did I add that, so comment it for now (and not remove), in case it will be needed again
			-- (although, it causes #59 and #60, so it will anyway be needed to rework)
			-- if voidelements[tag.name:lower()] then break end -- already closed
			if closingloop == limit then
				err("Tag closing loop reached loop limit (%d). Consider either increasing it or checking HTML-code for syntax errors", limit)
				break
			end

			local closestart, closing, closename
			closestart, closeend, closing, closename = root._text:find("[^<]*<(/?)([%w-]+)", closeend)
			dbg("[TagCloseLoop]:#LINE# closestart=%s || closeend=%s || closing=%s || closename=%s",str(closestart),str(closeend),str(closing),str(closename))

			if not closing or closing == "" then break end

			tag = table.remove(opentags[closename] or {}) or tag -- kludges for the cases of closing void or non-opened tags
			closestart = root._text:find("<", closestart)
			dbg("[TagCloseLoop]:#LINE# closestart=%s",str(closestart))
			tag:close(closestart, closeend + 1)
			node = tag.parent
			descend = true
			closingloop = (closingloop or 0) + 1
		end -- }}}
	end -- }}}

View on GitHub (pinned to afd2eddd3a)

Solutions

  1. Inspect the HTML being parsed (log `root._text` or fetch the page yourself) and fix the malformed markup: close opened tags, remove stray `</...>` closers, ensure tag-name case matches.
  2. Increase the `limit` argument passed to the HTML parser call (the loop budget) if the input is legitimately large but well-formed.
  3. Update the vfox plugin to a newer version that tolerates the upstream page's current HTML, or patch the plugin's parsing logic/regex to match the changed markup.
  4. If the page markup is beyond repair, replace HTML scraping in the plugin with a stable API/JSON endpoint or a stricter parsing approach.
  5. Report the issue to the plugin maintainer (or htmlparser upstream, issues #59/#60 discuss this loop) with the failing URL/input.

Example fix

// before (plugin calls parser with default limit on a huge page)
local root = htmlparser.parse(body)

// after (raise the loop limit for large documents)
local root = htmlparser.parse(body, nil, nil, 5000) -- limit high enough for page size
Defensive patterns

Strategy: validation

Validate before calling

-- validate before parsing: balance check for the tags the parser will consume
local function count_balance(html)
  local opens, closes = 0, 0
  for _, sl, name in html:gmatch("<(%/?)([%w-]+)") do
    if sl == "/" then closes = closes + 1 else opens = opens + 1 end
  end
  return opens == closes, opens, closes
end
local ok = count_balance(body)
if not ok then error("unbalanced HTML tags; refusing to parse") end

Type guard

-- Lua has no static types; assert parseability instead
local function is_parseable(html, limit)
  if type(html) ~= "string" or #html == 0 then return false end
  local closes = 0
  for _ in html:gmatch("<(/)([%w-]+)") do closes = closes + 1 end
  return closes < (limit or 100) -- under the TagCloseLoop budget
end

Try / catch

-- wrap the parse call; htmlparser's err() aborts the chunk, so guard at the caller
local ok, root = pcall(function() return htmlparser.parse(body, nil, nil, larger_limit) end)
if not ok then
  log("HTML parse failed: " .. tostring(root))
  return fallback_versions
end

Prevention

When it happens

Trigger: Calling a vfox plugin function that parses HTML (via htmlparser.lua's `parse`) where the document contains many consecutive closing tags without matching opens, mismatched tag names (`<Div>` closed by `</div>` in a way the kludge path mishandles), stray `</tag>` closers for void or never-opened tags, or simply more closing-tag iterations than the parser's `limit` argument permits.

Common situations: Plugin authors scraping a tool's download/releases page whose HTML changed upstream (new markup, broken or unclosed tags); malformed snippets fed to the parser in tests; very large HTML pages where the default loop limit is too small; copying HTML that contains comments or text with `</`-looking sequences the simplistic regex `[^<]*<(/?)([%w-]+)` misreads as closers.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/ba870f0bf35e2b39. Report an issue: GitHub.