[grid] honor client-advertised se:remoteUrl for reachable BiDi/CDP/VNC URLs#17790
[grid] honor client-advertised se:remoteUrl for reachable BiDi/CDP/VNC URLs#17790titusfortner wants to merge 3 commits into
Conversation
PR Summary by QodoGrid: use client-advertised se:remoteUrl for reachable BiDi/CDP/VNC endpoints
AI Description
Diagram
High-Level Assessment
Files changed (13)
|
Code Review by Qodo
1.
|
| public Builder gridUrlSpecified(boolean configured) { | ||
| this.gridUrlSpecified = configured; | ||
| return this; | ||
| } | ||
|
|
There was a problem hiding this comment.
4. Gridurlspecified footgun 🐞 Bug ⚙ Maintainability
LocalNode.Builder defaults gridUrlSpecified to false and requires callers to set it separately from the gridUri they pass. Any caller that supplies a configured/public gridUri but forgets to set gridUrlSpecified(true) will silently allow client se:remoteUrl to override what the caller intended to be authoritative.
Agent Prompt
### Issue description
The precedence decision in `resolvePublicGridUri` depends on `gridUrlSpecified`, but the builder API lets callers pass a `gridUri` while leaving `gridUrlSpecified` at its default `false`. This creates an easy-to-miss misconfiguration where an explicitly supplied public grid URI can be overridden by client-provided `se:remoteUrl`.
### Issue Context
`LocalNodeFactory` sets the flag correctly, but other builder call sites (tests/extensions) can pass a non-default `gridUri` without remembering to also set the boolean.
### Fix Focus Areas
- java/src/org/openqa/selenium/grid/node/local/LocalNode.java[1485-1505]
- java/src/org/openqa/selenium/grid/node/local/LocalNode.java[1577-1588]
### Suggested fix
Implement one of:
1) Make `gridUrlSpecified` derive from constructor inputs by default (e.g., initialize it in `Builder` based on whether `gridUri` differs from `uri`), while still allowing explicit override for the "equal URIs but configured" edge case.
2) Change the builder API to make the configured-vs-auto-detected nature explicit (e.g., introduce a dedicated `publicGridUri(URI configuredUri)` setter that both stores the URI and marks it configured, instead of a separate boolean).
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
There was a problem hiding this comment.
The only non-test caller of LocalNode.builder is LocalNodeFactory, and it derives both values from the same source in the same place
Additionally forgetting the flag only matters if a session request also carries se:remoteUrl
|
Code review by qodo was updated up to the latest commit 9a20e5d |
|
Code review by qodo was updated up to the latest commit c8274b1 |
There was a problem hiding this comment.
Pull request overview
This PR makes Selenium Grid return BiDi/CDP/VNC endpoints (webSocketUrl, se:cdp, se:vnc) that are reachable from the client when Grid is accessed via Docker port-mapping or reverse proxies, by having clients advertise the URL they actually used to reach the Grid (se:remoteUrl) and teaching the Node to prefer it when rewriting proxied URLs (unless an operator-configured grid-url is set).
Changes:
- Add
se:remoteUrladvertising in all bindings (Java, JS, Python, Ruby, .NET) when starting remote sessions. - Update Grid Node URL rewriting to use
se:remoteUrlwhengrid-urlis not explicitly configured, while preserving credentials and keepinggrid-urlprecedence. - Add/adjust unit tests (notably Java Node tests and binding-level unit tests) to cover precedence and credential preservation.
Reviewed changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| rb/spec/unit/selenium/webdriver/remote/driver_spec.rb | Updates request expectations to include se:remoteUrl in new-session payloads. |
| rb/lib/selenium/webdriver/remote/driver.rb | Ruby Remote driver now advertises se:remoteUrl based on the configured server URL. |
| py/test/unit/selenium/webdriver/remote/new_session_tests.py | Adjusts assertions to ignore the new se:remoteUrl capability when comparing payloads. |
| py/selenium/webdriver/remote/webdriver.py | Python Remote WebDriver adds se:remoteUrl for remote (non-service) sessions. |
| javascript/selenium-webdriver/index.js | JS Builder advertises se:remoteUrl when creating remote sessions. |
| java/test/org/openqa/selenium/remote/RemoteWebDriverUnitTest.java | Adds tests for advertising se:remoteUrl in Java RemoteWebDriver (remote vs local). |
| java/test/org/openqa/selenium/remote/RemoteWebDriverBuilderTest.java | Adds test ensuring builder-advertised se:remoteUrl is sent to the server. |
| java/test/org/openqa/selenium/grid/node/NodeTest.java | Adds regression tests for Node URL rewriting using se:remoteUrl, precedence, and credential preservation. |
| java/src/org/openqa/selenium/remote/RemoteWebDriverBuilder.java | Injects se:remoteUrl into alwaysMatch when a base URI is configured. |
| java/src/org/openqa/selenium/remote/RemoteWebDriver.java | Adds se:remoteUrl to capabilities when the RemoteWebDriver has a non-null base URI. |
| java/src/org/openqa/selenium/grid/node/local/LocalNodeFactory.java | Plumbs whether grid-url was explicitly specified into LocalNode construction. |
| java/src/org/openqa/selenium/grid/node/local/LocalNode.java | Rewrites BiDi/CDP/VNC URLs using resolved “public Grid URI” with precedence rules. |
| dotnet/src/webdriver/WebDriver.cs | .NET WebDriver adds se:remoteUrl when using HttpCommandExecutor (remote sessions). |
| dotnet/src/webdriver/Remote/HttpCommandExecutor.cs | Exposes the remote server URI internally to support se:remoteUrl advertising. |
| if (typeof url === 'string') { | ||
| capabilities.set('se:remoteUrl', url) | ||
| } |
There was a problem hiding this comment.
Zero functional impact. The only thenable url source is startSeleniumServer(SELENIUM_SERVER_JAR) (line 647), which starts a local server and resolves to its localhost address. That's precisely the case where se:remoteUrl is pointless
The proposed fix doesn't work Executor.execute serializes the capabilities at lib/http.js:445 (buildRequest) before it awaits the client promise at line 450. So setting se:remoteUrl inside the Promise.resolve(url).then(...) chain runs after the NEW_SESSION body is already built — too late
| if (typeof url === 'string') { | ||
| capabilities.set('se:remoteUrl', url) | ||
| } |
There was a problem hiding this comment.
3. Promise url skips remoteurl 🐞 Bug ≡ Correctness
In JS Builder, se:remoteUrl is only set when the server address is a string, so sessions created with a Promise-backed URL (e.g., SELENIUM_SERVER_JAR) or a URL object won’t advertise se:remoteUrl. In proxied/Docker Grid setups this makes Nodes fall back to their own (often unreachable) address when building returned BiDi/CDP/VNC URLs.
Agent Prompt
### Issue description
`se:remoteUrl` is only added when `url` is a string, but `url` can be a Promise (from `startSeleniumServer`) or potentially a `URL` object. In those cases the capability is never sent.
### Issue Context
`Builder.build()` already uses `Promise.resolve(url)` to construct the HTTP client, so you can reuse that same resolved value to set `se:remoteUrl` deterministically.
### Fix Focus Areas
- javascript/selenium-webdriver/index.js[651-660]
### Suggested change
Create a `resolvedUrl` promise (stringified) and:
- in the `.then(...)` handler, set `capabilities.set('se:remoteUrl', resolvedString)`
- use the same promise to build the `HttpClient`
This ensures the capability is set for string, Promise, and URL inputs without racing session creation.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| { | ||
| Dictionary<string, object> matchCapabilities = this.GetCapabilitiesDictionary(capabilities); | ||
|
|
||
| if (this.CommandExecutor is Remote.HttpCommandExecutor httpExecutor) | ||
| { | ||
| matchCapabilities["se:remoteUrl"] = httpExecutor.RemoteServerUri.AbsoluteUri; | ||
| } | ||
|
|
||
| List<object> firstMatchCapabilitiesList = new List<object>(); | ||
| firstMatchCapabilitiesList.Add(matchCapabilities); | ||
|
|
There was a problem hiding this comment.
4. Remotesessionsettings omits remoteurl 🐞 Bug ≡ Correctness
In .NET WebDriver.StartSession, se:remoteUrl is injected only for non-RemoteSessionSettings capabilities; when callers use RemoteSessionSettings, the payload is sent via remoteSettings.ToDictionary() with no injection. This makes se:remoteUrl inconsistent across .NET session creation APIs and can break reachable BiDi/CDP/VNC URL construction behind proxies/NAT unless users manually add the metadata setting.
Agent Prompt
### Issue description
`se:remoteUrl` is added only in the non-`RemoteSessionSettings` branch. When `RemoteSessionSettings` is used, the request bypasses the injection and omits `se:remoteUrl`.
### Issue Context
`RemoteSessionSettings` supports adding metadata settings via `AddMetadataSetting` and serializes them in `ToDictionary()`. The injection should be applied in this branch too (ideally only if the user hasn’t already supplied `se:remoteUrl`).
### Fix Focus Areas
- dotnet/src/webdriver/WebDriver.cs[603-623]
- dotnet/src/webdriver/Remote/RemoteSessionSettings.cs[111-134]
- dotnet/src/webdriver/Remote/RemoteSessionSettings.cs[225-251]
### Suggested change
In the `else` branch:
- if `CommandExecutor` is `Remote.HttpCommandExecutor` and `remoteSettings["se:remoteUrl"]` is null (or `GetCapability("se:remoteUrl")` is null), call `remoteSettings.AddMetadataSetting("se:remoteUrl", httpExecutor.RemoteServerUri.AbsoluteUri)` before `ToDictionary()`.
- avoid overwriting an explicitly supplied `se:remoteUrl`.
ⓘ 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 2095666 |
2095666 to
66d29a8
Compare
Code Review by Qodo
1. Ruby http_client NoMethodError
|
| WebDriver(options=options) | ||
| expected_params = {"capabilities": {"firstMatch": [{}], "alwaysMatch": w3c_caps}} | ||
| mock.assert_called_with(Command.NEW_SESSION, expected_params) | ||
| command, params = mock.call_args[0] |
There was a problem hiding this comment.
1. se:remoteurl lacks assertion test 📘 Rule violation ☼ Reliability
Python WebDriver.start_session now injects se:remoteUrl, but the updated unit test explicitly removes the capability instead of asserting it is present (and correctly omitted for local drivers). This leaves the new behavior unprotected against regressions.
Agent Prompt
## Issue description
Python now adds `se:remoteUrl` during remote session creation, but there is no positive test asserting it is sent (and no test asserting it is not sent for local-driver/service sessions).
## Issue Context
The existing test was adjusted to `pop("se:remoteUrl", None)` from `alwaysMatch`, which prevents failures but does not validate the new capability injection behavior.
## Fix Focus Areas
- py/selenium/webdriver/remote/webdriver.py[394-413]
- py/test/unit/selenium/webdriver/remote/new_session_tests.py[30-43]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
|
|
||
| // A configured grid-url always wins; only when the node falls back to its auto-detected address | ||
| // (which may be unreachable behind Docker/proxy) do we use the client-advertised se:remoteUrl. | ||
| private URI resolvePublicGridUri(Capabilities caps) { |
There was a problem hiding this comment.
2. se:remoteurl rejection unlogged 📘 Rule violation ◔ Observability
LocalNode.resolvePublicGridUri silently ignores invalid/unsupported se:remoteUrl values and falls back to gridUri. This makes proxy/Docker reachability issues harder to diagnose because users get no operational signal about why their advertised URL was ignored.
Agent Prompt
## Issue description
When `se:remoteUrl` is present but invalid (bad URI syntax, missing host, non-http(s) scheme), the Node falls back to `gridUri` without any logging, which reduces diagnosability.
## Issue Context
This PR introduces `se:remoteUrl` specifically to fix unreachable returned BiDi/CDP/VNC URLs behind proxies/NAT; silent fallback makes it difficult for operators/users to understand why the feature is not taking effect.
## Fix Focus Areas
- java/src/org/openqa/selenium/grid/node/local/LocalNode.java[1303-1323]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
| caps = process_options(options, capabilities) | ||
| http_client ||= Remote::Http::Default.new(client_config: client_config) | ||
| http_client.server_url = url || client_config&.server_url || "http://#{Platform.localhost}:4444/wd/hub" | ||
| caps['se:remoteUrl'] = http_client.client_config.server_url.to_s |
There was a problem hiding this comment.
3. Ruby http_client nomethoderror 🐞 Bug ≡ Correctness
Ruby Remote::Driver now unconditionally reads http_client.client_config.server_url when setting se:remoteUrl. If callers provide a custom http_client (supported by Remote::Bridge) that doesn’t expose client_config, driver construction will raise NoMethodError before a session is created.
Agent Prompt
### Issue description
`Remote::Driver#initialize` sets `caps['se:remoteUrl']` by dereferencing `http_client.client_config.server_url`. This assumes every caller-supplied HTTP client exposes `client_config`, but the public contract allows custom clients implementing the `Http::Default` protocol (not necessarily inheriting from `Http::Common`). This can raise `NoMethodError` at construction time.
### Issue Context
The code already has the authoritative remote URL available as `url || client_config&.server_url || default`, and it already assigns that into `http_client.server_url = ...`. The capability should be derived from that known value (or guarded behind `respond_to?(:client_config)`), rather than requiring `client_config` on the HTTP client.
### Fix Focus Areas
- rb/lib/selenium/webdriver/remote/driver.rb[34-47]
- rb/lib/selenium/webdriver/remote/bridge.rb[53-60]
### Suggested fix
1. Compute a `remote_url` local variable from `url || client_config&.server_url || default`.
2. Set both:
- `http_client.server_url = remote_url`
- `caps['se:remoteUrl'] = remote_url.to_s` (ensure trailing `/` if you want consistency with `ClientConfig#server_url` normalization)
3. Do **not** dereference `http_client.client_config` unless you first check `http_client.respond_to?(:client_config)`.
4. Add a regression test that passes a minimal custom http_client implementing `server_url=`/`call` but **not** `client_config`, and asserts initialization/session creation does not crash and that `se:remoteUrl` is present.
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
🔗 Related Issues
Fixes #17782
Fixes #17788
💥 What does this PR do?
When a Grid is reached through Docker port-mapping or a reverse proxy, the BiDi/CDP/VNC URLs it hands back (
webSocketUrl,se:cdp,se:vnc) point at the Grid's own view of its address — e.g. a container-internalws://172.20.0.2:4444/...— which the client cannot reach, and causes attempted connections to error.The Grid currently addresses this with a
grid-urlconfiguration (--grid-url/SE_NODE_GRID_URL): when set to the externally-reachable address the Node uses it for the proxied URLs. Except:This PR has each bindiung advertise the url they actually reached the Grid on via a new
se:remoteUrlcapability, which the Node uses to build the proxied URLs. Additional connections to Docker then work with no extra configuration. Note that an explicitly configuredgrid-urlsetting still takes precedence.🔧 Implementation Notes
LocalNode.resolvePublicGridUri), so one server-side change makes the returned URLs reachable for every client; each binding only adds the small producer that sendsse:remoteUrl.resolvePublicGridUri: a configured public Grid URL (grid-url/ hub) is used as-is; otherwisese:remoteUrl; otherwise the Node's auto-detected address (previous behavior). So the capability only fills the gapgrid-urlleaves and never overrides operator intent.se:remoteUrlis not sent for local driver services (only for remote sessions), and when it is absent the behavior is unchanged for older clients.Host-header inference: the client is authoritative about its own reachable address, it is robust behind proxies, and it avoids changes to Grid routing.💡 Additional Considerations
https://user:key@host) are preserved in the resolved URL, identical to how an operator-configuredgrid-urlwith credentials already behaves. This keeps BiDi/CDP auth working against a basic-auth-protected Grid, where the returned websocket URL must carry the credentials. Under HTTPS these are protected in transit; the only residual is at-rest logging, consistent with the Grid already logging full capabilities today. Reducing that would be a separate, Grid-wide log-redaction change rather than mangling the URL the auth path depends on.ClientConfigto set local authentication and proxy behavior, in which case the connection URL carries no credentials andse:remoteUrlcarries none.🤖 AI assistance
🔄 Types of changes