symfony/css-selector · error · SyntaxErrorException

Unclosed/invalid string at

Error message

Unclosed/invalid string at %s.

What it means

StringHandler::handle() tokenizes quoted strings in selector input. After matching an opening quote, it validates the string is properly closed: if the match reaches the end of the remaining input, SyntaxErrorException::unclosedString() is thrown because the closing quote is missing. The %s in the raw message is the reader position where the string started.

Solutions

  1. Add the missing closing quote to the string literal in the selector.
  2. Escape or strip embedded quotes when interpolating values into selectors (e.g. remove " characters from attribute values).
  3. Validate selector strings with a try/catch around cssToXPath() before using them in production paths.

Example fix

// before
$css = "a[href=\"example]"; // unclosed string

// after
$css = "a[href=\"example\"]"; // properly closed
Defensive patterns

Strategy: validation

Validate before calling

// ensure quotes are balanced before handing the selector to the parser
function quotesBalanced(string $css): bool {
    $dq = substr_count($css, '"'); $sq = substr_count($css, "'");
    return $dq % 2 === 0 && $sq % 2 === 0;
}
if (!quotesBalanced($css)) { throw new \InvalidArgumentException('Unbalanced quotes in selector'); }

Try / catch

use Symfony\Component\CssSelector\Exception\SyntaxErrorException;
try {
    $xpath = $translator->cssToXPath($css);
} catch (SyntaxErrorException $e) {
    if (str_contains($e->getMessage(), 'Unclosed string') || str_contains($e->getMessage(), 'unclosedString')) {
        // report bad selector input to caller
    }
    throw $e;
}

Prevention

When it happens

Trigger: Passing a selector string containing an unterminated quoted literal to cssToXPath()/parse(), e.g. "a[href=\"foo\"" missing the final quote — the string token runs to end-of-input.

Common situations: Dynamically built selector strings where interpolated values contained quotes that broke the literal; user-supplied CSS selectors copied incompletely; template concatenation dropping a closing quote.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of symfony/css-selector@08e2905152 (2026-09-14). Data as JSON: /api/errors/513da725dd341616. Report an issue: GitHub.

Appendix: source

Thrown at Parser/Handler/StringHandler.php:57

    public function handle(Reader $reader, TokenStream $stream): bool
    {
        $quote = $reader->getSubstring(1);

        if (!\in_array($quote, ["'", '"'], true)) {
            return false;
        }

        $reader->moveForward(1);
        $match = $reader->findPattern($this->patterns->getQuotedStringPattern($quote));

        if (!$match) {
            throw new InternalErrorException(\sprintf('Should have found at least an empty match at %d.', $reader->getPosition()));
        }

        // check unclosed strings
        if (\strlen($match[0]) === $reader->getRemainingLength()) {
            throw SyntaxErrorException::unclosedString($reader->getPosition() - 1);
        }

        // check quotes pairs validity
        if ($quote !== $reader->getSubstring(1, \strlen($match[0]))) {
            throw SyntaxErrorException::unclosedString($reader->getPosition() - 1);
        }

        $string = $this->escaping->escapeUnicodeAndNewLine($match[0]);
        $stream->push(new Token(Token::TYPE_STRING, $string, $reader->getPosition()));
        $reader->moveForward(\strlen($match[0]) + 1);

        return true;
    }
}

View on GitHub (pinned to 08e2905152)