PRACTICAL GUIDE / InvalidArgumentException WebDriver capability payload
Why WebDriver rejects your capabilities before Chrome starts
Learn to isolate the malformed WebDriver capability, capture the server response, and replace fragile maps with typed Selenium browser options.
In this guide6 sections
What you will learn
- Why session creation fails before a browser exists
- Read the response before editing the payload
- Three payload failures need three different fixes
- Build capabilities without stringly typed surprises
The Grid accepts the connection, then refuses the session before a browser window ever appears. Selenium surfaces a session-creation failure whose nested message points at a capability that looked harmless in a YAML file. Retrying the same request only produces another rejected session because the failure is in the request, not in browser availability.
Why session creation fails before a browser exists
A WebDriver session begins with a POST /session command. The request contains a capabilities object, normally split into alwaysMatch and firstMatch. The required values in alwaysMatch are combined with each candidate in firstMatch, and the remote end tries the resulting candidates in order. This happens before test navigation, element lookup, or application code can matter.
The W3C processing algorithm validates the shape at several levels. The outer capabilities value must be an object. alwaysMatch, when supplied, must be an object. firstMatch must be a non-empty list of objects. Each standard capability also has a defined JSON type. For example, acceptInsecureCerts is a boolean, while browserName, browserVersion, and platformName are strings. pageLoadStrategy accepts a limited set of values rather than any descriptive label a team happens to prefer.
Those details matter when configuration passes through several representations. A YAML boolean can become a Java Boolean, but a quoted YAML value becomes a String. Both print as something a reviewer may casually read as false. On the wire they are different JSON values: false and "false". The first satisfies the protocol. The second does not.
WebDriver reports malformed command arguments with the protocol error string invalid argument and HTTP status 400. The human-readable message and stacktrace fields are implementation-defined, so matching an entire message is brittle. Selenium's Java error decoder maps that protocol category to org.openqa.selenium.InvalidArgumentException. This is why the Java exception class is useful for classification but its prose is a poor test oracle.
That is what a bare endpoint returns, and it is only half the story. Send a malformed payload straight to ChromeDriver on its own port and the answer is HTTP 400 with value.error set to invalid argument. Send the same bytes to a Selenium Grid or standalone server and the shape changes. The node's session factory catches the driver's rejection and rethrows it as a SessionNotCreatedException, so the client receives HTTP 500 with value.error set to session not created. Selenium 4.46.0 behaves this way, and the same code path runs inside the pinned selenium/standalone-chrome images. The capability has not been forgiven; the rejection has been relabelled by the machine in the middle.
The evidence you need therefore sits one level down. Grid preserves the driver's own sentence inside value.message, so a quoted boolean still reports invalid argument: cannot parse capability: acceptInsecureCerts even while the outer category reads session not created. Read the nested text first and treat the outer category as a hint about which server answered rather than as a diagnosis. A framework that branches only on the decoded Java exception class will route a genuinely malformed payload to the infrastructure team and wait for capacity that was never the problem.
Capability names follow another rule. Standard names such as browserName do not contain a colon. Extension capabilities do. A provider might document cloud:options, and Chromium uses goog:chromeOptions. The namespace tells the remote end that another specification or implementation owns the key. An unknown, unprefixed name sent to an endpoint node is not a harmless metadata field. The W3C algorithm can reject it, and Selenium's server-side capability parser rejects it even earlier, answering unknown error with the message Illegal key values seen in w3c capabilities: [testName] before any driver is consulted.
Namespacing does not validate everything inside an extension object. Once a value reaches a vendor-specific deserializer, that implementation decides which nested keys and types are legal. A correctly prefixed capability can therefore still fail. The fix comes from the provider's current schema, not from guessing that any object behind a colon is accepted.
Merging adds a third validation boundary. If platformName appears in both alwaysMatch and one firstMatch entry, the merge is invalid even when both values are identical. alwaysMatch means every candidate must include that requirement. Repeating the same key in a candidate is a collision, not reinforcement. Shared framework code often creates this defect by combining a base map with a browser map after both have acquired the same setting. Selenium names the offending key directly in its response: Overlapping keys between w3c always and first match capabilities: [platformName], again carried under unknown error.
None of these cases needs a running page. A screenshot will be absent because no page was available. Browser console logs will be empty or irrelevant. The useful evidence sits at the session boundary: the serialized request, the HTTP status, both value.error and value.message from the response, the server that answered, and the client exception decoded from that response.
Read the response before editing the payload
Record the protocol category and the nested message together, then read them in that order of specificity. The category tells you which server answered. The message tells you what that server objected to. Selenium adds build, host, driver, and system details to exceptions, and Grid or cloud providers wrap the original response again. A phrase such as "could not start a new session" can appear near several causes, including invalid capabilities, a missing browser binary, a driver mismatch, or a full Grid. Against a Grid every one of those arrives under the same outer category, so the category on its own cannot separate them.
Preserve the first failed attempt. Record the endpoint URL without credentials, Selenium client version, remote server version when available, and the exact capability structure after configuration has been merged. Log the serialized types as well as values. acceptInsecureCerts=FALSE in a flattened text line hides whether the original value was a string or boolean. JSON preserves that distinction.
Do not print credentials, access tokens, encoded extensions, proxy passwords, or a complete vendor object by default. Copy the map and replace sensitive values before serialization. Redaction should be based on an explicit list of keys used by your providers. A generic replacement for any key containing key can remove harmless evidence such as platformKey, while failing to catch a token stored under authorization.
When the client wrapper obscures the outbound request, send a minimal request directly to a disposable local endpoint. The following diagnostic intentionally uses the wrong type for acceptInsecureCerts. It proves that no session was created and that the rejection names the capability, without assuming which of the two response shapes you will receive. Write the assertions to accept either category and then require the nested evidence, because a bare driver on port 9515 and a Grid on port 4444 categorize the identical payload differently.
#!/usr/bin/env bash
set -euo pipefail
: "${WEBDRIVER_URL:?Set WEBDRIVER_URL, for example http://localhost:4444}"
response_file="$(mktemp)"
trap 'rm -f "$response_file"' EXIT
http_code="$(
curl --silent --show-error \
--output "$response_file" \
--write-out '%{http_code}' \
--header 'Content-Type: application/json; charset=utf-8' \
--request POST \
--data-binary @- \
"${WEBDRIVER_URL%/}/session" <<'JSON'
{"capabilities":{"alwaysMatch":{"browserName":"chrome","acceptInsecureCerts":"false"},"firstMatch":[{}]}}
JSON
)"
printf 'HTTP %s\n' "$http_code"
jq '.value | {error, message}' "$response_file"
# Whichever endpoint answered, no session may exist.
test "$http_code" != '200'
jq -e '.value.sessionId == null' "$response_file" >/dev/null
# A bare driver answers 400 with "invalid argument". A Grid node wraps the same
# driver rejection as 500 with "session not created". Accept either category,
# then insist on the nested sentence that names the rejected capability.
jq -e '.value.error == "invalid argument" or .value.error == "session not created"' \
"$response_file" >/dev/null
jq -e '.value.message | test("acceptInsecureCerts")' "$response_file" >/dev/nullEach of those checks can fail for a different reason. If the payload ever became acceptable, the status would be 200 and .value.sessionId would be a string. If the endpoint started rejecting the request for an unrelated reason, such as an absent browser image, the message assertion would fail because it would no longer mention the capability. That is the difference between a probe and a formality.
Run a negative probe only against infrastructure intended for testing. It is deliberately malformed traffic and may trigger security monitoring. The trade-off is also operational: a raw HTTP probe bypasses some client-side normalization, so it tells you what the remote protocol endpoint accepts, not necessarily what a particular Selenium binding will serialize from your framework objects. Running it against both a bare driver and the Grid you actually use is worth the extra minute, because it is the cheapest way to see the relabelling happen.
Compare the raw probe with the framework request. If the probe fails as expected but the framework succeeds with an apparently identical value, inspect serialization before blaming the server. A configuration library may have converted the string to a boolean. If both requests fail with the same protocol category, reduce the real request while retaining its structure. Change one capability per attempt and keep a record of each response.
Network capture is not always the right tool. TLS termination, provider authentication, and organizational policy may prevent recording the request. In that case, log a sanitized copy immediately before constructing RemoteWebDriver. That copy is still more useful than rebuilding a payload from CI environment variables after the run, because environment expansion and defaults are part of the suspected mechanism.
Three payload failures need three different fixes
The quoted boolean is the common case, but treating every rejection as a type conversion problem creates its own blind spot. Three failures can produce the same Java exception while requiring unrelated changes. Against a Grid they do not even share one protocol category, so the messages below are the part worth memorizing.
A scalar changed type during configuration loading. A team stores acceptInsecureCerts: "false" because its deployment system represents every override as text. The framework copies the value into a generic capability map. JSON serialization faithfully sends a string. A bare ChromeDriver endpoint answers HTTP 400 with invalid argument. Selenium 4.46.0 answers HTTP 500 with session not created, and its message repeats the driver's own words: invalid argument: cannot parse capability: acceptInsecureCerts. The endpoint decides the category; the sentence stays the same.
The decisive evidence is the JSON token, not the displayed text. Fix the conversion at the configuration boundary. Parse the allowed values into a boolean and reject anything other than an intentional true or false. Do not repair the value with Java's Boolean.parseBoolean unless silent conversion of every unrecognized string to false is acceptable. A typo such as flase should stop configuration loading, not quietly alter certificate behavior.
The cost of strict parsing is that old pipelines with loosely formatted values will fail earlier. That is desirable during rollout but disruptive if many repositories share the same secrets or variables. Inventory the existing values first, add a warning period if necessary, and then switch the parser to rejection.
A reporting field was placed beside protocol capabilities. Suppose a cloud dashboard expects a test name, and a framework sends testName at the top of alwaysMatch. That key is neither a standard capability nor namespaced. Some intermediaries may consume proprietary top-level data, but an endpoint node following the core validation rules cannot assume what testName means. Selenium 4.46.0 rejects it with HTTP 500, the category unknown error, and the message Illegal key values seen in w3c capabilities: [testName]. Read that category twice: this payload defect is not reported as invalid argument either, so a router keyed on the outer error string would file it beside a browser crash.
Move provider data into the exact documented extension capability, perhaps a nested vendor:options object. Do not copy a name from another provider. Namespaces are contracts, not cosmetic prefixes. acme:options and cloud:options are distinct even when their nested fields happen to look alike.
The evidence that separates this from a scalar problem is the rejected property name and its location. The value may have a perfectly valid JSON type. A provider intermediary can process its documented extension data before forwarding a request, while a local ChromeDriver is the endpoint that validates what it receives. Reproduce against the same endpoint type that failed in CI and compare the provider's documented request shape.
The trade-off is coupling. Once the framework models provider options explicitly, it knows about that provider. Keep the provider adapter at the session factory boundary and expose neutral inputs such as build label and test name to the rest of the suite. A single global map full of every vendor's keys avoids classes today but creates collisions and accidental leakage later.
A shared key appears in both matching branches. A base factory adds platformName to alwaysMatch. A browser matrix independently adds platformName to every firstMatch candidate. The combined request looks consistent in a code review because all values say linux, yet the merge algorithm rejects duplicate property names. Selenium answers HTTP 500 with unknown error and the message Overlapping keys between w3c always and first match capabilities: [platformName]. Three payload defects, three different outer categories once a Grid is in the path, and not one of them is invalid argument.
Remove the property from one side based on intent. Put a requirement shared by all candidates in alwaysMatch. Put alternatives in separate firstMatch objects. If Chrome and Firefox can use the same platform, the platform belongs in the shared object. If each browser targets a different platform, keep it out of the shared object and place it once in each candidate.
This case is easiest to spot in structured JSON. Flattened logs erase branch ownership and can make two occurrences look like harmless repetition. A unit test should inspect the request builder before serialization and fail when key sets intersect. Unlike an assertion over a fixed sample map, that test exercises the production merge function, so a future change that adds the same property to both branches makes it fail.
One near-miss deserves separate treatment, and its signature is silence rather than a different error string. A valid browserVersion string that no registered Grid slot can satisfy is not malformed merely because no browser matches it. The Distributor puts the request in the New Session Queue and holds it there, so the client waits out its own session timeout instead of receiving a prompt rejection. Timing is the discriminator here, not the category. A malformed payload comes back in milliseconds; a well-formed request with no matching slot sits in the queue until something gives up. Compare response latency, queue depth, and slot-matching logs before rewriting a syntactically valid capability.
Another near-miss lives inside goog:chromeOptions.args. WebDriver sees a recognized extension key and passes its value to the Chromium-specific handler. A bad Chrome command-line argument can survive core capability validation and fail when Chrome launches. The timeline changes: server logs show a driver or browser process starting, and the message names the browser rather than a capability. Against a Grid the outer category is session not created in both this case and the quoted-boolean case, so use the process-start records and the nested sentence to separate them. The absence of a page is shared evidence; the point at which the node stopped is not.
Build capabilities without stringly typed surprises
A typed session request prevents many malformed values from reaching Selenium. It does not make the server infallible, and it cannot validate provider features it does not know about. Its value is narrower: standard options become Java types at the edge instead of unreviewed Object values assembled throughout the test suite.
The following factory accepts a boolean, a Selenium enum, and a list of Chrome arguments. A configuration loader must construct BrowserRequest; a quoted boolean cannot inhabit the boolean field. The factory uses Selenium's option methods for standard capabilities and keeps Chromium arguments inside ChromeOptions.
package example.webdriver;
import java.net.MalformedURLException;
import java.net.URI;
import java.util.List;
import org.openqa.selenium.PageLoadStrategy;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.RemoteWebDriver;
public final class DriverFactory {
public record BrowserRequest(
boolean acceptInsecureCerts,
PageLoadStrategy pageLoadStrategy,
List<String> chromeArguments) {
public BrowserRequest {
chromeArguments = List.copyOf(chromeArguments);
}
}
public static ChromeOptions optionsFor(BrowserRequest request) {
ChromeOptions options = new ChromeOptions();
options.setAcceptInsecureCerts(request.acceptInsecureCerts());
options.setPageLoadStrategy(request.pageLoadStrategy());
options.addArguments(request.chromeArguments());
return options;
}
public static WebDriver create(URI gridUri, BrowserRequest request)
throws MalformedURLException {
return new RemoteWebDriver(gridUri.toURL(), optionsFor(request));
}
private DriverFactory() {}
}Typed methods do not remove all escape hatches. Selenium still exposes setCapability because extensions evolve and not every implementation-specific option warrants a dedicated method. Use it at one adapter boundary. Require a colon in extension names, keep an allowlist of providers your organization actually uses, and validate the nested object with that provider's schema or a small local model.
Avoid converting the final ChromeOptions object back into a generic map merely to merge it with other maps. That round trip discards the advantage of typed construction. Instead, give the factory all inputs it owns, or merge well-defined Capabilities objects once and inspect the result. Remember that Selenium's in-memory Capabilities.merge semantics are a client API concern; they are not a substitute for reasoning about W3C alwaysMatch and firstMatch branches in a raw request.
Contract tests should fail when production behavior drifts. This example calls the real factory and checks both value and runtime type. If someone replaces setAcceptInsecureCerts with a generic string loaded from an environment variable, the assertion fails. If the expected policy changes from false to true, the value assertion forces a deliberate test update.
package example.webdriver;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.PageLoadStrategy;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.remote.CapabilityType;
class DriverFactoryTest {
@Test
void keepsStandardOptionsTypedUntilSerialization() {
var request = new DriverFactory.BrowserRequest(
false,
PageLoadStrategy.NORMAL,
List.of("--window-size=1440,900"));
ChromeOptions options = DriverFactory.optionsFor(request);
Object insecureCerts = options.getCapability(CapabilityType.ACCEPT_INSECURE_CERTS);
Object strategy = options.getCapability(CapabilityType.PAGE_LOAD_STRATEGY);
assertAll(
() -> assertInstanceOf(Boolean.class, insecureCerts),
() -> assertEquals(Boolean.FALSE, insecureCerts),
() -> assertInstanceOf(PageLoadStrategy.class, strategy),
() -> assertEquals(PageLoadStrategy.NORMAL, strategy));
}
}Do not turn that test into an assertion that a hard-coded map contains the same hard-coded values declared three lines above. Its target is the production factory. A defect in the factory must be capable of breaking the test. For dynamic provider settings, test the adapter with representative valid and invalid inputs and assert that invalid input is rejected before any network request.
There is a maintenance cost. Selenium may add typed methods, providers may revise nested schemas, and a local allowlist can become stale. Assign ownership to the session factory, review it when upgrading Selenium or a Grid provider, and keep its tests small enough that failures explain which boundary changed.
Roll the change through a shared test framework
Large suites rarely build capabilities in one place. Browser defaults live in a base class, CI injects environment variables, a cloud adapter adds reporting metadata, and individual tests occasionally call setCapability. Replacing all of that in one commit makes it hard to tell whether a session failure came from the new model or an unrelated infrastructure change.
First, add observation without changing behavior. At the final construction point, serialize a sanitized capability snapshot and attach a request ID. Log the same ID with the caught exception. If the provider exposes a session-request identifier even for failures, store it too. Run this on failed session creation only to limit noise and exposure.
Second, inventory every writer. Search for setCapability, DesiredCapabilities, raw alwaysMatch, environment lookups used by driver creation, and provider option keys. Classify each setting as a standard capability, browser-specific option, provider extension, or test metadata that should not be a capability at all. This classification catches misplaced values before code changes begin.
Write down precedence while doing that inventory. A value can begin in a repository file, be replaced by a CI variable, then be overwritten again by a test profile. If the snapshot shows only the final value, engineers may repair the repository default while CI continues sending the override. Emit the source of each resolved setting in a separate diagnostic record, such as acceptInsecureCerts <- CI, without copying secret values. Then test precedence directly: give the loader conflicting non-secret values and assert which source wins. That test can fail when a later refactor changes merge order, unlike a checklist that merely documents the intended order.
Treat missing values separately from explicit false values. A configuration loader that drops false because it uses a truthiness check changes the payload without producing a type error. The server then applies its default, and the session may start with behavior the test did not request. Presence checks should use key membership or nullable types, not boolean truthiness. This is a valid-session defect, so the negative HTTP probe will not catch it; the factory contract test must compare the emitted capability map.
Third, introduce the typed request beside the old path. Convert one browser and one CI lane. The comparison should use the same Grid, browser image, credentials, and test. A successful new path plus a successful old path proves only compatibility, so add negative contract cases for a quoted boolean, an unknown unprefixed key, and a duplicate merge key. Those probes should fail for the intended reason.
Fourth, remove write access from tests. A test that needs a special browser feature should request a named profile or provide a typed override accepted by the factory. Arbitrary maps make every test a protocol author. Profiles create review friction, but they also reveal when one scenario depends on a nonstandard browser configuration.
Fifth, make the contract lane visible in CI. The job below starts a real Selenium standalone server, runs the shown factory test, and sends one inline negative request that must be refused before a session exists. Its assertions describe what that pinned server actually returns, which is a wrapped session not created carrying the driver's cannot parse capability sentence. Because the image tag is fixed, asserting the exact category is legitimate rather than optimistic: if an upgrade changes the wrapping, this job is supposed to fail and force a review. Upgrade the tag through the same dependency process as the Selenium client.
name: WebDriver capability contract
on:
pull_request:
jobs:
capability-contract:
runs-on: ubuntu-latest
services:
selenium:
image: selenium/standalone-chrome:4.46.0-20260707
ports:
- 4444:4444
options: >-
--health-cmd "/opt/bin/check-grid.sh --host 0.0.0.0 --port 4444"
--health-interval 5s
--health-timeout 3s
--health-retries 20
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
cache: maven
- name: Verify the typed factory
run: ./mvnw --batch-mode -Dtest=DriverFactoryTest test
- name: Verify the remote validation boundary
run: |
payload='{"capabilities":{"alwaysMatch":{"browserName":"chrome","acceptInsecureCerts":"false"},"firstMatch":[{}]}}'
status="$(curl --silent --show-error \
--output capability-error.json \
--write-out '%{http_code}' \
--header 'Content-Type: application/json; charset=utf-8' \
--data "$payload" \
http://localhost:4444/session)"
cat capability-error.json
test "$status" = "500"
jq -e '.value.sessionId == null' capability-error.json
jq -e '.value.error == "session not created"' capability-error.json
jq -e '.value.message
| test("cannot parse capability: acceptInsecureCerts")' \
capability-error.jsonThis lane costs startup time and container resources. Keep fast factory tests in the normal unit suite and reserve the remote integration probe for capability serialization and negotiation behavior that cannot be proved in memory. If every application test runs in this job, a focused protocol failure will be buried under unrelated UI failures.
During rollout, monitor the nested messages rather than exception categories. Against a Grid, malformed payloads and genuine startup failures both arrive as session not created, so counting that category measures nothing at all. Extract the first clause of value.message and group on that instead. A falling count of cannot parse capability, Illegal key values seen in w3c capabilities, and Overlapping keys between w3c always and first match capabilities, with browser startup errors holding steady, is the signal that the payload work landed. Sample whole response bodies before drawing a trend, because an intermediary can rewrite messages after an upgrade.
Finally, delete the legacy construction path once all writers have moved. Two live paths invite drift, especially when one still accepts maps. Keep a short migration note listing renamed environment variables, rejected legacy values, and the provider keys now owned by adapters. That note earns its place because it tells maintainers why a previously tolerated string now stops the build.
When not to blame the capability payload
An invalid argument can occur on commands after session creation. A malformed window rectangle, script argument, cookie, or action sequence also belongs to that protocol error category. If the logs contain a session ID and successful navigation before the exception, a new-session capability analysis is pointed at the wrong command. Identify the last request URI and command name first.
Do not treat session not created as proof that the payload is innocent. On a Grid it is the default wrapper for every driver-side rejection, malformed capabilities included, so the outer category clears nothing. Read value.message first. If it names a capability rule, as cannot parse capability, Illegal key values seen in w3c capabilities, and Overlapping keys between w3c always and first match capabilities all do, the request is the defect and the request is what you change. Only when the message describes a binary, a process, a profile directory, or an unavailable slot should you move on to browser and driver startup logs, executable discovery, container shared memory, permissions, and Grid capacity. Removing capabilities may accidentally route to a different slot and appear to fix the problem while changing the test environment.
Authentication and routing failures sit outside capability validation. HTTP 401, 403, or a proxy-generated HTML 404 requires checking credentials, base paths, and gateways. A JSON parser error from your own client may mean the endpoint returned HTML. Looking only for the word invalid in a stack trace collapses these layers into one misleading bucket.
Avoid a strict local allowlist when a provider intentionally introduces extension keys faster than your framework can release. In that environment, validate the namespace and basic object shape locally, then let the provider own nested validation. The trade-off is that some mistakes travel over the network, but the suite remains compatible with documented provider additions. Pair that looser policy with good redacted request capture.
Do not send the deliberately malformed curl probe to a production cloud account on every test run. It consumes provider capacity, creates noisy failed-session records, and may violate traffic policies. Run it against a controlled endpoint during framework or infrastructure upgrades. Normal pull requests can exercise the local type model without opening a session.
Do not standardize browser arguments merely to make capability snapshots identical across operating systems. Some arguments are platform-specific, and a cloud provider may add its own options. Compare the fields relevant to the incident and record intentional differences. Exact snapshot equality turns legitimate infrastructure metadata into churn.
The strongest stopping rule combines command timing with the nested message. If no session ID was returned, the failing command was POST /session, the response came back promptly instead of after a queue wait, and value.message names a capability rule that the saved request violates, fix the request. If any one of those facts is missing, keep the diagnosis open. A shorter claim supported by the actual response body is safer than a confident capability rewrite based on an exception headline or an outer error string that a Grid chose on the driver's behalf.
// FIELD DISPATCH
Get the QA Field Notes
Weekly QA battles, AI testing guides, and interview drills. Free on Substack.
// LIVE COURSE / THE TESTING ACADEMY
Playwright Automation Mastery
Go beyond Selenium. Master Playwright with JS/TS in 90 days.
From the instructor behind this guide.
Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.
PRIMARY REFERENCES
Verify the details at the source
QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.
- 01Official w3.org reference
w3.org
Primary documentation selected and verified for the claims in this guide.
- 02Official w3.org reference
w3.org
Primary documentation selected and verified for the claims in this guide.
- 03Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
- 04Official selenium.dev reference
selenium.dev
Primary documentation selected and verified for the claims in this guide.
FAQ / QUICK ANSWERS
Questions testers ask
Why does Selenium throw InvalidArgumentException before the browser opens?
Session creation can fail while the remote end validates the requested capabilities. A wrong JSON type, an unknown unprefixed key, or a duplicate key across alwaysMatch and firstMatch can produce the WebDriver invalid argument error before a browser process is useful. Against a Selenium Grid the same three defects arrive as SessionNotCreatedException instead, because the node wraps whatever the driver rejected.
How can I find which WebDriver capability is invalid?
Capture the exact outbound capability map and the complete HTTP response from the same attempt, with secrets removed. Read the nested value.message before the outer error code, because a Grid rewrites the category while preserving the driver's own sentence, then compare each standard capability name and JSON type with the W3C contract.
Should custom Selenium Grid capabilities contain a colon?
Vendor extension capabilities use a namespaced key such as vendor:options. The remote implementation defines the contents, so follow that provider's schema instead of placing arbitrary unprefixed keys beside standard WebDriver capabilities.
Is SessionNotCreatedException the same as InvalidArgumentException?
They are different Java classes, but which one you receive depends on the endpoint that answered. A bare driver returns invalid argument and Selenium decodes InvalidArgumentException. A Grid node wraps that identical rejection as session not created, so the same malformed payload becomes SessionNotCreatedException. Keep the nested response message, because the exception class alone cannot separate a bad payload from a missing browser.
Can I fix a capability error by removing fields until the session starts?
That tactic may locate a suspect, but it is not a durable fix by itself. Reduce one field at a time on a saved payload, explain why that field violates a type or namespace rule, and add a contract test before restoring the full request.
RELATED GUIDES
Continue the learning route
GUIDE 01
WebDriver Capability Negotiation with alwaysMatch and firstMatch
Understand WebDriver capability negotiation with alwaysMatch, firstMatch, Selenium RemoteWebDriver, conflict rules, and session evidence.
GUIDE 02
WebDriver Capability Merge Without Silent Overrides
Master WebDriver capability merge with practical examples, architecture decisions, failure analysis, CI guidance, metrics, and scenario-led interview answers.
GUIDE 03
Set WebDriver Timeouts at Session Creation
Set the WebDriver timeouts capability at session creation with correct implicit, pageLoad, and script values, Java examples, and Grid verification steps.
GUIDE 04
20 WebDriver Protocol and Capability Negotiation Interview Scenarios
Practice 20 senior WebDriver protocol and capability scenarios covering session payloads, matching rules, remote errors, and negotiated outcomes.
GUIDE 05
Instrument Selenium Commands with WebDriver Listeners
Instrument Selenium commands with WebDriverListener, structured timing events, failure screenshots, safe redaction, and useful execution timelines.