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
- Add the missing closing quote to the string literal in the selector.
- Escape or strip embedded quotes when interpolating values into selectors (e.g. remove " characters from attribute values).
- 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
- Escape or strip quote characters from dynamic values interpolated into selectors
- Validate user-supplied selectors for balanced quotes before parsing
- Prefer building selectors with addAttributeMatching APIs instead of string concatenation
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
- String not allowed as function argument.
- Unexpected pseudo-element
- Got immediate child pseudo-element
- Got too deeply nested
- Got too deeply nested :has().
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)