From 1a468fdc4f279205328385a0d3343abfb44c2f41 Mon Sep 17 00:00:00 2001 From: dajiaohuang Date: Tue, 27 Oct 2026 09:38:32 +0800 Subject: [PATCH] fix: isolate Appium session idempotency keys --- appium/webdriver/appium_connection.py | 9 ++++-- test/unit/webdriver/appium_connection_test.py | 31 +++++++++++++++++++ 2 files changed, 37 insertions(+), 3 deletions(-) diff --git a/appium/webdriver/appium_connection.py b/appium/webdriver/appium_connection.py index 559a695c0..95368f06b 100644 --- a/appium/webdriver/appium_connection.py +++ b/appium/webdriver/appium_connection.py @@ -56,10 +56,13 @@ def get_remote_connection_headers(cls, parsed_url: 'ParseResult', keep_alive: bo This method will be used in sending a request method in this class. """ + extra_headers = cls.extra_headers.copy() if parsed_url.path.endswith('/session'): # https://github.com/appium/appium-base-driver/pull/400 - cls.extra_headers[HEADER_IDEMOTENCY_KEY] = str(uuid.uuid4()) + # Keep the per-request idempotency key local: sessions may be created concurrently. + extra_headers[HEADER_IDEMOTENCY_KEY] = str(uuid.uuid4()) else: - cls.extra_headers = _get_new_headers(HEADER_IDEMOTENCY_KEY, cls.extra_headers) + extra_headers = _get_new_headers(HEADER_IDEMOTENCY_KEY, extra_headers) + cls.extra_headers = extra_headers - return {**super().get_remote_connection_headers(parsed_url, keep_alive=keep_alive), **cls.extra_headers} + return {**super().get_remote_connection_headers(parsed_url, keep_alive=keep_alive), **extra_headers} diff --git a/test/unit/webdriver/appium_connection_test.py b/test/unit/webdriver/appium_connection_test.py index c59303e73..8608e3249 100644 --- a/test/unit/webdriver/appium_connection_test.py +++ b/test/unit/webdriver/appium_connection_test.py @@ -1,4 +1,7 @@ +import threading import unittest +from concurrent.futures import ThreadPoolExecutor +from unittest.mock import patch from urllib import parse from appium.webdriver import appium_connection @@ -30,6 +33,34 @@ def test_get_remote_connection_headers(self): self.assertIsNone(headers.get('X-Idempotency-Key')) self.assertEqual(headers.get('custom'), 'header') + def test_concurrent_new_sessions_get_distinct_idempotency_keys(self): + barrier = threading.Barrier(2) + + def base_headers(cls, parsed_url, keep_alive=True): + barrier.wait(timeout=2) + return {} + + appium_connection.AppiumConnection.extra_headers = {} + with ( + patch.object( + appium_connection.RemoteConnection, + 'get_remote_connection_headers', + new=classmethod(base_headers), + ), + ThreadPoolExecutor(max_workers=2) as pool, + ): + results = list( + pool.map( + lambda _: appium_connection.AppiumConnection.get_remote_connection_headers( + parse.urlparse('http://127.0.0.1:4723/session') + ), + range(2), + ) + ) + + keys = [headers['X-Idempotency-Key'] for headers in results] + self.assertEqual(len(set(keys)), 2) + def test_remove_headers_case_insensitive(self): for h in ['X-Idempotency-Key', 'X-idempotency-Key', 'x-idempotency-key']: appium_connection.AppiumConnection.extra_headers = {h: 'value'}