[adr] Behavioral contract for the low-level WebDriver BiDi layer - #17786
[adr] Behavioral contract for the low-level WebDriver BiDi layer#17786titusfortner wants to merge 10 commits into
Conversation
PR Summary by QodoAdd ADR defining behavioral contract for low-level WebDriver BiDi layer
AI Description
Diagram
High-Level Assessment
Files changed (1)
|
Code Review by Qodo
1.
|
|
Code review by qodo was updated up to the latest commit 98e1858 |
There was a problem hiding this comment.
Pull request overview
Adds a new ADR to the cross-binding decision log that defines a behavioral contract for Selenium’s internal, low-level WebDriver BiDi protocol layer (validation, extensibility, parsing strictness, and surface shape), intended to keep binding implementations aligned on wire-observable behavior.
Changes:
- Introduces a decision record enumerating required outbound/inbound behaviors for the BiDi low-level layer.
- Defines a two-tier taxonomy (Compliance vs Decision) to separate spec-forced requirements from Selenium-standardized choices.
- Records rationale, alternatives, and consequences to support consistent adoption across bindings.
|
Code review by qodo was updated up to the latest commit dfe1c07 |
|
Code review by qodo was updated up to the latest commit 5b77f31 |
|
Code review by qodo was updated up to the latest commit e965829 |
|
Code review by qodo was updated up to the latest commit 1b0e91a |
…uire only that malformed input raises
Code Review by Qodo
1. Extras retention rule undefined
|
| strict default (a Decision, item 9), not part of this compliance floor; for a union whose arms are | ||
| all objects, a non-object payload has no variant to select and raises. | ||
|
|
||
| 6. **Inbound fields are validated strictly against the resolved type.** *(Decision.)* Once item 5 has |
There was a problem hiding this comment.
This may lead to bad situation. Let's image one required (non-nullable) field is added into the spec. Selenium added it accordingly. Users will get serialization error because this field is missing in payload... because... some vendor still didn't implement it.
IMHO serialization process should be relaxed.
Real example: w3c/webdriver-bidi#1117
There was a problem hiding this comment.
So you know, this comment resulted in my spending the past 5 days overhauling the ADR. Not sure it was worth that much time investment, but I'm much happier with it now. The TL/DR is that I now think inbound should be strictly validated except for missing required parameters.
… split, relaxation scope, consistency pass
|
Code review by qodo was updated up to the latest commit 541031b |
…n, inbound validation & retention, typed surface
|
Code review by qodo was updated up to the latest commit deb870f |
|
I just pushed a substantial rewrite of both the ADR and updated the PR description to match it. The main behavioral change is inbound required-field absence: the proposal now leaves a missing required inbound field omitted and warns, rather than rejecting the whole message by default. That will likely end up being the main policy question for review: should Selenium tolerate that incorrectness in order to be more flexible to browser/schema drift. The ADR is now framed around representability: outbound stays strict because Selenium controls what it sends; inbound tolerates only departures that can be represented without inventing data; corrupt present values and unknown closed-vocabulary members still error. The implementation comparison in the PR body is there to show which current differences are implementation cost/adoption gaps and which ones are real policy choices. |
…validity floor, numeric boundary, event params, union resolution)
| declared. A primitive matches by JSON kind, not language representation: `number` admits any JSON | ||
| number, while `integer` rejects a fractional or float-encoded one (a whole-valued `5.0` included); | ||
| neither direction coerces across the boundary silently. |
There was a problem hiding this comment.
1. Integer token handling unclear 🐞 Bug ≡ Correctness
Decision 1 requires integer to reject whole-valued 5.0 as “float-encoded”, which depends on the JSON number’s lexical form (not just its numeric value) and is not enforceable once a typical JSON parser has normalized the token. Without explicitly requiring lexeme-preserving parsing (or weakening the rule), bindings will either be unable to conform or will diverge in what they accept/reject.
Agent Prompt
### Issue description
The ADR requires rejecting an `integer` value that was encoded as `5.0`, which implies inspecting the JSON numeric lexeme. Many JSON stacks do not preserve that distinction after parsing, so the behavior is underspecified and likely to diverge across bindings.
### Issue Context
This is in the normative validation rule for primitives.
### Fix Focus Areas
- docs/decisions/17786-bidi-low-level-behavioral-contract.md[31-33]
### Suggested change
Amend the text to either:
1) explicitly require lexeme-preserving number parsing (or an equivalent exact-number/token API) for inbound validation when enforcing `5` vs `5.0`, **or**
2) redefine the rule in terms of numeric value only (e.g., “reject values with a fractional component”), removing the lexical-form requirement.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| - **An undeclared property warns, and is tolerated** (forward-compatibility). Where the type is | ||
| round-trippable — the received type or its command-parameter counterpart is extensible — the unknown | ||
| property is kept on the object, so a caller can reproduce it on the wire: a vendor attribute received on | ||
| a `network.Cookie` can be set again through `storage.setCookie` unchanged. Other undeclared properties | ||
| are tolerated but not retained. |
There was a problem hiding this comment.
2. Extras retention rule undefined 🐞 Bug ⚙ Maintainability
Decision 3 retains unknown properties only when “the received type or its command-parameter counterpart is extensible”, but it doesn’t define what a “command-parameter counterpart” is or how to determine this mapping for results and events. This makes the retain-vs-drop behavior non-deterministic across bindings even though it’s presented as part of the contract.
Agent Prompt
### Issue description
The ADR’s rule for when to retain unknown inbound properties depends on an undefined concept (“command-parameter counterpart”), which will lead to inconsistent implementations.
### Issue Context
This affects forward-compatibility behavior and round-tripping of vendor/future fields.
### Fix Focus Areas
- docs/decisions/17786-bidi-low-level-behavioral-contract.md[57-61]
### Suggested change
Define the rule in a schema-addressable way, for example:
- explicitly define what “counterpart” means (e.g., a type pair linked by an explicit schema annotation or a named relationship), and how it applies to command results vs events.
- or simplify the retention condition to depend only on properties of the *received* type (e.g., “retain extras only on types marked extensible AND designated round-trippable by the shared schema”).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| 3. **Inbound, those same two are tolerated.** Process error responses first: a remote error surfaces as an | ||
| error even if its payload fails validation — an unrecognized error code included — and the checks here | ||
| must not turn it into a local serialization failure; the error's contents are otherwise out of scope. | ||
| Otherwise these rules govern every typed payload the layer parses — a command's result and a | ||
| server-initiated event's parameters alike, with response correlation and event routing out of scope: |
There was a problem hiding this comment.
3. Error surfacing boundary unclear 🐞 Bug ☼ Reliability
Decision 3 says remote errors must surface even if their payload fails validation, but it doesn’t specify the minimum classification boundary for “this is an error response” versus “this is a malformed/unclassifiable message”. Without that clarification, implementations can still legitimately throw local serialization errors for malformed error envelopes, contradicting the stated requirement to not let validation override remote errors.
Agent Prompt
### Issue description
The ADR promises that remote errors are surfaced even when the error payload fails validation, but it doesn’t define what must be successfully parsed to classify a message as an error response, nor what happens when those identifying fields are missing/invalid.
### Issue Context
Transport behavior is said to be out of scope, but this section still imposes a behavioral requirement on error handling that depends on a clear classification boundary.
### Fix Focus Areas
- docs/decisions/17786-bidi-low-level-behavioral-contract.md[47-51]
### Suggested change
Add a short normative clarification such as:
- which minimal envelope fields must be read to classify an inbound message as an error response (before full validation), and
- if those are missing/invalid, whether the message is treated as a transport/protocol error (out of scope here) versus a local serialization error.
This keeps the “errors must surface” promise implementable and consistent across bindings.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
Code review by qodo was updated up to the latest commit fdb7525 |
📄 The decision, rationale, considered options, and consequences are in the record file this PR adds —
read it there. Below are proposal notes and review logistics.
🔗 Related
📝 Proposal notes
shared schema makes some behavior cheap to derive, but the contract is still the behavior every binding
exhibits, regardless of how it is implemented.
messages back into typed objects. Transport behavior is out of scope and should be decided separately if
needed.
decision list. It separates three things that are easy to conflate: behavior that is already agreed,
behavior that is unfinished or not-yet-adopted, and behavior where there is a real policy choice.
from generation are the pressure points: closed-type rejection, vendor extras on extensible types,
omitted-vs-null preservation, extra-property retention, and typed value objects. Selenium can choose a
simpler common standard that is easier for handwritten implementations, but that permits lossy behavior. Or
it can require the more precise per-type behavior everywhere. The table matters here because it shows
handwritten does not automatically mean "cannot do this"; some handwritten implementations pay the per-type
cost where they consider it correct.
tolerate only what can be represented without inventing data; reject what would produce a false typed
object.
and may be retained where useful.
resolved type. The corrupt-value case is the hard floor, since accepting it means abandoning runtime
validation entirely.
remain omitted and warn so Selenium does not block users when its schema is ahead of a browser
implementation, accepting that this also tolerates a genuine defect because the layer cannot tell the two
apart?
📊 Implementation comparison
Where each implementation (including current, proposed, and external) currently stands against the ADR
The record states four behavioral decisions (D1–D4); the table expands them into the comparison points
that best distinguish current implementations.
Per-implementation through-lines
time, while inbound is whatever falls out of casting the payload onto the object — no runtime checks, so
1.3/1.4 are ❌ and 3.1 passes only incidentally.
data is carried on spec-extensible types, and corrupt or malformed inbound values error. Its intentional
split from the ADR is policy, not cost: a missing required inbound field is an error because the payload is
objectively invalid against the schema.
bag instead of being modeled per type, and missing fields get defaults instead of being tracked as absent.
Those choices avoid expensive per-type work, but they are lossy, which is why its misses line up with the
rows that are costly by hand and cheap to generate.
adoption gaps.
Notes
① A malformed outbound message fails wdio's TypeScript compilation and never reaches the wire, which the
ADR accepts as local validation for a statically-typed binding.
② .NET trunk holds vendor extras only in a blanket
additionalDatasink on the message envelope, with noper-type extension on the spec-extensible types themselves — so a vendor capability (
goog:*/moz:*) orcookie attribute can't attach to the type that carries it, and never reaches the wire or survives a
round-trip. (bidi-net has the same envelope hatch but also carries per-type extension data on the
extensible types, so its extras do reach the wire — which is why it passes 2.3 and 3.5.)
③ wdio does not validate inbound payloads at runtime, so it tolerates a missing required field (absence
stays absent) and accepts an undeclared property alike — but with no runtime pass it has no path to warn on
either, which the ADR requires.
④ This binding errors on a missing required field instead of tolerating it as absent, so the whole message
is rejected where the ADR would keep going.
⑤ .NET trunk and Python trunk don't error, but instead of leaving the field absent they fill it with a
default (
0/null/false) a caller can't distinguish from a genuine value — a missing size reads as 0, amissing flag as false, and downstream code acts on fabricated data with nothing to signal the field never
arrived.
⑥ Cheaper for static bindings: an attribute-driven deserializer rejects nulls and wrong types (1.3–1.4)
from the type system, even un-generated.
⑦ Cheaper for dynamic bindings: a sentinel keeps omitted distinct from null (3.2) without a static
Optional/wrapper.🗣 Discussion
Notes from Slack and TLC minutes are recorded here as the proposal is discussed.
📌 Tracking
Tracking issue: (linked on acceptance)