invalid_selector

invalid_selector means a CSS selector in your request is not valid CSS. The API cannot look for an element when the selector cannot be parsed, so nothing is rendered. The error is about the syntax of the selector, not the page: the same selector fails on every site.

Field Value
HTTP status 400 Bad Request
error.type validation_error
error.code invalid_selector
retryable false
Credit used No

It is part of the error code reference. If the selector is valid but the page has no matching element, you get selectornotfound instead.

Example response

{
  "error": {
    "type": "validation_error",
    "code": "invalid_selector",
    "message": "'div[' is not a valid selector",
    "param": "capture.selector",
    "retryable": false,
    "docs": "https://docs.renderscreenshot.com/errors/invalid_selector"
  },
  "request_id": "req_2b81d0c4e7a95f13"
}

A selector that only the browser rejects can come back as a target_error (HTTP 422) whose message contains is not a valid selector. Treat both forms the same way: fix the selector and do not retry.

Where selectors appear

These parameters take a CSS selector. Any of them can cause the error:

Selectors that fail

We ran each selector through document.querySelector in headless Chrome 148. That is the parser the renderer uses.

Selector Valid? Why Use instead
#123 No An ID cannot start with a digit in CSS #\31 23 or [id="123"]
.md:flex No :flex is read as a pseudo-class .md\:flex
[data-test=1] No Unquoted attribute values cannot start with a digit [data-test="1"]
.price$ No $ is not allowed in a class name without escaping [class~="price$"]
div[ No Unclosed bracket div[data-id]
.a,,.b No Empty item in a selector list .a, .b
button:has-text("Buy") No Playwright syntax button[data-action="buy"]
text=Pricing No Playwright syntax an id, class or attribute selector
.btn:contains("Buy") No jQuery syntax, never part of CSS an attribute selector
//div[@id="x"] No XPath div#x
main >>> .x No Puppeteer-only deep combinator main .x
.md\:flex, :has(> img) Yes Escaped colon; :has() is supported

Tailwind class names are a frequent cause. Classes such as md:flex, w-1/2 and hover:underline need a backslash before : and /.

Escaping in JSON and in URLs

The backslash that CSS needs must survive your transport too:

  • JSON body: double it. To send .md\:flex, write ".md\\:flex". JSON.stringify and json.dumps do this for you.
  • Query string: percent-encode the whole selector. A raw # in a URL starts the fragment, so #main never reaches the API. Encode it as %23main.

Check selectors before you send them

This script asks Chrome itself whether each selector is valid. It needs Puppeteer but no network access:

// check-selectors-chrome.mjs  (Node 22, puppeteer)
import puppeteer from 'puppeteer';

const selectors = process.argv.slice(2);
const browser = await puppeteer.launch();
let bad = 0;
try {
  const page = await browser.newPage();
  for (const s of selectors) {
    const error = await page.evaluate((sel) => {
      try {
        document.querySelector(sel);
        return null;
      } catch (err) {
        return err.message;
      }
    }, s);
    if (error) bad++;
    console.log(`${error ? 'INVALID' : 'ok     '}  ${JSON.stringify(s)}${error ? '  ' + error : ''}`);
  }
} finally {
  await browser.close();
}
process.exitCode = bad ? 1 : 0;
node check-selectors-chrome.mjs '#main' '.md:flex' '[data-test="1"]'

The script exits with code 1 if any selector is invalid, so you can run it in CI over the selectors in your config. A lighter check with jsdom was not reliable in our test: jsdom accepted .a,,.b, which Chrome rejects.

Handle it in code

In Python, give the bad selector its own exception, so callers do not put it in a retry queue:

# element_capture.py  (Python 3.9+, requests)
import os

import requests

API = os.environ.get("RS_BASE_URL", "https://api.renderscreenshot.com")


class BadSelector(Exception):
    """The selector itself is wrong. Fix it; a retry gives the same result."""


def capture_element(url, selector):
    res = requests.post(
        f"{API}/v1/screenshot",
        headers={"Authorization": f"Bearer {os.environ['RS_API_KEY']}"},
        json={"url": url, "capture": {"mode": "element", "selector": selector}},
        timeout=(10, 90),
    )
    if res.ok:
        return res.content

    error = res.json()["error"]
    message = error.get("message", "")
    if error["code"] == "invalid_selector" or "is not a valid selector" in message:
        raise BadSelector(f"{selector!r}: {message} (request {res.json().get('request_id')})")
    res.raise_for_status()

Tested on 2026-10-15: the Chrome check with Node 22.18, Puppeteer 24.43.1 and Chrome 148; the Python function with Python 3.14 and requests 2.32.3 against a local mock that returns the API's error envelope.

Related in this topic

Was this page helpful?