In the remove_intercept method, when removing all intercepts, modifying the list while iterating over it can cause issues. Consider using a copy of the list for iteration.
for intercept_id in self.intercepts: # remove all intercepts
self.conn.execute(self.command_builder("network.removeIntercept", {"intercept": intercept_id}))
self.intercepts.remove(intercept_id)
When removing a request handler, the code calls session.subscribe instead of session.unsubscribe, which would keep the subscription active instead of removing it.
The _on_request callback function doesn't handle all possible parameters from the event data, potentially missing important request information like method and headers.
✅ Fix incorrect event unsubscriptionSuggestion Impact:The commit implemented the exact change suggested, replacing session.subscribe with session.unsubscribe when unsubscribing from events with no remaining callbacks
When unsubscribing from an event with no remaining callbacks, you should use session.unsubscribe instead of session.subscribe. The current implementation incorrectly resubscribes to events when trying to unsubscribe.
def remove_request_handler(self, event, callback_id):
"""Remove a request handler from the network.
Parameters:
----------
event_name (str): The event to unsubscribe from.
callback_id (int): The callback id to remove.
"""
try:
event_name = self.EVENTS[event]
except KeyError:
raise Exception(f"Event {event} not found")
net_event = NetworkEvent(event_name)
self.conn.remove_callback(net_event, callback_id)
self.remove_intercept(self.callbacks[callback_id])
del self.callbacks[callback_id]
self.subscriptions[event_name].remove(callback_id)
if len(self.subscriptions[event_name]) == 0:
params = {}
params["events"] = [event_name]
- self.conn.execute(self.command_builder("session.subscribe", params))
+ self.conn.execute(self.command_builder("session.unsubscribe", params))
del self.subscriptions[event_name]
[Suggestion has been applied]
Suggestion importance[1-10]: 10
__
Why: The code incorrectly calls "session.subscribe" when trying to unsubscribe from events with no remaining callbacks. This is a critical bug that would cause the opposite of the intended behavior, keeping subscriptions active instead of removing them.
High
✅ Fix list modification during iterationSuggestion Impact:The commit implemented the exact suggestion by changing the code to create a copy of the self.intercepts list before iterating over it, preventing potential issues with modifying a list during iteration
code diff:
- for intercept_id in self.intercepts: # remove all intercepts
+ intercepts_to_remove = self.intercepts.copy() # create a copy before iterating
+ for intercept_id in intercepts_to_remove: # remove all intercepts
The current implementation modifies the self.intercepts list while iterating over it, which can cause items to be skipped during removal. Create a copy of the list before iterating to avoid this issue.
def remove_intercept(self, intercept=None):
"""Remove a specific intercept, or all intercepts.
Parameters:
----------
intercept (str, optional): The intercept to remove.
Default is None.
Raises:
------
Exception: If intercept is not found.
Notes:
-----
If intercept is None, all intercepts will be removed.
"""
if intercept is None:
- for intercept_id in self.intercepts: # remove all intercepts
+ intercepts_to_remove = self.intercepts.copy() # create a copy before iterating
+ for intercept_id in intercepts_to_remove: # remove all intercepts
self.conn.execute(self.command_builder("network.removeIntercept", {"intercept": intercept_id}))
self.intercepts.remove(intercept_id)
[Suggestion has been applied]
Suggestion importance[1-10]: 9
__
Why: Modifying a list while iterating over it can cause elements to be skipped, leading to a potential bug where not all intercepts are removed. Creating a copy before iteration is a critical fix to ensure proper cleanup.
High
Add missing request fields
The _on_request method doesn't extract the method and headers fields from the request data, but these fields are used in the continue_request method. Add these missing fields to ensure proper request continuation.
def _on_request(self, event_name, callback):
"""Set a callback function to subscribe to a network event.
Parameters:
----------
event_name (str): The event to subscribe to.
callback (function): The callback function to execute on event.
Takes Request object as argument.
Returns:
-------
int : callback id
"""
event = NetworkEvent(event_name)
def _callback(event_data):
request = Request(
network=self,
request_id=event_data.params["request"].get("request", None),
body_size=event_data.params["request"].get("bodySize", None),
cookies=event_data.params["request"].get("cookies", None),
resource_type=event_data.params["request"].get("goog:resourceType", None),
+ headers=event_data.params["request"].get("headers", None),
headers_size=event_data.params["request"].get("headersSize", None),
+ method=event_data.params["request"].get("method", None),
timings=event_data.params["request"].get("timings", None),
url=event_data.params["request"].get("url", None),
)
callback(request)
[ ] Apply this suggestion <!-- /improve --apply_suggestion=2 -->
Suggestion importance[1-10]: 8
__
Why: The method doesn't extract 'method' and 'headers' fields from request data, but these fields are used in the continue_request method. This omission would cause issues when trying to continue intercepted requests with the original parameters.
Medium
Learned best practice
Add null validation for required parameters before accessing their attributes to prevent potential NoneType errors
The method doesn't validate that the request parameter is not None before accessing its request_id attribute. This could lead to a NoneType attribute error if a null request is passed. Add a null check at the beginning of the method to validate the required parameter.
def _continue_with_auth(self, request, username=None, password=None):
"""Continue with authentication.
Parameters:
----------
request (Request): The request to continue with.
username (str): The username to authenticate with.
password (str): The password to authenticate with.
Notes:
-----
If username or password is None, it attempts auth with no credentials
"""
+ if request is None:
+ raise ValueError("Request cannot be None")
params = {}
params["request"] = request.request_id
[ ] Apply this suggestion <!-- /improve --apply_suggestion=3 -->
Why: The code uses uuid.uuid4() but the uuid module is not imported, which would cause a NameError at runtime. This is a critical issue that would break the request/response handling functionality.
8
General
✅ Remove inconsistent subscription handling to prevent potential state issuesSuggestion Impact:The commit implemented a more comprehensive solution by completely refactoring the subscription handling logic. Instead of just uncommenting the session_subscribe line, it created a new implementation where both subscribe and unsubscribe are properly handled in the new code structure.
code diff:
+ params = {}
+ params["events"] = [event_name]
+ self.conn.execute(self.command_builder("session.subscribe", params))
+ self.subscriptions[event_name] = [callback_id]
+
+ self.callbacks[callback_id] = result["intercept"]
+ return callback_id
+
+ def remove_request_handler(self, event, callback_id):
+ """Remove a request handler from the network.
+
+ Parameters:
+ ----------
+ event_name (str): The event to unsubscribe from.
+ callback_id (int): The callback id to remove.
+ """
+ try:
+ event_name = self.EVENTS[event]
+ except KeyError:
+ raise Exception(f"Event {event} not found")
+
+ net_event = NetworkEvent(event_name)
+
+ self.conn.remove_callback(net_event, callback_id)
+ self.remove_intercept(self.callbacks[callback_id])
+ del self.callbacks[callback_id]
+ self.subscriptions[event_name].remove(callback_id)
+ if len(self.subscriptions[event_name]) == 0:
+ params = {}
+ params["events"] = [event_name]
+ self.conn.execute(self.command_builder("session.subscribe", params))
+ del self.subscriptions[event_name]
The session_subscribe call is commented out but the session_unsubscribe is still active, which could lead to inconsistent subscription state - either uncomment the subscribe call or remove both.
Why: The inconsistency between commented subscribe and active unsubscribe calls could lead to subscription state issues. This needs to be resolved to ensure proper event handling.
7
Security
✅ Add validation for authentication credentials to prevent silent failuresSuggestion Impact:The commit implemented credential validation in the _continue_with_auth method, but in a different way than suggested. The code was completely refactored, and the validation was implemented in the new structure.
code diff:
+ def _continue_with_auth(self, request, username, password):
"""Continue with authentication."""
- command = {'method': 'network.continueWithAuth', 'params':
- {
- 'request': request_id,
- 'action': 'provideCredentials',
- 'credentials': {
- 'type': 'password',
- 'username': username,
- 'password': password
- }
- }
- }
- self.conn.execute(self.command_iterator(command))
-
- def __on(self, event, callback):
- """Set a callback function to subscribe to a network event."""
- event = self.EVENTS.get(event, event)
- self.callbacks[event] = callback
- if len(self.subscriptions[event]) == 0:
- # session_subscribe(self.conn, event, self.__handle_event)
- return self.conn.add_callback(event, self.__handle_event)
-
- def __handle_event(self, event, data):
- """Perform callback function on event."""
- if event in self.callbacks:
- self.callbacks[event](data)
-
- def add_authentication_handler(self, username, password):
- """Adds an authentication handler."""
- self.__add_intercept(phases=[self.PHASES['auth_required']])
- self.__on('auth_required', lambda data: self.__continue_with_auth(data['request']['request'], username, password))
- self.subscriptions['auth_required'] = [username, password]
-
- def remove_authentication_handler(self):
- """Removes an authentication handler."""
- self.__remove_intercept(intercept='auth_required')
- del self.subscriptions['auth_required']
- session_unsubscribe(self.conn, self.EVENTS['auth_required'])
-
- def add_request_handler(self, callback, url_pattern=''):
- """Adds a request handler that executes a callback function when a
- request matches the given URL pattern.
-
- Parameters:
- callback (function): A function to be executed when url is matched by a URL pattern
- The callback function receives a `Response` object as its argument.
- url_pattern (str, optional): A substring to match against the response URL.
- Default is an empty string, which matches all URLs.
-
- Returns:
- str: The request ID of the intercepted response.
- """
- self.__add_intercept(phases=[self.PHASES['before_request']])
- def callback_on_url_match(data):
- if url_pattern in data['request']['url']:
- # create request object to pass to callback
- request_id = data['request'].get('requestId', uuid.uuid4())
- url = data['request'].get('url')
- method = data['request'].get('method')
- headers = data['request'].get('headers', {})
- body = data['request'].get('body', None)
- request = Request(request_id, url, method, headers, body, self)
- callback(request)
- request_id = self.__on('before_request', callback_on_url_match)
- self.callbacks[request_id] = callback
- if 'before_request' not in self.subscriptions or not self.subscriptions.get('before_request'):
- self.subscriptions['before_request'] = [request_id]
- else:
- self.subscriptions['before_request'].append(request_id)
- return request_id
-
- def remove_request_handler(self, request_id):
- """Removes a request handler."""
- self.__remove_intercept(request_id=request_id)
- self.subscriptions['before_request'].remove(request_id)
- del self.callbacks[request_id]
- if len(self.subscriptions['before_request']) == 0:
- session_unsubscribe(self.conn, self.EVENTS['before_request'])
-
- def add_response_handler(self, callback, url_pattern=''):
- """Adds a response handler that executes a callback function when a
- response matches the given URL pattern.
-
- Parameters:
- callback (function): A function to be executed when url is matched by a url_pattern
- The callback function receives a `Response` object as its argument.
- url_pattern (str, optional): A substring to match against the response URL.
- Default is an empty string, which matches all URLs.
-
- Returns:
- str: The request ID of the intercepted response.
- """
- self.__add_intercept(phases=[self.PHASES['response_started']])
- def callback_on_url_match(data):
- # create response object to pass to callback
- if url_pattern in data['response']['url']:
- request_id = data['request'].get('requestId', uuid.uuid4())
- url = data['response'].get('url')
- status_code = data['response'].get('status')
- body = data['response'].get('body', None)
- headers = data['response'].get('headers', {})
- response = Response(request_id, url, status_code, headers, body, self)
- callback(response)
- request_id = self.__on('response_started', callback_on_url_match)
- self.callbacks[request_id] = callback
- if 'response_started' not in self.subscriptions or not self.subscriptions.get('response_started'):
- self.subscriptions['response_started'] = [request_id]
- else:
- self.subscriptions['response_started'].append(request_id)
- return request_id
-
- def remove_response_handler(self, response_id):
- """Removes a response handler."""
- self.__remove_intercept(request_id=response_id)
- self.subscriptions['response_started'].remove(response_id)
- del self.callbacks[response_id]
- if len(self.subscriptions['response_started']) == 0:
- session_unsubscribe(self.conn, self.EVENTS['response_started'])
+
+ params = {}
+ params["request"] = request.request_id
+ params["action"] = "provideCredentials"
+ params["credentials"] = {"type": "password", "username": username, "password": password}
+
+ self.conn.execute(self.command_builder("network.continueWithAuth", params))
Add error handling for invalid credentials in the authentication handler to prevent silent failures when authentication fails.
def __continue_with_auth(self, request_id, username, password):
+ if not username or not password:
+ raise ValueError("Both username and password must be provided")
command = {'method': 'network.continueWithAuth', 'params':
{
'request': request_id,
'action': 'provideCredentials',
'credentials': {
'type': 'password',
'username': username,
'password': password
}
}
}
self.conn.execute(self.command_iterator(command))
[Suggestion has been applied]
Suggestion importance[1-10]: 6
Why: Adding input validation for authentication credentials would prevent potential security issues and improve error handling, though the current implementation would still work for valid credentials.
6
✅ Suggestions up to commit 01f6aaf
Category
Suggestion
Score
Best practice
✅ Add input validation to network operation methods to prevent issues caused by invalid parametersSuggestion Impact:The commit implemented input validation in the continue_request method (lines 348-351) by checking if request_id exists before proceeding. This is a partial implementation of the suggested input validation, though not as comprehensive as the original suggestion which included type checking and value validation for multiple parameters.
code diff:
+ if not self.request_id:
+ raise ValueError("Request not found.")
+
+ params = {"request": self.request_id}
Consider adding input validation for the parameters in methods like continue_response, continue_request, and add_intercept. This can help prevent issues caused by invalid input and improve the overall robustness of the code.
def continue_response(self, request_id, status_code, headers=None, body=None):
+ if not isinstance(request_id, str) or not request_id:
+ raise ValueError("request_id must be a non-empty string")
+ if not isinstance(status_code, int) or status_code < 100 or status_code >= 600:
+ raise ValueError("status_code must be a valid HTTP status code")
params = {
'requestId': request_id,
'status': status_code
}
if headers is not None:
+ if not isinstance(headers, dict):
+ raise ValueError("headers must be a dictionary")
params['headers'] = headers
if body is not None:
+ if not isinstance(body, (str, bytes)):
+ raise ValueError("body must be a string or bytes")
params['body'] = body
self.conn.execute('network.continueResponse', params)
Suggestion importance[1-10]: 8
Why: Input validation is crucial for preventing errors and ensuring that methods receive valid data. This suggestion enhances the robustness and reliability of the code by checking parameter types and values before proceeding with network operations.
8
Enhancement
✅ Implement robust error handling for network operations to improve reliability and debuggingSuggestion Impact:The commit implemented error handling in the remove_intercept method (lines 177-181) which catches exceptions from conn.execute() calls and provides informative error messages, similar to what was suggested.
Consider using a more robust error handling mechanism for the network operations. Wrap the self.conn.execute() calls in try-except blocks to catch and handle potential exceptions, providing more informative error messages or logging.
-self.conn.execute('network.continueResponse', params)
+try:
+ self.conn.execute('network.continueResponse', params)
+except Exception as e:
+ logging.error(f"Failed to continue response: {str(e)}")
+ raise
Suggestion importance[1-10]: 7
Why: Adding error handling around network operations can improve the robustness of the code by catching and logging exceptions, which aids in debugging and ensures that the application can handle unexpected issues gracefully.
7
✅ Add a method to remove event listeners, improving flexibility in event managementSuggestion Impact:The commit introduced methods to remove event handlers, such as `remove_authentication_handler`, `remove_request_handler`, and `remove_response_handler`, which align with the suggestion to provide more flexibility in managing event subscriptions.
code diff:
+ def remove_authentication_handler(self, username,):
+ """Removes an authentication handler."""
+ self.__remove_intercept(intercept='auth_required')
+ del self.subscriptions['auth_required']
+ session_unsubscribe(self.conn, self.EVENTS['auth_required'])
+
+ def add_request_handler(self, callback, url_pattern=''):
+ """
+ Adds a request handler that executes a callback function when a request matches the given URL pattern.
+
+ Parameters:
+ callback (function): A function to be executed when url is matched by a URL pattern
+ The callback function receives a `Response` object as its argument.
+ url_pattern (str, optional): A substring to match against the response URL.
+ Default is an empty string, which matches all URLs.
+
+ Returns:
+ str: The request ID of the intercepted response.
+ """
+ self.__add_intercept(phases=[self.PHASES['before_request']])
+ def callback_on_url_match(data):
+ if url_pattern in data['request']['url']:
+ # create request object to pass to callback
+ request_id = data['request'].get('requestId')
+ url = data['request'].get('url')
+ method = data['request'].get('method')
+ headers = data['request'].get('headers', {})
+ body = data['request'].get('postData', None)
+ request = Request(request_id, url, method, headers, body, self)
+ callback(request)
+ self.__on('before_request', callback_on_url_match)
+ self.callbacks[request_id] = callback
+ if 'before_request' not in self.subscriptions or not self.subscriptions.get('before_request'):
+ self.subscriptions['before_request'] = [request_id]
+ else:
+ self.subscriptions['before_request'].append(request_id)
+ return request_id
+
+ def remove_request_handler(self, request_id):
+ """Removes a request handler."""
+ self.__remove_intercept(request_id=request_id)
+ self.subscriptions['before_request'].remove(request_id)
+ del self.callbacks[request_id]
+ if len(self.subscriptions['before_request']) == 0:
+ session_unsubscribe(self.conn, self.EVENTS['before_request'])
+
+ def add_response_handler(self, callback, url_pattern=''):
+ """
+ Adds a response handler that executes a callback function when a response matches the given URL pattern.
+
+ Parameters:
+ callback (function): A function to be executed when url is matched by a url_pattern
+ The callback function receives a `Response` object as its argument.
+ url_pattern (str, optional): A substring to match against the response URL.
+ Default is an empty string, which matches all URLs.
+
+ Returns:
+ str: The request ID of the intercepted response.
+ """
+ self.__add_intercept(phases=[self.PHASES['response_started']])
+ def callback_on_url_match(data):
+ # create response object to pass to callback
+ if url_pattern in data['response']['url']:
+ request_id = data['request'].get('requestId')
+ url = data['response'].get('url')
+ status_code = data['response'].get('status')
+ body = data['response'].get('body', None)
+ headers = data['response'].get('headers', {})
+ response = Response(request_id, url, status_code, headers, body, self)
+ callback(response)
+ self.__on('response_started', callback_on_url_match)
+ self.callbacks[request_id] = callback
+ if 'response_started' not in self.subscriptions or not self.subscriptions.get('response_started'):
+ self.subscriptions['response_started'] = [request_id]
+ else:
+ self.subscriptions['response_started'].append(request_id)
+ return request_id
+
+ def remove_response_handler(self, response_id):
+ """Removes a response handler."""
+ self.__remove_intercept(request_id=response_id)
+ self.subscriptions['response_started'].remove(response_id)
+ del self.callbacks[response_id]
+ if len(self.subscriptions['response_started']) == 0:
+ session_unsubscribe(self.conn, self.EVENTS['response_started'])
Consider implementing a method to remove all event listeners for a specific event or all events. This would provide more flexibility in managing event subscriptions and prevent potential memory leaks.
def on(self, event, callback):
event = self.EVENTS.get(event, event)
self.callbacks[event] = callback
session_subscribe(self.conn, event, self.handle_event)
+def off(self, event=None):
+ if event:
+ event = self.EVENTS.get(event, event)
+ if event in self.callbacks:
+ del self.callbacks[event]
+ session_unsubscribe(self.conn, event, self.handle_event)
+ else:
+ for event in list(self.callbacks.keys()):
+ session_unsubscribe(self.conn, event, self.handle_event)
+ self.callbacks.clear()
+
Suggestion importance[1-10]: 7
Why: Implementing a method to remove event listeners provides better control over event management and can prevent potential memory leaks. This enhancement increases the flexibility and maintainability of the code.
7
✅ Expand test coverage to include various scenarios and edge cases for network operationsSuggestion Impact:The commit implements expanded test coverage by adding multiple new test cases for network operations, including request handlers, response handlers, and authentication scenarios. While not exactly matching the suggested parametrized test, it achieves the same goal of comprehensive testing.
Enhance the test coverage by adding more comprehensive test cases for different scenarios, including edge cases and error conditions. This will help ensure the robustness of the network functionality.
Why: Enhancing test coverage by including different scenarios and edge cases can significantly improve the reliability of the network functionality. This suggestion is valuable for ensuring that the code behaves correctly under various conditions.
6
✅ Suggestions up to commit 14bf971
Category
Suggestion
Score
Possible issue
Validate input parameters to prevent potential issues with invalid data being passed to the BiDi protocol
Consider implementing input validation for the add_intercept method to ensure that the provided phases are valid according to the PHASES dictionary. This can prevent potential issues with invalid phase names being passed to the BiDi protocol.
def add_intercept(self, phases=None, contexts=None, url_patterns=None):
if phases is None:
phases = []
+ else:
+ valid_phases = set(self.PHASES.values())
+ if not all(phase in valid_phases for phase in phases):
+ raise ValueError(f"Invalid phase(s) provided. Valid phases are: {', '.join(valid_phases)}")
params = {
'phases': phases,
'contexts': contexts,
'urlPatterns': url_patterns
}
self.conn.execute('network.addIntercept', params)
Suggestion importance[1-10]: 8
Why: Implementing input validation for the add_intercept method is a valuable enhancement that prevents invalid phase names from causing issues. This suggestion addresses a potential bug and improves the method's robustness.
8
Enhancement
✅ Implement error handling for network operations to improve robustness and prevent unexpected crashesSuggestion Impact:The commit implemented error handling in the remove_intercept method (lines 191-195) which catches exceptions when executing network operations, similar to what was suggested. While not implementing the exact try-except pattern in continue_response, it added error handling to network operations.
Consider using a more robust error handling mechanism for the network operations. Wrap the self.conn.execute() calls in try-except blocks to catch and handle potential exceptions, ensuring graceful error handling and preventing crashes.
def continue_response(self, request_id, status_code, headers=None, body=None):
params = {
'requestId': request_id,
'status': status_code
}
if headers is not None:
params['headers'] = headers
if body is not None:
params['body'] = body
- self.conn.execute('network.continueResponse', params)
+ try:
+ self.conn.execute('network.continueResponse', params)
+ except Exception as e:
+ logging.error(f"Error in continue_response: {str(e)}")
+ raise
Suggestion importance[1-10]: 7
Why: Adding error handling to network operations can improve the robustness of the code by preventing crashes due to unhandled exceptions. This suggestion is relevant and enhances the reliability of the continue_response method.
7
✅ Enhance test assertions to provide more comprehensive verification of network interception behaviorSuggestion Impact:The commit added more comprehensive assertions to verify network interception behavior, such as checking the status of responses and the method of requests, which aligns with the suggestion to enhance test assertions.
Consider adding more comprehensive assertions to verify the behavior of network interceptions. This could include checking the content of intercepted requests or responses, verifying headers, or ensuring that the correct number of interceptions occur.
Why: Adding more comprehensive assertions in tests improves the verification of network interception behavior, ensuring the tests are more thorough and reliable. This suggestion enhances the quality of the test suite.
7
Best practice
Use a context manager for WebDriver instances to ensure proper resource cleanup
Consider using a context manager for the WebDriver instance to ensure proper cleanup, even if an exception occurs during the test. This can help prevent resource leaks and improve test reliability.
Why: Using a context manager for the WebDriver instance ensures proper cleanup and resource management, even if exceptions occur. This suggestion follows best practices for resource handling in tests.
I'm not a Python expert but I ended up being the person who drafted the initial BiDi implementation. I tried to avoid using the async/await pattern because I assumed that it can't be used in a sync code that the majority of Python clients of the library do. Is there any reason to use the pattern in this PR?
It has pros and cons, but I think you're right that it isn't needed and could be more trouble than it's worth. I'll remove it for the time being and if we decide later it's an easy shift.
Can we add some high-level API examples and tests per an original issue?
driver.network.add_request_handler(...)
driver.network.add_response_handler(...)
This would allow us to surface the API usage and make sure it works nicely via lambdas.
Could you provide a bit more guidance here? What would those two tests entail that isn't already being covered? the tests I added are an expanded version of the .spec from the ruby implementation
I think the current tests expose the low-level API that users are not supposed to call. The high-level API unfortunately is not implemented in any bindings so there is no example to follow. However, the proposal is to allow the add_request_handler to accept a matcher (e.g. URL filter for what requests should be handled) and a callback that received the request to handle. This callable is responsible for continuing/failing the request optionally allowing to mutate it (e.g. add headers).
I think the current tests expose the low-level API that users are not supposed to call. The high-level API unfortunately is not implemented in any bindings so there is no example to follow. However, the proposal is to allow the add_request_handler to accept a matcher (e.g. URL filter for what requests should be handled) and a callback that received the request to handle. This callable is responsible for continuing/failing the request optionally allowing to mutate it (e.g. add headers).
(selenium_dev_curr) (base) simonbenzer@Simons-MBP selenium % bazel test //py:bidi_network_tests
WARNING: Ignoring JAVA_HOME, because it must point to a JDK, not a JRE.
Starting local Bazel server and connecting to it...
WARNING: For repository 'aspect_bazel_lib', the root module requires module version [email protected], but got [email protected] in the resolved dependency graph.
INFO: Analyzed target //py:bidi_network_tests (395 packages loaded, 10836 targets configured).
INFO: Found 1 test target...
Target //py:bidi_network_tests up-to-date:
bazel-bin/py/bidi_network_tests
INFO: Elapsed time: 8.816s, Critical Path: 1.27s
INFO: 29 processes: 4 internal, 25 darwin-sandbox.
INFO: Build completed successfully, 29 total actions
//py:bidi_network_tests PASSED in 0.7s
Interesting. I think I understood - what do you think about this most recent push?
Much closer to what we are aiming for! Let's add some abstractions (Request/Response) objects, move continuation there, and write more tests. I've left more detailed comments in the PR review.
Not quite, we need to make Request/Response be passed to the callbacks instead of plain event, see https://github.com/SeleniumHQ/selenium/pull/14592#issuecomment-2477187410 for the example.
Not quite, we need to make Request/Response be passed to the callbacks instead of plain event, see #14592 (comment) for the example.
Hmm... Seems I misunderstood. Let me see if I got it now:
move add_response_handler, remove_response_handler, add_request_handler, and remove_request_handler back into Network class
in the callback_on_url_match() wrapper function inside each add_handler:
def callback_on_url_match(data):
if url_pattern in data['request']['url']:
callback(data)
it should create a request/response object, then perform the callback on that object instead of on the event itself.
3. The response and request classes should just be simple objects with the continue method (and mark continue public). e.g.
class Request:
def __init__(self, request_id, url, method, headers, body, network: Network):
self.request_id = request_id
self.url = url
self.method = method
self.headers = headers
self.body = body
self.network = network
def continue_request(self):
"""Continue after sending a request."""
params = {
'requestId': self.request_id
}
if self.url is not None:
params['url'] = url
if self.method is not None:
params['method'] = method
if self.headers is not None:
params['headers'] = headers
if self.postData is not None:
params['postData'] = postData
self.network.conn.execute('network.continueRequest', params)
Current status: Only 1 test is currently passing (test_network_initialized()), however the browser now recognizes commands as legitimate seemingly. I am not sure why callbacks are not being called, as the returned request_ids from add_request/response_handler() are Null
Possible causes:
race-conditions (should we make implementation async again?)
incorrect implementation of add_intercept (could be why auth handler is not working in pages.load(basicAuth))
@shbenzer You might need to add some waits to give the time for the callback to be called. Start by adding sleep(5) and see if it stabilizes the test. If yes, you can figure out a better waiting strategy. If no, there is probably something wrong with the implementation itself.
@shbenzer Any chance you could take a look at https://github.com/SeleniumHQ/selenium/pull/14900? The implementation @aguspe made works and you can follow it to make the Python bindings part.
Oh sorry @shbenzer I think I got your notification lost and I marked everything as read :)
I can give it a deeper look over the weekend, but maybe the issue is here:
def __on(self, event, callback): """Set a callback function to subscribe to a network event.""" event = self.EVENTS.get(event, event) self.callbacks[event] = callback if len(self.subscriptions[event]) == 0: session_subscribe(self.conn, event, self.__handle_event)
The part that confused me the most was not how to add a callback, but also to have the listener listening for the event, it looks right but maybe you can print out to the console the responses and the requests
I noticed a couple of errors while I was implementing it, and by debugging it that way I noticed that I was using the listener wrongly :)
Haven't had a ton of time lately, but should hopefully be able to take a good look over the next two weeks. I'm currently getting this error when trying to test though
Use --sandbox_debug to see verbose messages from the sandbox and retain the sandbox build root for debugging
In file included from external/abseil-cpp~/absl/base/log_severity.cc:15:
external/abseil-cpp~/absl/base/log_severity.h:18:10: fatal error: 'array' file not found
18 | #include <array>
| ^~~~~~~
1 error generated.
Target //py:bidi_network_tests failed to build
INFO: Elapsed time: 39.731s, Critical Path: 0.64s
INFO: 440 processes: 433 internal, 7 darwin-sandbox.
ERROR: Build did NOT complete successfully
//py:bidi_network_tests FAILED TO BUILD
Executed 0 out of 1 test: 1 fails to build.
@aguspe and @p0deje So I finally got bazel working again, and have a few thoughts on the implementation:
Bear in mind I wrote this pr 2 months ago now lol, so I'm trying to get back into this.
I don't like the command iterator. I added it because just doing self.conn.execute(command) wasn't working because it was requiring an iterator to execute. script.py uses self.conn.execute(session_subscribe(LogEntryAdded.event_class)), can we utilize session_subscribe like this for network? execute() takes in a command and serializes it itself, so I'm not sure if session_subscribe() will accomplish what we need. but maybe add_callback() would (see 2)
the ruby bindings use the add_callback() function from the bidi object, the equivalent in python is self.conn.add_callback() and it's used by script.py. Do we think we can shift to use that instead? Also, do we think we should be storing the callbacks inside the network object instead of using that functionality in the conn object? (or evend do both?)
The problems occur/tests fail in the communication with the bidi connection - it's not able to add, remove, continue intercepts/auth. The only thing performing correctly is the establishment of the connection. I think the communication/command problems would either be fixed or made easier to solve by points 1 and 2.