-
-
Notifications
You must be signed in to change notification settings - Fork 8.7k
[adr] Behavioral contract for the low-level WebDriver BiDi layer #17786
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
titusfortner
wants to merge
7
commits into
SeleniumHQ:trunk
Choose a base branch
from
titusfortner:adr-bidi-low-level-contract
base: trunk
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+190
β0
Open
Changes from 2 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
613cd02
[docs] add ADR for binding-neutral BiDi low-level behavioral contract
titusfortner 98e1858
[docs] number the low-level BiDi behavioral-contract ADR as 17786
titusfortner dfe1c07
[docs] align ADR intro with the Compliance/Decision split; fix grammar
titusfortner 5b77f31
[docs] drop envelope/body item from BiDi contract; renumber to 12 behβ¦
titusfortner e965829
[docs] extend BiDi contract item 3 to reject wrong const values outbound
titusfortner 1b0e91a
[docs] attribute unknown-property drop to item 8, not item 7, in BiDiβ¦
titusfortner 412d9f2
[docs] drop shared-error-category requirement from BiDi contract; reqβ¦
titusfortner File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
188 changes: 188 additions & 0 deletions
188
docs/decisions/17786-bidi-low-level-behavioral-contract.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,188 @@ | ||
| # 17786. Behavioral contract for the low-level WebDriver BiDi layer | ||
|
|
||
| - Status: Proposed | ||
| - Discussion: https://github.com/SeleniumHQ/selenium/pull/17786 | ||
|
|
||
| ## Context | ||
|
|
||
| The WebDriver BiDi specification is defined in CDDL. Any client implementation has a low-level layer | ||
| that turns typed calls into wire messages and wire messages back into typed objects. It sits above a | ||
| transport (which sends commands and correlates responses) and below the orchestration and high-level | ||
| API that program against it. *This record is about the low-level layer.* | ||
|
|
||
| Because these behaviors follow from the specification rather than from taste, they are identical for any | ||
| conforming implementation, in any language β so one contract can state them for every binding at once. | ||
|
|
||
| ## Decision | ||
|
|
||
| **Any implementation of this layer must exhibit the behaviors below, each a consequence of conforming to | ||
| the WebDriver BiDi specification.** | ||
|
titusfortner marked this conversation as resolved.
Outdated
|
||
|
|
||
| An implementation must exhibit them at runtime, not merely declare them in its types or schema. A | ||
| statically-typed deserializer will fill a correctly-typed object from malformed input β most often a null | ||
| in a non-nullable field β and return it as valid unless a check is written, so a layer can type-check | ||
| perfectly and still violate the contract. Conformance is shown by the rejection firing on malformed | ||
| input, not by a happy-path round trip or matching types. | ||
|
|
||
| ### How to read this contract | ||
|
|
||
| Each item is tagged with the *kind* of requirement it is: | ||
|
|
||
| | Tier | Meaning | | ||
| |---|---| | ||
| | **Compliance** | Follows from conforming to the spec. Enumerated for comprehensiveness and correctness. | | ||
| | **Decision** | A genuine choice this record owns, not compelled by the spec. | | ||
|
|
||
| The behaviors are uniform across every binding β a malformed payload is malformed in any language. Only | ||
| their *form* varies: | ||
|
|
||
| - **Mechanism** β a static type system or runtime checks. | ||
| - **Object** β a dataclass, a record, a value type. | ||
| - **Exception** β a shared error *category* is required; the exact type is idiomatic. | ||
|
|
||
| ### Outbound (constructing and sending) | ||
|
|
||
| 1. **Spec strings go on the wire verbatim.** *(Compliance.)* The exact spec token appears on the wire for | ||
| method names, enum values, and fixed/const values; no casing or naming transform alters it. The | ||
| spec's `beforeunload` goes on the wire unchanged; a binding that camelCases it to `beforeUnload` fails. | ||
|
|
||
| 2. **Optionality and nullability are represented exactly as the spec declares.** *(Compliance.)* | ||
| - **Omitted is distinct from explicit null.** An unset optional is *absent* from the payload; an | ||
| explicit null serializes as `null`. The remote end acts on the difference. | ||
| - **Per-field nullability is honored** as declared. | ||
| - **A nullable constant is a settable value.** Where a field's value is a constant *and* nullable | ||
| (`browsingContext.setBypassCSP`, `emulation.setScriptingEnabled`), the layer must send the literal | ||
| *or* `null`, never only the literal. (A non-nullable constant is always the literal, so it can be a | ||
| fixed value.) | ||
|
|
||
| 3. **The definitions produce only the message body, never the envelope.** *(Compliance.)* This layer emits | ||
| the command or event body; forming the `{id, method, params}` envelope is the transport's | ||
| responsibility, not this layer's. | ||
|
|
||
| 4. **Outbound is validated locally, before sending.** *(Decision.)* Enum membership, required-field | ||
| presence, and no unknown properties on closed types are checked before the message leaves, so a caller | ||
| mistake is a local error rather than a server round-trip. Passing an enum value the spec doesn't | ||
| define, for instance, raises locally instead of being sent for the remote end to reject. A static | ||
| binding gets the check from the type system; a dynamic one does it explicitly. (These classes carry | ||
| only spec-modeled, validated commands; a higher layer needing a command the spec doesn't model would | ||
| create its own separate implementation rather than route untyped data through a typed one.) | ||
|
|
||
| ### Extensibility (cross-cutting) | ||
|
|
||
| 5. **Extensibility follows the spec's per-type signal, inbound and outbound.** *(Decision.)* Whether a | ||
| type admits extra properties is read from its own spec definition, per type and never a hand-maintained | ||
| list, so vendor extension works exactly where the spec permits it and validation stays strict | ||
| everywhere else. In practice: an `Extensible` type carries caller-supplied extras onto the wire (vendor | ||
| fields such as `goog:*` capabilities or vendor proxy keys) and tolerates unknown inbound properties, | ||
| while a closed type rejects unknown properties outbound (item 4) and accepts-but-ignores them inbound | ||
| (item 8). The one prohibition is never injecting arbitrary properties into a *closed* type, which would | ||
| defeat item 4. (Message-level extras are separate: the command envelope is itself `Extensible` | ||
| (`Command = { id, CommandData, Extensible }`), but it is formed above this layer per item 3, so those | ||
| extras are out of scope here β neither required nor forbidden, surfaced wherever a binding likes.) | ||
|
|
||
| ### Inbound (receiving and parsing) | ||
|
|
||
| 6. **Variants and vocabulary are resolved by the spec's declared rule.** *(Compliance.)* A union resolves | ||
| to a variant by its declared rule (discriminator value, presence of required keys, or declared | ||
| default), never a re-derived structural guess, so a valid payload always resolves to the same variant; | ||
| resolving it wrong is a parsing bug. Raising on an *unrecognized* enum token or union variant is the | ||
| strict default (a Decision, item 10), 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. | ||
|
|
||
| 7. **Inbound fields are validated strictly against the resolved type.** *(Decision.)* Once item 6 has | ||
| resolved the type, each field is checked against it, because a wrong field populated silently β a null | ||
| dropped into a non-nullable slot, say β misrepresents the protocol state instead of failing. Each of | ||
| these raises: a missing required field; a null in a non-nullable field; a value of the wrong primitive | ||
| type; a list/scalar cardinality mismatch; a non-object where a field expects an object. Relaxations are | ||
| reactive (item 10). **Satisfying item 6 does not satisfy item 7.** | ||
|
|
||
| 8. **Unknown inbound properties never cause an error.** *(Compliance.)* Every type tolerates unknown | ||
| properties β non-negotiable, because the spec permits extension. Most parsers do this by default; a | ||
| reject-unmapped setting would violate it. (Whether an unknown property is also *preserved* is item 9.) | ||
|
|
||
| 9. **Preserving extras is scoped to types that are both extensible and re-sendable.** *(Decision.)* | ||
| Tolerating an unknown property (item 8) is the floor; *preserving* it β **storing** it readable after | ||
| parse and **echoing** it back on serialization β is worth doing only where a received instance can go | ||
| back out onto the wire, so it happens only when both spec-derivable facts hold: | ||
| - **Extensible** β its CDDL definition includes the `Extensible` group (`Extensible = (*text => any)`), | ||
| admitting arbitrary extra properties. | ||
| - **Re-sendable** β it can appear within a command's parameters, so a received instance can be handed | ||
| back. | ||
|
|
||
| The types meeting both are read from the schema, not a fixed list β cookies, capabilities, and proxy | ||
| configuration among them. A cookie read from `storage.getCookies` | ||
| with a vendor attribute, then passed to `storage.setCookie`, must reach the wire with that attribute | ||
| intact. Every other type (an extensible-but-inbound-only log entry, or a non-extensible type) tolerates | ||
| and drops (item 8). Where a type both stores inbound extras and takes caller-set outbound extras, the | ||
| two merge on serialization and a caller-set value wins. (Widening this scope, if the layer ever went | ||
| public, is the alternative weighed in Considered options.) | ||
|
|
||
| 10. **Inbound is strict by default, relaxed only reactively and uniformly.** *(Decision.)* The layer | ||
| rejects spec-incorrect data rather than absorbing it, because a clear error beats silently | ||
| misrepresenting the response, and it is cheaper to loosen a strict contract than to tighten a lenient | ||
| one. When a real remote end is seen sending off-spec data, the specific field or command is relaxed | ||
| through a documented, reversible change (citing the offending implementation and the spec issue), | ||
| applied to every binding at once β never one alone, and never by starting lenient. One exception is | ||
| sanctioned from the start: an enum that reports an error (an unrecognized error code in an | ||
| `ErrorResponse`), where raising would swallow the error being reported; there, an unknown token is | ||
| surfaced, not thrown. | ||
|
|
||
| ### Surface | ||
|
|
||
| 11. **Names mirror the spec, mapped to language idiom.** *(Decision.)* The surface method mirrors the spec | ||
| command and params/fields carry idiomatic names of their wire keys, so the layer reads as a direct | ||
| projection of the spec and cross-references cleanly across bindings. For example, a Python | ||
| `set_viewport(device_pixel_ratio=β¦)` call serializes the wire key `devicePixelRatio` under | ||
| `browsingContext.setViewport`. This is a naming convention rather than a spec requirement (keeping the | ||
| *wire* names exact is item 1); language sugar belongs in the higher public layer, not here. | ||
|
|
||
| 12. **Structured data is typed, not raw maps.** *(Decision.)* Params and results are typed value objects: | ||
| enums are the language's closed-vocabulary type, and discriminated-union variants are distinct types | ||
| branched on by type, not by inspecting a tag value. A `script.evaluate` result, for example, comes | ||
| back as a `StringValue` or `NumberValue`, not a raw `{type, value}` map. This typing is the foundation | ||
| the inbound items stand on: a raw-map surface structurally cannot do union dispatch (6), field checks | ||
| (7), or read-only objects (13). | ||
|
|
||
| 13. **Objects this layer hands to callers are read-only.** *(Decision.)* A received object that reaches a | ||
| caller (e.g. the request inside a network handler) is immutable at the top level, because mutating it | ||
| changes nothing on the wire and the layer would rather forbid the mutation than let it mislead. The | ||
| caller reads the object and acts through the higher layer. Shallow immutability is enough; nested | ||
| containers need not be deep-frozen. | ||
|
|
||
| ## Considered options | ||
|
|
||
| The Compliance items (1, 2, 3, 6, 8) have no valid alternative; a divergence there is a bug. | ||
|
|
||
| - **Specify a production method (mandate generation) rather than behavior.** Generation does not by | ||
| itself guarantee any behavior and imposes cost where it isn't ergonomic; the behaviors are identical | ||
| however the code is produced. Rejected: the contract is the behavior, generation an optional strategy. | ||
| - **Item 4 β defer to the server.** Send the command and let the remote end return an error rather than | ||
| validating locally; the spec defines those errors, so it is legal. Rejected: a local error is clearer | ||
| and cheaper than a round-trip, and a static binding gets it for free. | ||
| - **Item 5 β surface message-level extras in the definitions layer** rather than leaving them to the | ||
| transport. Rejected: the envelope is the transport's (item 3); this layer governs per-type | ||
| extensibility only. | ||
| - **Item 7 β lenient inbound.** Best-effort an off-spec response rather than raising. Rejected: silently | ||
| misrepresenting protocol state is worse than a clear error; strictness relaxes reactively (item 10) | ||
| when a real payload demands it. | ||
| - **Item 9 β keep extras broad**, preserving unknown fields on all extensible types. This is the right | ||
| choice for a public surface, which can't narrow later without dropping a field a user relies on. | ||
| Rejected here only because the layer is internal: it can scope tight and widen later at no cost. *This | ||
| is the one decision the internal premise actually turns on.* | ||
| - **Item 10 β start lenient and tighten later** rather than starting strict. Rejected on merit: it is | ||
| cheaper to loosen a strict contract than to tighten a lenient one. Being internal only lowers the cost | ||
| of the strict path (a rejection on browser drift is a dev-side fix, not a user-facing break); it is not | ||
| the reason to be strict β a careful public implementation can be strict too. | ||
| - **Item 11 β free naming** rather than mirroring the spec. Rejected: mirroring the spec command and | ||
| params aids cross-referencing and keeps bindings comparable. | ||
| - **Item 12 β raw dicts** rather than typed objects; the spec permits it. Rejected: raw maps structurally | ||
| cannot do strict dispatch, field checks, or read-only objects. | ||
| - **Item 13 β mutable received objects** rather than read-only. Rejected: a received object is | ||
| informational, so read-only prevents mistaking it for a control surface. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - The contract holds for any implementation regardless of language or production method; conformance is | ||
| checkable independently of how the layer was built. | ||
| - The per-type signals items 6/7/9 need are all derivable from the spec; a binding that discards one, or | ||
| parses into a lenient runtime, falls out of conformance on exactly the items that signal feeds. | ||
|
titusfortner marked this conversation as resolved.
Outdated
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.