{"record":{"id":"ba870f0bf35e2b39","repo":"jdx/mise","slug":"tag-closing-loop-reached-loop-limit-d-consider","errorCode":null,"errorMessage":"Tag closing loop reached loop limit (%d). Consider either increasing it or checking HTML-code for syntax errors","messagePattern":"Tag closing loop reached loop limit \\((.+?)\\)\\. Consider either increasing it or checking HTML-code for syntax errors","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/vfox/lua/htmlparser.lua","lineNumber":228,"sourceCode":"\t\tend\n\t\t-- }}}\n\t\tif voidelements[tag.name:lower()] then -- {{{\n\t\t\tdescend = false\n\t\t\ttag:close()\n\t\telse\n\t\t\tdescend = true\n\t\t\topentags[tag.name] = opentags[tag.name] or {}\n\t\t\ttable.insert(opentags[tag.name], tag)\n\t\tend\n\t\t-- }}}\n\t\tlocal closeend = tpos\n\t\tlocal closingloop\n\t\twhile true do -- TagCloseLoop {{{\n\t\t\t-- Can't remember why did I add that, so comment it for now (and not remove), in case it will be needed again\n\t\t\t-- (although, it causes #59 and #60, so it will anyway be needed to rework)\n\t\t\t-- if voidelements[tag.name:lower()] then break end -- already closed\n\t\t\tif closingloop == limit then\n\t\t\t\terr(\"Tag closing loop reached loop limit (%d). Consider either increasing it or checking HTML-code for syntax errors\", limit)\n\t\t\t\tbreak\n\t\t\tend\n\n\t\t\tlocal closestart, closing, closename\n\t\t\tclosestart, closeend, closing, closename = root._text:find(\"[^<]*<(/?)([%w-]+)\", closeend)\n\t\t\tdbg(\"[TagCloseLoop]:#LINE# closestart=%s || closeend=%s || closing=%s || closename=%s\",str(closestart),str(closeend),str(closing),str(closename))\n\n\t\t\tif not closing or closing == \"\" then break end\n\n\t\t\ttag = table.remove(opentags[closename] or {}) or tag -- kludges for the cases of closing void or non-opened tags\n\t\t\tclosestart = root._text:find(\"<\", closestart)\n\t\t\tdbg(\"[TagCloseLoop]:#LINE# closestart=%s\",str(closestart))\n\t\t\ttag:close(closestart, closeend + 1)\n\t\t\tnode = tag.parent\n\t\t\tdescend = true\n\t\t\tclosingloop = (closingloop or 0) + 1\n\t\tend -- }}}\n\tend -- }}}","sourceCodeStart":210,"sourceCodeEnd":246,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/crates/vfox/lua/htmlparser.lua#L210-L246","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","Increase the `limit` argument passed to the HTML parser call (the loop budget) if the input is legitimately large but well-formed.","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.","If the page markup is beyond repair, replace HTML scraping in the plugin with a stable API/JSON endpoint or a stricter parsing approach.","Report the issue to the plugin maintainer (or htmlparser upstream, issues #59/#60 discuss this loop) with the failing URL/input."],"exampleFix":"// before (plugin calls parser with default limit on a huge page)\nlocal root = htmlparser.parse(body)\n\n// after (raise the loop limit for large documents)\nlocal root = htmlparser.parse(body, nil, nil, 5000) -- limit high enough for page size","handlingStrategy":"validation","validationCode":"-- validate before parsing: balance check for the tags the parser will consume\nlocal function count_balance(html)\n  local opens, closes = 0, 0\n  for _, sl, name in html:gmatch(\"<(%/?)([%w-]+)\") do\n    if sl == \"/\" then closes = closes + 1 else opens = opens + 1 end\n  end\n  return opens == closes, opens, closes\nend\nlocal ok = count_balance(body)\nif not ok then error(\"unbalanced HTML tags; refusing to parse\") end","typeGuard":"-- Lua has no static types; assert parseability instead\nlocal function is_parseable(html, limit)\n  if type(html) ~= \"string\" or #html == 0 then return false end\n  local closes = 0\n  for _ in html:gmatch(\"<(/)([%w-]+)\") do closes = closes + 1 end\n  return closes < (limit or 100) -- under the TagCloseLoop budget\nend","tryCatchPattern":"-- wrap the parse call; htmlparser's err() aborts the chunk, so guard at the caller\nlocal ok, root = pcall(function() return htmlparser.parse(body, nil, nil, larger_limit) end)\nif not ok then\n  log(\"HTML parse failed: \" .. tostring(root))\n  return fallback_versions\nend","preventionTips":["Validate fetched HTML for balanced open/close tags before parsing.","Raise the parser's limit argument proportionally to document size.","Pin and periodically update plugins so scraping logic tracks upstream page changes.","Prefer JSON/API endpoints over HTML scraping in vfox plugins.","Test plugins against cached snapshots of the real pages they scrape to catch upstream markup changes early."],"tags":["lua","html-parsing","vfox-plugin","loop-limit","malformed-html"],"backgroundTag":"internal-invariant-violation","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}