-
-
Notifications
You must be signed in to change notification settings - Fork 8.7k
[adr] network handler behavior proposal #17685
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
3
commits into
SeleniumHQ:trunk
Choose a base branch
from
titusfortner:adr_handler_behavior
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.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
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
There are no files selected for viewing
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,220 @@ | ||
| # 17685. The user directly controls network handler behavior and event disposition | ||
|
|
||
| - Status: Proposed | ||
| - Discussion: [#17685](https://github.com/SeleniumHQ/selenium/pull/17685) | ||
|
|
||
| ## Context | ||
|
|
||
| A user can register more than one handler for the same network phase, and matching handlers | ||
| can disagree: the company framework always adds a test header, the local suite stubs all calls | ||
| to a domain, and one test aborts a single call. Selenium must resolve that and provide a single | ||
| response to the browser in a consistent and obvious way. | ||
|
|
||
| The bindings diverge today: each grew its handler API independently, so dispatch order, | ||
| multi-handler resolution, error handling, and what an event exposes are all inconsistent. | ||
|
|
||
| | Binding | Current behavior | | ||
| |------------|------------------| | ||
| | Java | Only the first matching handler runs; disposition is always continue; a throwing handler propagates and leaves the request blocked; return-value driven; no response handler or managed body collection. | | ||
| | Python | An explicit `continue` in a handler fires immediately and wins; otherwise staged outcomes reconcile by `fail` > `provide_response` > `continue`; response handlers have no `fail`; dispatch is FIFO; a throwing handler's staged mutations are still sent; only the mutated event is visible; body is not collected behind the handler. | | ||
| | Ruby | Handlers run in parallel threads, so multi-handler disposition races; exceptions are logged; dispatch is FIFO with no default-continue; only the mutated event is visible; body collection is user-managed. | | ||
| | .NET | No request or response handler API. | | ||
| | JavaScript | No request or response handler API. | | ||
|
|
||
| ## Decision | ||
|
|
||
| A network handler is registered either to **observe** or to **intercept**, and the event object it | ||
| receives enforces the difference (behavior 1). For intercept handlers, Selenium consults them one at | ||
| a time and lets each dispose of the event as it runs: the first handler to specify a disposition | ||
| (fail, respond, or submit) resolves the event and stops the chain, while a handler that only stages | ||
| mutations passes the event to the next one. Selenium does not gather every handler's outcome and | ||
| reconcile it at the end. The behaviors below apply to request and response handlers, but not | ||
| authentication handlers, since authentication should not use a callable. | ||
|
|
||
| Note that there are multiple ways to implement these behaviors; the code examples are one | ||
| option in one language, and represent user-facing code. | ||
|
|
||
| 1. **A handler is added to observe or to intercept, and the event object it receives enforces | ||
| which.** There is one method to add handlers, and which mode the handler operates under is decided | ||
| at creation; intercepting is the default and observing is opt-in. An observing handler receives a | ||
| read-only event object: it can read the event but has no methods to mutate or settle it, and it | ||
| does not pause | ||
| network traffic. An intercepting handler receives a mutable event object: it can stage changes and | ||
| settle the event, and network traffic is paused until handling resolves it. Because the object's | ||
| type carries the difference β a read-only object simply has no mutate-or-settle methods β nothing | ||
| has to introspect the callable to tell the modes apart. | ||
| * How a binding lets the user pick the mode β a keyword argument, an options object, an overload β | ||
| is its own idiom; what is fixed is that it is the same method, not a separate observe one. | ||
|
|
||
| ```ruby | ||
| # Same method, two modes; the event object handed to the block differs | ||
| network.add_request_handler { |r| r.fail if something } # intercept: mutable, blocking | ||
| network.add_request_handler(observe: true) { |r| log(r.url) } # observe: read-only, non-blocking | ||
|
|
||
| # An observed event object has no mutation methods, so trying to mutate raises | ||
| network.add_request_handler(observe: true) { |r| r.fail } # raises: observed events are read-only | ||
| ``` | ||
|
|
||
| 2. **An intercept handler can specify event disposition.** Allow the user to specify how the event is disposed of | ||
| by acting on the object provided to the callable. | ||
| * Playwright only intercepts requests and requires an explicit disposition: continue (stop | ||
| processing other handlers), fulfill (respond with a mock), abort (respond with an error), | ||
| fallback (process other handlers, if any). | ||
| * Selenium supports: | ||
| * Request: `fail` (Playwright's `abort`, BiDi's `FailRequest`), `respond` (Playwright's `fulfill`, BiDi's `ProvideResponse`), and `submit` (Playwright's `continue`, BiDi's `ContinueRequest`). | ||
| * Response: `fail` (BiDi's `FailRequest`), and `submit`: note that since we don't need to prevent a round trip from a request, whether this is a BiDi `ContinueResponse` or `ProvideResponse` can be an implementation detail based on whether a replacement body value is provided. | ||
|
|
||
| ```ruby | ||
| # Specifics of parameters and names can match spec details | ||
| network.add_request_handler { |r| r.fail if something } | ||
| network.add_request_handler { |r| r.respond(content: mocked_response) if something } | ||
| network.add_request_handler { |r| r.add_header("X-Test", true) && r.submit if something } | ||
|
|
||
| network.add_response_handler { |r| r.fail if something } | ||
| network.add_response_handler { |r| r.submit(content: mocked_response) if something } | ||
| network.add_response_handler { |r| r.add_header("X-Test", true) && r.submit if something } | ||
| ``` | ||
|
|
||
| 3. **Default disposition is to process other handlers.** If a handler does not specify the | ||
| disposition, the original event and any staged mutations pass to the next handler. If no | ||
| handler ever specifies one, the event proceeds with the staged mutations. | ||
| * In Playwright request interception there is no default; the user must specify fallback if | ||
| that is the intent. | ||
|
|
||
| ```ruby | ||
| # All of these stage a change and pass to the next handler | ||
| network.add_request_handler { |r| r.add_header("X-Test", true) } | ||
| network.add_request_handler { |r| r.remove_header("upgrade-insecure-requests") } | ||
| network.add_request_handler { |r| r.content = r.request.content.gsub("a", "b") } | ||
| ``` | ||
|
|
||
| 4. **Later-registered handlers are consulted first.** Registering an additional handler can | ||
| mutate the state used by previously registered ones. | ||
| * Matches Playwright's Last-In-First-Out (LIFO) behavior. | ||
| * Allows users to locally override handlers set by a shared library or suite. | ||
| * The alternative is being stuck with the top-level behavior everywhere, or not being able to | ||
| set top-level defaults at all. | ||
|
|
||
| ```ruby | ||
| # Header will be there because removal is attempted before it is added | ||
| network.add_request_handler { |r| r.add_header("X-Test", true) } | ||
| network.add_request_handler { |r| r.remove_header("X-Test") } | ||
| ``` | ||
|
|
||
| 5. **An uncaught exception discards the handler's staged changes; it propagates for an intercept | ||
| handler and is logged for an observe handler.** Either way the event keeps flowing as if that | ||
| handler had not run, so one broken handler cannot corrupt live traffic or stall the page. The | ||
| difference is visibility: an intercept handler expresses the test's intent, so a failure in it | ||
| surfaces to the user; an observe handler is passive monitoring, so an incidental failure (a | ||
| third-party beacon, an analytics call) is logged and never fails the test. | ||
| * In Playwright every uncaught exception ends the session; routing by mode keeps interception | ||
| strict without coupling the test to errors from the open internet it never meant to assert on. | ||
|
|
||
| ```ruby | ||
| # Intercept: the error surfaces; the header addition from the other handler still applies | ||
| network.add_request_handler { |r| r.add_header("X-Test", true) } | ||
| network.add_request_handler { |r| raise Exception } | ||
|
|
||
| # Observe: the error is logged, the test is unaffected | ||
| network.add_request_handler(observe: true) { |r| raise Exception } | ||
| ``` | ||
|
|
||
| 6. **Return values within the callables are ignored.** No meaning will ever be applied to | ||
| anything a user explicitly or implicitly returns within the callable. | ||
| * Playwright also does this, as does Selenium's current Python implementation. | ||
|
|
||
| ```ruby | ||
| # Ruby: this implicit return value is ignored | ||
| network.add_request_handler { |r| r.add_header("X-Test", true); "this value is ignored" } | ||
| ``` | ||
|
|
||
| ```python | ||
| # Python: this explicit return value is ignored | ||
| def handler(r): | ||
| r.add_header("X-Test", True) | ||
| return "this value is ignored" | ||
|
|
||
| driver.network.add_request_handler(handler) | ||
| ``` | ||
|
|
||
| 7. **A handler has access to the original event value.** It may see the changes staged by | ||
| handlers already executed, but can also read the unmodified event value. | ||
| * Even when intercepting and mutating, a conditional can be evaluated against the original value | ||
| rather than the version a prior handler changed. | ||
| * Playwright uses a completely separate mechanism to differentiate observation from mutation, | ||
| so it does not need to address this. | ||
|
|
||
| ```ruby | ||
| # Nothing gets raised | ||
| network.add_request_handler { |r| raise unless r.headers.include?("X-Test") } | ||
| network.add_request_handler { |r| raise if r.request.headers.include?("X-Test") } | ||
| network.add_request_handler { |r| r.add_header("X-Test", true) } | ||
| ``` | ||
|
|
||
| 8. **Body data is collected only when the handler opts in at registration.** A body is not | ||
| available by default; the handler declares that it needs the body when it is registered β not | ||
| from inside the callback, since the collector must be in place before the event β and Selenium | ||
| then owns the collector's lifecycle, size cap, and browser-support quirks. The body is readable | ||
| on the event inside that handler. | ||
| * The user never calls `addDataCollector` / `getData` or tears a collector down. | ||
| * There is no way to collect or read body data outside a handler; collection happens only | ||
| through `add_x_handler`. | ||
| * Playwright exposes bodies through its response object without a user-managed collector; | ||
| Selenium does the same, owning the collector behind the handler. | ||
|
|
||
| ```ruby | ||
| # Declare body collection at registration; the body is then available on the event | ||
| network.add_response_handler(collect_body: true) { |r| log(r.body) } | ||
| ``` | ||
|
|
||
| ## Considered options | ||
|
|
||
| - **Modes (behavior 1).** | ||
| - We could give observation its own method, separate from interception, but both modes share the | ||
| same registration shape (URL patterns, body opt-in, the removal handle), so a separate method | ||
| duplicates the whole surface; and the read-only contract cannot be enforced by the method | ||
| anyway β we will not introspect the callable β so the event object's type carries it and the | ||
| method stays the same. | ||
| - We could make every handler an interception and add read-only observation later, but routing | ||
| observation through interception pauses traffic and perturbs what it records (cache behavior, | ||
| timing), so observation-only is worth providing as its own option now, not deferred. | ||
| - **Reconciliation (behaviors 2 & 3).** | ||
| - We could run every handler and reconcile by a fixed priority (fail > stub > continue). But | ||
| this prevents a user from exercising the `continueRequest` behavior from a specific | ||
| handler, so all mutations from all handlers would be applied by default. | ||
| - We could run every handler but have `continueRequest` override failures and stubs (current | ||
| Python behavior), but it is not obvious why that command should have precedence. | ||
| - **Verb names (behavior 2).** | ||
| - We could follow Playwright's (abort / fulfill / continue / fallback). | ||
| - We could follow BiDi's more explicitly (failRequest / provideResponse / continueRequest / continueResponse). | ||
| - **Explicit disposition (3).** | ||
| - We could require the user to specify fallback explicitly like Playwright does. | ||
| - **Ordering (behavior 4).** | ||
| - We could run in order of handler registration, but this prevents users from overriding global settings locally. | ||
| - **Failure (5).** | ||
| - We could propagate the uncaught exception to end the session like Playwright does, but this | ||
| puts a larger burden on the users to manage network issues and bugs that aren't part of a | ||
| test. This is likely to be a bigger issue if we intercept every event by default. | ||
| - We could log every handler's exception regardless of mode, but an intercept handler's failure | ||
| is the test's own bug and should not be swallowed. | ||
| - **Return values (6).** | ||
| - We could have the return values set state for the event or handler rather than storing it in | ||
| the event wrapper object we provide, but this is not as straightforward in all languages | ||
| and adds additional complications. | ||
| - **Original access (7).** | ||
| - We could only expose the modified event, or only the original, instead of both. | ||
| - We could provide a separate observation API like Playwright, but even when mutating it could | ||
| make sense to evaluate a conditional from the original event rather than the mutated one. | ||
| - **Data collection (8).** | ||
| - We could collect every body always, but bodies are large and most handlers never read them, | ||
| so collection is opt-in at registration instead. | ||
| - We could require the user to manage the data collector directly through the low-level | ||
| commands, but collection has no meaning outside a handler and would push lifecycle, | ||
| size-cap, and browser-support bookkeeping onto the user. | ||
|
|
||
| ## Consequences | ||
|
|
||
| - Client code can override shared handlers locally and resolve a request its own way, a broken | ||
| handler stays contained, and the original event remains readable. | ||
| - This changes handler behavior that several bindings already ship, so it is not backwards | ||
| compatible. | ||
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.