Skip to content

[py] send extra headers with network.setExtraHeaders - #18117

Open
yuriy-mikityuk wants to merge 7 commits into
SeleniumHQ:trunkfrom
yuriy-mikityuk:fix-py-extra-headers-set-extra-headers
Open

yuriy-mikityuk wants to merge 7 commits into
SeleniumHQ:trunkfrom
yuriy-mikityuk:fix-py-extra-headers-set-extra-headers

Conversation

@yuriy-mikityuk

@yuriy-mikityuk yuriy-mikityuk commented Oct 2, 2026 •

Copy link
Copy Markdown

🔗 Related Issues

Fixes #18116

💥 What does this PR do?

add_extra_header() paused every request with a match-everything intercept and continued it with the header list rebuilt from the beforeRequestSent event. In Chrome that has two consequences: driver.get() hangs until the page load timeout, and headers the event does not list are dropped, so adding one header removes Accept from navigation requests. test_handler_with_classic_navigation already skips Chrome and Edge because interception does not work with classic navigation there, and add_extra_header() put every caller into that state without them asking for interception.

network.setExtraHeaders is in the spec and set_extra_headers() already wraps it, so the registry now keeps the store and pushes it to the browser. The browser adds the headers itself, nothing is paused, and the public API does not change.

🔧 Implementation Notes

The extra-headers intercept, its subscription bookkeeping and the _before_resolve merging are gone, so the registry is about request handlers again. The store stays in the registry because clear_request_handlers() has to keep the headers.

I checked the semantics of the native command in Chrome 154 and Firefox 154: adding a header of the same name replaces the previous one, an empty list clears them, and it composes with request handlers (they see the extra header in beforeRequestSent, and it is still sent when a handler rewrites headers with set_headers).

Verified locally on macOS: bazel test //py:unit passes (35/35), and so do the bidi/network_tests targets for chrome-bidi and firefox-bidi. With the old implementation the new browser test fails on Chrome with a 60 second timeout, which is the hang it guards against.

Cross-binding: .NET and Ruby expose setExtraHeaders directly and have no intercept-based helper, so this brings Python in line with them.

🤖 AI assistance

  • No substantial AI assistance used
  • AI assisted (complete below)
    • Tool(s): Claude code
    • What was generated: a draft of the change, the tests and this description
    • I reviewed all AI output and can explain the change

💡 Additional Considerations

  • Touches BiDi semantics, which AGENTS.md lists as high risk.
  • network.setExtraHeaders has to be supported by the browser. It works in Chrome 154 and Firefox 154, but the old implementation only needed interception, so on a browser without the command add_extra_header() now raises. Happy to add a fallback if you would rather keep that working.
  • The command is sent without contexts / userContexts, which matches the previous session-wide behaviour. Exposing that scoping could be a follow-up.
  • Explicit request handlers combined with driver.get() still hang in Chrome ([🐛 Bug]: network.continueRequest() hangs navigation in Chrome #17373, https://issues.chromium.org/issues/425906330). This PR only stops extra headers from needing interception at all.

🔄 Types of changes

  • Bug fix (backwards compatible)

add_extra_header() paused every request with a match-everything
intercept and continued it with the headers from the beforeRequestSent
event. In Chrome that hangs driver.get(), because ChromeDriver does not
process network.continueRequest while a classic command is running, and
it drops the headers the event does not list, such as Accept.

network.setExtraHeaders is in the spec and set_extra_headers() already
wraps it, so the store is pushed to the browser instead and the browser
adds the headers itself. Nothing is paused and the public API does not
change.

The browser tests navigated with browsing_context.navigate, which hid
the hang, so the new one uses driver.get() and also checks that Accept
survives.

Fixes SeleniumHQ#18116
The existing composition test only checked that the request continued
and that the handler ran. Assert that both the extra header and the
header the handler sets arrive, since the browser now adds the extra
headers after the handler's continue.
@selenium-ci selenium-ci added C-py Python Bindings B-devtools Includes everything BiDi or Chrome DevTools related labels Oct 2, 2026
@CLAassistant

CLAassistant commented Oct 2, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@yuriy-mikityuk
yuriy-mikityuk marked this pull request as ready for review October 2, 2026 10:04
@qodo-code-review

Copy link
Copy Markdown
Contributor

PR Summary by Qodo

[py] Send extra headers through BiDi instead of intercepting requests

🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Send extra headers through network.setExtraHeaders to avoid pausing requests and hanging classic
 navigation.
• Preserve browser-generated headers and keep extra headers when request handlers are cleared.
• Add browser and unit coverage for navigation, header removal, and handler composition.
Diagram

graph TD
  API["Extra header API"] --> Registry["Request registry"] --> Store[("Header store")] --> Command["setExtraHeaders"] --> Browser["Browser requests"]
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Fallback to interception on unsupported browsers
  • ➕ Preserves extra-header support where network.setExtraHeaders is unavailable.
  • ➖ Reintroduces request pausing and the classic-navigation hang on fallback paths.
  • ➖ Requires two implementations with different header semantics and additional browser coverage.

Recommendation: Use the native BiDi command as proposed: it fixes the hang and preserves browser-generated headers while retaining the public API. Consider a fallback only if supporting browsers without this command is a requirement; the previous interception approach is not a safe default.

Files changed (4) +80 / -129

Bug fix (1) +15 / -53
_network_handlers.pyPush stored extra headers directly to the browser +15/-53

Push stored extra headers directly to the browser

• Replaces the match-everything intercept and request-header merge with 'network.setExtraHeaders' after each add, remove, or clear. Request-handler subscriptions and reconciliation no longer manage extra headers.

py/private/_network_handlers.py

Tests (2) +58 / -66
network_tests.pyCover classic navigation and header-rewriting handlers +29/-0

Cover classic navigation and header-rewriting handlers

• Adds a 'driver.get()' regression test checking that extra headers arrive without losing 'Accept'. Adds a test confirming extra headers and handler-set headers both reach the server after a handler rewrites request headers.

py/test/selenium/webdriver/common/bidi/network_tests.py

bidi_network_tests.pyVerify native extra-header commands and lifecycle +29/-66

Verify native extra-header commands and lifecycle

• Replaces interception-focused assertions with checks for 'network.setExtraHeaders', case-insensitive names, removal, and clearing. Verifies that handler continuation does not inject extra headers and clearing handlers leaves browser-configured headers intact.

py/test/unit/selenium/webdriver/common/bidi_network_tests.py

Documentation (1) +7 / -10
bidi_enhancements_manifest.pyDocument browser-managed extra headers +7/-10

Document browser-managed extra headers

• Updates the generated Network API documentation to describe native header delivery and no request pausing. Clarifies that clearing request handlers leaves no request-registry intercept but preserves extra headers.

py/private/bidi_enhancements_manifest.py

@yuriy-mikityuk

Copy link
Copy Markdown
Author

Three weeks on this one, and the reason it looks stalled is probably not review: CI has never run. The three workflows are still sitting at action_required from October 2, so the only status on the PR is the CLA check. Could someone approve the run? If you would rather fold this into the get()-to-BiDi work instead, say so and I will close it, though #17715 has not moved since June.

@diemol

diemol commented Oct 29, 2026

Copy link
Copy Markdown
Member

@AutomatedTester @cgoldberg can you please review this one?

@AutomatedTester

Copy link
Copy Markdown
Member

/agentic_review

@qodo-code-review

qodo-code-review Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Remediation recommended

1. Failed removal keeps headers active 🐞 Bug ☼ Reliability
Description
remove_extra_header() deletes the stored entry before _push_extra_headers() sends the
replacement list, and clear_extra_headers() likewise clears the store before sending it. If
network.setExtraHeaders rejects either command, the browser retains the previously configured
headers while the registry records them as removed; retrying a removal then raises ValueError.
Code

py/private/_network_handlers.py[798]

+        self._push_extra_headers()
Evidence
The mutators change the only registry store before issuing the browser command, which can raise. On
a rejected removal or clear command, the browser has not applied the replacement, but the next
removal consults the already-changed store.

py/private/_network_handlers.py[793-808]
py/test/unit/selenium/webdriver/common/bidi_network_tests.py[24-43]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A rejected browser command leaves the registry’s header store inconsistent with the headers still active in the browser.
## Fix Focus Areas
- py/private/_network_handlers.py[789-808]
## Recommended Fix
Build a proposed copy of the header store, send that copy to the browser, and commit it to `extra_headers` only after the command succeeds. Apply this ordering to add, remove, and clear, and test command failures followed by retries.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


2. Headers fail on unsupported browsers 📘 Rule violation ≡ Correctness
Description
_push_extra_headers() now unconditionally calls network.setExtraHeaders instead of retaining the
previous interception path. When a browser does not support that command, add_extra_header(),
remove_extra_header(), and clear_extra_headers() raise rather than preserving their prior
behavior for those users.
Code

py/private/_network_handlers.py[R806-808]

+    def _push_extra_headers(self) -> None:
+        """Replace the browser's extra headers with the current store."""
+        self._network.set_extra_headers(headers=dict_to_headers(self.extra_headers))
Evidence
The changed registry implementation directly invokes the native command for every extra-header
update, and the generated public API documentation confirms that this command replaces the previous
mechanism. No fallback is present in the changed implementation, so browsers without the command can
no longer use the existing public methods.

AGENTS.md: Preserve Public API and ABI Compatibility and Deprecate Before Removal
py/private/_network_handlers.py[806-808]
py/private/bidi_enhancements_manifest.py[1347-1350]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

Issue description
The public extra-header APIs now depend unconditionally on `network.setExtraHeaders`, which breaks callers using browsers or browser versions that do not support the command.

Fix Focus Areas
- py/private/_network_handlers.py[806-808]
- py/private/bidi_enhancements_manifest.py[1347-1350]

Recommended Fix
Detect whether `network.setExtraHeaders` is supported and retain the prior interception-based implementation as a fallback, or provide a clear compatibility path that preserves `add_extra_header()`, `remove_extra_header()`, and `clear_extra_headers()` behavior on unsupported browsers.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


3. Directly set headers disappear on add 🐞 Bug ≡ Correctness
Description
_push_extra_headers() sends only the registry’s private extra_headers store as the complete
network.setExtraHeaders list. If a caller first configures headers through the existing low-level
Network(driver).set_extra_headers() API and then calls driver.network.add_extra_header(), the
second command replaces the first API’s headers rather than preserving them.
Code

py/private/_network_handlers.py[808]

+        self._network.set_extra_headers(headers=dict_to_headers(self.extra_headers))
Evidence
The registry tracks only its own headers before sending them as the replacement list. The existing
low-level API configures browser headers independently, and the PR’s new push replaces that
configuration; previously the high-level helper merged its headers into intercepted requests
instead.

py/private/_network_handlers.py[781-808]
py/test/selenium/webdriver/common/_bidi/network_tests.py[44-59]
py/generate_bidi.py[325-330]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The high-level registry replaces browser extra headers without preserving headers set through the low-level Python API.
## Fix Focus Areas
- py/private/_network_handlers.py[789-808]
- py/selenium/webdriver/remote/webdriver.py[1293-1302]
## Recommended Fix
Coordinate browser-side extra-header state across the low-level command and high-level helper so adding, removing, or clearing a registry header does not discard headers configured through the other API. Add a test that sets headers through both APIs and verifies both are sent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


Grey Divider

Context sources
Review mode: Auto: ⚖️ Balanced: BiDi network behavior and browser protocol semantics carry substantial runtime risk.

Grey Divider

Tip of the day
💡 Did you know, you can tweak Display settings with a live preview to see your comment before it ships

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment on lines +806 to +808
def _push_extra_headers(self) -> None:
"""Replace the browser's extra headers with the current store."""
self._network.set_extra_headers(headers=dict_to_headers(self.extra_headers))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

1. Headers fail on unsupported browsers 📘 Rule violation ≡ Correctness

_push_extra_headers() now unconditionally calls network.setExtraHeaders instead of retaining the
previous interception path. When a browser does not support that command, add_extra_header(),
remove_extra_header(), and clear_extra_headers() raise rather than preserving their prior
behavior for those users.
Agent Prompt
Issue description
The public extra-header APIs now depend unconditionally on `network.setExtraHeaders`, which breaks callers using browsers or browser versions that do not support the command.

Fix Focus Areas
- py/private/_network_handlers.py[806-808]
- py/private/bidi_enhancements_manifest.py[1347-1350]

Recommended Fix
Detect whether `network.setExtraHeaders` is supported and retain the prior interception-based implementation as a fallback, or provide a clear compatibility path that preserves `add_extra_header()`, `remove_extra_header()`, and `clear_extra_headers()` behavior on unsupported browsers.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

request.set_headers(merged)
def _push_extra_headers(self) -> None:
"""Replace the browser's extra headers with the current store."""
self._network.set_extra_headers(headers=dict_to_headers(self.extra_headers))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

2. Directly set headers disappear on add 🐞 Bug ≡ Correctness

_push_extra_headers() sends only the registry’s private extra_headers store as the complete
network.setExtraHeaders list. If a caller first configures headers through the existing low-level
Network(driver).set_extra_headers() API and then calls driver.network.add_extra_header(), the
second command replaces the first API’s headers rather than preserving them.
Agent Prompt
## Issue description
The high-level registry replaces browser extra headers without preserving headers set through the low-level Python API.
## Fix Focus Areas
- py/private/_network_handlers.py[789-808]
- py/selenium/webdriver/remote/webdriver.py[1293-1302]
## Recommended Fix
Coordinate browser-side extra-header state across the low-level command and high-level helper so adding, removing, or clearing a registry header does not discard headers configured through the other API. Add a test that sets headers through both APIs and verifies both are sent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

raise ValueError(f"Extra header '{name}' not found")
if not self.extra_headers:
self._drop_extra_headers_intercept()
self._push_extra_headers()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remediation recommended

3. Failed removal keeps headers active 🐞 Bug ☼ Reliability

remove_extra_header() deletes the stored entry before _push_extra_headers() sends the
replacement list, and clear_extra_headers() likewise clears the store before sending it. If
network.setExtraHeaders rejects either command, the browser retains the previously configured
headers while the registry records them as removed; retrying a removal then raises ValueError.
Agent Prompt
## Issue description
A rejected browser command leaves the registry’s header store inconsistent with the headers still active in the browser.
## Fix Focus Areas
- py/private/_network_handlers.py[789-808]
## Recommended Fix
Build a proposed copy of the header store, send that copy to the browser, and commit it to `extra_headers` only after the command succeeds. Apply this ordering to add, remove, and clear, and test command failures followed by retries.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗

@AutomatedTester

Copy link
Copy Markdown
Member

Please handle the qodo review comments and I'll be back to merge it . PR is definitely in the right direction so thank you!

@cgoldberg cgoldberg added the Q-qodo-review Request a Qodo review label Nov 3, 2026
@selenium-ci selenium-ci removed the Q-qodo-review Request a Qodo review label Nov 3, 2026
@cgoldberg cgoldberg added the Q-qodo-review Request a Qodo review label Nov 3, 2026
@selenium-ci

Copy link
Copy Markdown
Member

/agentic_review

@selenium-ci selenium-ci removed the Q-qodo-review Request a Qodo review label Nov 3, 2026
@SeleniumHQ SeleniumHQ deleted a comment from qodo-code-review Bot Nov 4, 2026
@SeleniumHQ SeleniumHQ deleted a comment from qodo-code-review Bot Nov 5, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

B-devtools Includes everything BiDi or Chrome DevTools related C-py Python Bindings

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[🐛 Bug]: [py] add_extra_header() hangs driver.get() and drops the Accept header in Chrome

6 participants