abpframework/abp · error · FormatException

Incorrect syntax at char {i}! format string can not contain

Error message

Incorrect syntax at char {i}! format string can not contain nested dynamic value expression!

What it means

FormatStringTokenizer.Tokenize splits a format string into ConstantText and DynamicValue tokens, where dynamic segments are delimited by { and }. When a '{' character is encountered while already inside a dynamic segment (inDynamicValue == true), the tokenizer throws FormatException because nested dynamic placeholders are unsupported. This tokenizer powers FormattedStringValueExtracter.Extract, which reverses string.Format to pull named values out of a target string (e.g. format "My name is {name}." matched against "My name is Neo.").

Source

Thrown at framework/src/Volo.Abp.Core/Volo/Abp/Text/Formatting/FormatStringTokenizer.cs:24

internal class FormatStringTokenizer
{
    public List<FormatStringToken> Tokenize(string format, bool includeBracketsForDynamicValues = false)
    {
        var tokens = new List<FormatStringToken>();

        var currentText = new StringBuilder();
        var inDynamicValue = false;

        for (var i = 0; i < format.Length; i++)
        {
            var c = format[i];
            switch (c)
            {
                case '{':
                    if (inDynamicValue)
                    {
                        throw new FormatException("Incorrect syntax at char " + i + "! format string can not contain nested dynamic value expression!");
                    }

                    inDynamicValue = true;

                    if (currentText.Length > 0)
                    {
                        tokens.Add(new FormatStringToken(currentText.ToString(), FormatStringTokenType.ConstantText));
                        currentText.Clear();
                    }

                    break;
                case '}':
                    if (!inDynamicValue)
                    {
                        throw new FormatException("Incorrect syntax at char " + i + "! These is no opening brackets for the closing bracket }.");
                    }

                    inDynamicValue = false;

View on GitHub (pinned to 7ed43b1931)

Solutions

  1. Flatten the format so each dynamic segment contains only its name and no inner braces: "a{b}" instead of "a{b{c}}".
  2. If a literal brace is needed in constant text, that is unsupported by this tokenizer — keep braces out of constant text and restructure the template.
  3. Validate the format string with a brace-balance/nesting check before calling Extract, and reject malformed templates at config load time.
  4. Wrap Extract in a try/catch(FormatException) to degrade gracefully when the format comes from an untrusted source.

Example fix

// before: nested braces inside a dynamic segment
var r = FormattedStringValueExtracter.Extract(input, "doc-{id{rev}}.pdf");
// throws FormatException at the inner '{'

// after: single, non-nested dynamic segment
var r = FormattedStringValueExtracter.Extract(input, "doc-{id}.pdf");
Defensive patterns

Strategy: try-catch

Validate before calling

static bool IsValidFormatString(string format)
{
    if (format.IsNullOrEmpty()) return true;
    var depth = 0;
    foreach (var c in format)
    {
        if (c == '{') { if (depth > 0) return false; depth++; }
        else if (c == '}') { if (depth == 0) return false; depth--; }
    }
    return depth == 0;
}

// usage:
if (!IsValidFormatString(format)) { /* reject or sanitize */ }

Try / catch

ExtractionResult result;
try
{
    result = FormattedStringValueExtracter.Extract(str, format, ignoreCase);
}
catch (FormatException ex)
{
    logger.LogDebug(ex, "Malformed format string: {Format}", format);
    result = new ExtractionResult(false);
}

Prevention

When it happens

Trigger: Calling FormattedStringValueExtracter.Extract(str, format) (or IsMatch) with a format containing an opening brace inside an already-open dynamic segment: "a{b{c}}", "{x{y}}", or "{0{1}}" (the test case at FormattedStringTokenizer_Test.cs:14). Any second '{' before the matching '}' triggers it.

Common situations: User-supplied or config-driven format templates that accidentally nest placeholders; copying a string.Format numeric-index style ("{0}") into a template and then embedding another token; feeding route-like patterns containing literal braces without realizing braces are always interpreted as dynamic delimiters (there is no escape sequence).

Related errors


AI-assisted analysis of abpframework/abp@7ed43b1931 (2026-08-13). Data as JSON: /api/errors/0a8961081b942ad8. Report an issue: GitHub.