spectreconsole/spectre.console · error · InvalidOperationException
Encountered closing tag when none was expected near position
Error message
Encountered closing tag when none was expected near position {token.Position}. What it means
In AnsiMarkup.Parse the tokenizer yielded a Close token (a `[/]`) but the style stack is empty, meaning there is no matching opening tag to close. The position reported is the token's position in the source markup.
Source
Thrown at src/Spectre.Console.Ansi/AnsiMarkup.cs:77
var link = default(Link?);
while (tokenizer.MoveNext())
{
var token = tokenizer.Current;
if (token.Kind == MarkupTokenKind.Open)
{
var parsed = AnsiMarkupTagParser.Parse(token.Value);
linkStack.Push(link);
link = parsed.Link ?? link;
styleStack.Push(style.Value);
style = style.Value.Combine(parsed.Style);
}
else if (token.Kind == MarkupTokenKind.Close)
{
if (styleStack.Count == 0)
{
throw new InvalidOperationException(
$"Encountered closing tag when none was expected near position {token.Position}.");
}
style = styleStack.Pop();
link = linkStack.Pop();
}
else if (token.Kind == MarkupTokenKind.Text)
{
if (result.Count > 0 && result[^1].Style.Equals(style) && Equals(result[^1].Link, link))
{
// Merge segments with same style and link
result[^1].Text += token.Value;
}
else
{
result.Add(
new AnsiMarkupSegment(
token.Value, style.Value, link));View on GitHub (pinned to 0acc92fada)
Solutions
- Ensure every `[/]` has a preceding matching `[...]` opener
- Escape literal brackets in dynamic text with AnsiMarkup.Escape(text) before splicing into markup
- Sanitize user input: strip or escape stray `[/]` sequences
- Use try/catch around AnsiMarkup.Parse when rendering untrusted markup
Example fix
// before (stray closing tag)
AnsiMarkup.Parse("Hello[/] world");
// after
AnsiMarkup.Parse("Hello world");
// or, for dynamic text, escape it first:
AnsiMarkup.Parse(AnsiMarkup.Escape(userText)); Defensive patterns
Strategy: validation
Validate before calling
// Check that openers and closers balance before parsing:
public static bool IsMarkupBalanced(string markup)
{
int depth = 0;
for (int i = 0; i < markup.Length; i++)
{
if (markup[i] == '[' && i + 1 < markup.Length && markup[i + 1] != '[') depth++;
else if (markup[i] == ']' && i + 1 < markup.Length && markup[i + 1] == ']') i++; // escaped
else if (markup[i] == ']' && i > 0 && markup[i - 1] == ']') { /* already handled */ }
}
return depth == 0;
} Try / catch
try { var segments = AnsiMarkup.Parse(markup); }
catch (InvalidOperationException ex) when (ex.Message.StartsWith("Encountered closing tag"))
{ /* log and fall back to plain text */ } Prevention
- Escape all dynamic text with AnsiMarkup.Escape before splicing into markup
- Render untrusted strings as plain text rather than markup
- Wrap AnsiMarkup.Parse in try/catch when input provenance is uncertain
When it happens
Trigger: Markup like `"text[/]"`, `"[/]text"`, or any string containing more `[/]` closers than `[...]` openers, passed to AnsiMarkup.Parse or AnsiMarkup.Write.
Common situations: Concatenating markup fragments and losing an opening tag; copy-pasting only the closing half of a styled span; user input containing literal `[/]`; a templating layer that strips `[tag]` but leaves `[/]`.
Related errors
- Unbalanced markup stack. Did you forget to close a tag?
- Encountered malformed markup tag at position {pos}.
- Encountered unknown markup token.
- Encountered unescaped ']' token at position {_reader.Positio
- A link has already been set.
AI-assisted analysis of spectreconsole/spectre.console@0acc92fada (2026-08-13).
Data as JSON: /api/errors/99c32ebf85f7ee0e.
Report an issue: GitHub.