microsoft/playwright · error · InvalidSelectorError
Malformed selector: ${part.name}=${part.body}
Error message
Malformed selector: ${part.name}=${part.body} What it means
Thrown while parsing the body of a nested selector engine (internal:has, internal:has-not, internal:and, internal:or, internal:chain, left-of, right-of, above, below, near). The body must be a JSON array of the form ["innerSelector"] or ["innerSelector", distance]. This specific throw fires when JSON parsed but the value is not an array, has wrong length (<1 or >2), or its first element is not a string.
Source
Thrown at packages/isomorphic/selectorParser.ts:65
for (const part of parsedStrings.parts) {
if (part.name === 'css' || part.name === 'css:light') {
if (part.name === 'css:light')
part.body = ':light(' + part.body + ')';
const parsedCSS = parseCSS(part.body, customCSSNames);
parts.push({
name: 'css',
body: parsedCSS.selector,
source: part.body
});
continue;
}
if (kNestedSelectorNames.has(part.name)) {
let innerSelector: string;
let distance: number | undefined;
try {
const unescaped = JSON.parse('[' + part.body + ']');
if (!Array.isArray(unescaped) || unescaped.length < 1 || unescaped.length > 2 || typeof unescaped[0] !== 'string')
throw new InvalidSelectorError(`Malformed selector: ${part.name}=` + part.body);
innerSelector = unescaped[0];
if (unescaped.length === 2) {
if (typeof unescaped[1] !== 'number' || !kNestedSelectorNamesWithDistance.has(part.name))
throw new InvalidSelectorError(`Malformed selector: ${part.name}=` + part.body);
distance = unescaped[1];
}
} catch (e) {
throw new InvalidSelectorError(`Malformed selector: ${part.name}=` + part.body);
}
const nested = { name: part.name, source: part.body, body: { parsed: parseSelector(innerSelector), distance } };
const lastFrame = [...nested.body.parsed.parts].reverse().find(part => part.name === 'internal:control' && part.body === 'enter-frame');
const lastFrameIndex = lastFrame ? nested.body.parsed.parts.indexOf(lastFrame) : -1;
// Allow nested selectors to start with the same frame selector.
if (lastFrameIndex !== -1 && selectorPartsEqual(nested.body.parsed.parts.slice(0, lastFrameIndex + 1), parts.slice(0, lastFrameIndex + 1)))
nested.body.parsed.parts.splice(0, lastFrameIndex + 1);
parts.push(nested);
continue;
}View on GitHub (pinned to c8fc3bf8d3)
Solutions
- Use the high-level locator API: locator.has(child), locator.and(other), locator.or(other) — Playwright builds the body correctly.
- If authoring raw, wrap the inner selector as a JSON array string: internal:has=["div >> span"].
- Validate the body is a JSON array with a string first element before constructing the selector.
Example fix
// before
await page.locator('div >> internal:has=span').click();
// after
await page.locator('div').locator(page.locator('span')).click();
// or first().filter({ has: page.locator('span') }) Defensive patterns
Strategy: try-catch
Validate before calling
function isValidNestedBody(body: string): boolean {
try {
const a = JSON.parse('[' + body + ']');
return Array.isArray(a) && a.length >= 1 && a.length <= 2 && typeof a[0] === 'string';
} catch { return false; }
} Try / catch
try { await page.locator(sel).click(); }
catch (e) { if (isInvalidSelectorError(e) && /Malformed selector/.test(e.message)) handleBadNested(sel); else throw e; } Prevention
- Use locator.has()/filter()/and()/or() instead of hand-writing nested engine bodies.
- When authoring raw, always JSON.stringify([innerSelector]).
- Validate nested bodies parse as ['string'] before sending.
When it happens
Trigger: Writing a nested engine selector with a malformed body, e.g. internal:has=div (not a JSON array), internal:has=[123] (first element not a string), internal:has=[] (empty), or internal:has=["a","b","c"] (too many elements).
Common situations: Hand-authoring or string-building nested selectors instead of using locator API (.has(), .and(), .or()); forgetting that the internal serialization wraps the inner selector as a JSON array; migrating from text-based selectors.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- Unsupported token "${unsupportedToken.toSource()}" while par
- Error while parsing css selector "${selector}". Did you mean
- "${parts[0].name}" selector cannot be first
- Unexpected end of selector while parsing selector `${selecto
- Error while parsing selector `${selector}`: ${e.message}
AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12).
Data as JSON: /api/errors/acb31ee521b19bff.
Report an issue: GitHub.