From 210f59b14ca01f61e1fd8334d0c8c0678610e08e Mon Sep 17 00:00:00 2001 From: jguz-pubnub Date: Thu, 14 May 2026 12:25:09 +0200 Subject: [PATCH 1/4] Add HTTP/2 support and protocol version logging - feat: enable HTTP/2 negotiation on the synchronous httpx handler - feat: add http_version field to ResponseInfo - feat: log negotiated protocol version at DEBUG level across all request handlers --- pubnub/request_handlers/async_aiohttp.py | 4 +++- pubnub/request_handlers/async_httpx.py | 4 +++- pubnub/request_handlers/httpx.py | 8 +++++--- pubnub/request_handlers/requests.py | 7 ++++++- pubnub/structures.py | 4 +++- 5 files changed, 20 insertions(+), 7 deletions(-) diff --git a/pubnub/request_handlers/async_aiohttp.py b/pubnub/request_handlers/async_aiohttp.py index dc3d0a91..bec34422 100644 --- a/pubnub/request_handlers/async_aiohttp.py +++ b/pubnub/request_handlers/async_aiohttp.py @@ -146,7 +146,8 @@ async def async_request(self, options_func, cancellation_event): uuid=uuid, auth_key=auth_key, client_request=None, - client_response=response + client_response=response, + http_version=f"HTTP/{response.version.major}.{response.version.minor}" if response.version else None ) # if body is not None and len(body) > 0 and not options.non_json_response: @@ -178,6 +179,7 @@ async def async_request(self, options_func, cancellation_event): data = "N/A" logger.debug(data) + logger.debug("PubNub request completed: operation=%s protocol=%s" % (options.operation_type, response_info.http_version)) if response.status not in (200, 307, 204): diff --git a/pubnub/request_handlers/async_httpx.py b/pubnub/request_handlers/async_httpx.py index acaf574f..005406b3 100644 --- a/pubnub/request_handlers/async_httpx.py +++ b/pubnub/request_handlers/async_httpx.py @@ -156,7 +156,8 @@ async def async_request(self, options_func, cancellation_event): uuid=uuid, auth_key=auth_key, client_request=None, - client_response=response + client_response=response, + http_version=response.http_version ) # if body is not None and len(body) > 0 and not options.non_json_response: @@ -188,6 +189,7 @@ async def async_request(self, options_func, cancellation_event): data = "N/A" logger.debug(data) + logger.debug("PubNub request completed: operation=%s protocol=%s" % (options.operation_type, response_info.http_version)) if response.status_code not in (200, 307, 204): diff --git a/pubnub/request_handlers/httpx.py b/pubnub/request_handlers/httpx.py index dc743383..683a4679 100644 --- a/pubnub/request_handlers/httpx.py +++ b/pubnub/request_handlers/httpx.py @@ -28,8 +28,7 @@ class HttpxRequestHandler(BaseRequestHandler): ENDPOINT_THREAD_COUNTER: int = 0 def __init__(self, pubnub): - self.session = httpx.Client() - + self.session = httpx.Client(http2=True) self.pubnub = pubnub async def async_request(self, options_func, cancellation_event): @@ -166,7 +165,8 @@ def _build_envelope(self, p_options, e_options): origin=res.url.host, uuid=uuid, auth_key=auth_key, - client_request=res.request + client_request=res.request, + http_version=res.http_version ) if res.status_code not in [200, 204, 307]: @@ -267,6 +267,8 @@ def _invoke_request(self, p_options, e_options, base_origin): try: res = self.session.request(**args) + logger.debug("PubNub request completed: operation=%s protocol=%s" % (e_options.operation_type, res.http_version)) + # Safely access response text - read content first for streaming responses try: logger.debug("GOT %s" % res.text) diff --git a/pubnub/request_handlers/requests.py b/pubnub/request_handlers/requests.py index 14de1448..5e41cd4c 100644 --- a/pubnub/request_handlers/requests.py +++ b/pubnub/request_handlers/requests.py @@ -174,7 +174,8 @@ def _build_envelope(self, p_options, e_options): origin=url.hostname, uuid=uuid, auth_key=auth_key, - client_request=res.request + client_request=res.request, + http_version=f"HTTP/{res.raw.version // 10}.{res.raw.version % 10}" if res.raw and res.raw.version else None ) if not res.ok: @@ -269,6 +270,10 @@ def _invoke_request(self, p_options, e_options, base_origin): try: res = self.session.request(**args) logger.debug("GOT %s" % res.text) + + http_ver = f"HTTP/{res.raw.version // 10}.{res.raw.version % 10}" if res.raw and res.raw.version else "unknown" + logger.debug("PubNub request completed: operation=%s protocol=%s" % (e_options.operation_type, http_ver)) + except requests.exceptions.ConnectionError as e: raise PubNubException( pn_error=PNERR_CONNECTION_ERROR, diff --git a/pubnub/structures.py b/pubnub/structures.py index a7ca2bb9..af7575c3 100644 --- a/pubnub/structures.py +++ b/pubnub/structures.py @@ -80,7 +80,8 @@ def __init__(self, headers, pn_config): class ResponseInfo(object): - def __init__(self, status_code, tls_enabled, origin, uuid, auth_key, client_request, client_response=None): + def __init__(self, status_code, tls_enabled, origin, uuid, auth_key, client_request, + client_response=None, http_version=None): self.status_code = status_code self.tls_enabled = tls_enabled self.origin = origin @@ -88,6 +89,7 @@ def __init__(self, status_code, tls_enabled, origin, uuid, auth_key, client_requ self.auth_key = auth_key self.client_request = client_request self.client_response = client_response + self.http_version = http_version class Envelope(object): From 00ff8bfbba00d353c533963da4573eae4922cf18 Mon Sep 17 00:00:00 2001 From: jguz-pubnub Date: Thu, 14 May 2026 12:33:44 +0200 Subject: [PATCH 2/4] Fix lint errors --- pubnub/request_handlers/async_aiohttp.py | 5 ++++- pubnub/request_handlers/async_httpx.py | 5 ++++- pubnub/request_handlers/httpx.py | 5 ++++- pubnub/request_handlers/requests.py | 15 ++++++++++++--- 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/pubnub/request_handlers/async_aiohttp.py b/pubnub/request_handlers/async_aiohttp.py index bec34422..df2430eb 100644 --- a/pubnub/request_handlers/async_aiohttp.py +++ b/pubnub/request_handlers/async_aiohttp.py @@ -179,7 +179,10 @@ async def async_request(self, options_func, cancellation_event): data = "N/A" logger.debug(data) - logger.debug("PubNub request completed: operation=%s protocol=%s" % (options.operation_type, response_info.http_version)) + logger.debug( + "PubNub request completed: operation=%s protocol=%s" + % (options.operation_type, response_info.http_version) + ) if response.status not in (200, 307, 204): diff --git a/pubnub/request_handlers/async_httpx.py b/pubnub/request_handlers/async_httpx.py index 005406b3..822f984f 100644 --- a/pubnub/request_handlers/async_httpx.py +++ b/pubnub/request_handlers/async_httpx.py @@ -189,7 +189,10 @@ async def async_request(self, options_func, cancellation_event): data = "N/A" logger.debug(data) - logger.debug("PubNub request completed: operation=%s protocol=%s" % (options.operation_type, response_info.http_version)) + logger.debug( + "PubNub request completed: operation=%s protocol=%s" + % (options.operation_type, response_info.http_version) + ) if response.status_code not in (200, 307, 204): diff --git a/pubnub/request_handlers/httpx.py b/pubnub/request_handlers/httpx.py index 683a4679..24078ba0 100644 --- a/pubnub/request_handlers/httpx.py +++ b/pubnub/request_handlers/httpx.py @@ -267,7 +267,10 @@ def _invoke_request(self, p_options, e_options, base_origin): try: res = self.session.request(**args) - logger.debug("PubNub request completed: operation=%s protocol=%s" % (e_options.operation_type, res.http_version)) + logger.debug( + "PubNub request completed: operation=%s protocol=%s" + % (e_options.operation_type, res.http_version) + ) # Safely access response text - read content first for streaming responses try: diff --git a/pubnub/request_handlers/requests.py b/pubnub/request_handlers/requests.py index 5e41cd4c..19df14b9 100644 --- a/pubnub/request_handlers/requests.py +++ b/pubnub/request_handlers/requests.py @@ -175,7 +175,10 @@ def _build_envelope(self, p_options, e_options): uuid=uuid, auth_key=auth_key, client_request=res.request, - http_version=f"HTTP/{res.raw.version // 10}.{res.raw.version % 10}" if res.raw and res.raw.version else None + http_version=( + f"HTTP/{res.raw.version // 10}.{res.raw.version % 10}" + if res.raw and res.raw.version else None + ) ) if not res.ok: @@ -271,8 +274,14 @@ def _invoke_request(self, p_options, e_options, base_origin): res = self.session.request(**args) logger.debug("GOT %s" % res.text) - http_ver = f"HTTP/{res.raw.version // 10}.{res.raw.version % 10}" if res.raw and res.raw.version else "unknown" - logger.debug("PubNub request completed: operation=%s protocol=%s" % (e_options.operation_type, http_ver)) + http_ver = ( + f"HTTP/{res.raw.version // 10}.{res.raw.version % 10}" + if res.raw and res.raw.version else "unknown" + ) + logger.debug( + "PubNub request completed: operation=%s protocol=%s" + % (e_options.operation_type, http_ver) + ) except requests.exceptions.ConnectionError as e: raise PubNubException( From dcc328565f932dc0812f6c8db7fd37a64775f873 Mon Sep 17 00:00:00 2001 From: jguz-pubnub Date: Mon, 25 May 2026 11:18:41 +0200 Subject: [PATCH 3/4] Update requirements-dev.txt --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index a5e406e4..403f6950 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,7 +5,7 @@ flake8>=7.1.2 pytest>=8.3.5 pytest-asyncio>=1.0.0 httpx>=0.28 -h2>=4.1 +h2>=4.3 requests>=2.32.2 aiohttp>=3.10.11 cbor2>=5.6 From edba4bfc1112b582391a4d1ad063e0e4852addc0 Mon Sep 17 00:00:00 2001 From: jguz-pubnub Date: Wed, 27 May 2026 14:00:48 +0200 Subject: [PATCH 4/4] Fixes according to review --- pubnub/request_handlers/async_aiohttp.py | 6 +----- pubnub/request_handlers/async_httpx.py | 9 +++------ pubnub/request_handlers/httpx.py | 15 ++++----------- pubnub/request_handlers/requests.py | 7 +------ setup.py | 2 +- 5 files changed, 10 insertions(+), 29 deletions(-) diff --git a/pubnub/request_handlers/async_aiohttp.py b/pubnub/request_handlers/async_aiohttp.py index e727e323..3b057b66 100644 --- a/pubnub/request_handlers/async_aiohttp.py +++ b/pubnub/request_handlers/async_aiohttp.py @@ -173,11 +173,7 @@ async def async_request(self, options_func, cancellation_event): else: data = "N/A" - logger.debug(data) - logger.debug( - "PubNub request completed: operation=%s protocol=%s" - % (options.operation_type, response_info.http_version) - ) + logger.debug("[%s %s] %s" % (options.operation_type, response_info.http_version, data)) if response.status not in (200, 307, 204): diff --git a/pubnub/request_handlers/async_httpx.py b/pubnub/request_handlers/async_httpx.py index fc23d65f..512e4a56 100644 --- a/pubnub/request_handlers/async_httpx.py +++ b/pubnub/request_handlers/async_httpx.py @@ -43,7 +43,8 @@ def __init__(self, pubnub): async def create_session(self): self._session = httpx.AsyncClient( timeout=httpx.Timeout(self.pubnub.config.connect_timeout), - transport=self._connector + transport=self._connector, + http2=True ) async def close_session(self): @@ -225,11 +226,7 @@ async def async_request(self, options_func, cancellation_event): else: data = "N/A" - logger.debug(data) - logger.debug( - "PubNub request completed: operation=%s protocol=%s" - % (options.operation_type, response_info.http_version) - ) + logger.debug("[%s %s] %s" % (options.operation_type, response_info.http_version, data)) if response.status_code not in (200, 307, 204): diff --git a/pubnub/request_handlers/httpx.py b/pubnub/request_handlers/httpx.py index 01db83f0..d9615072 100644 --- a/pubnub/request_handlers/httpx.py +++ b/pubnub/request_handlers/httpx.py @@ -174,7 +174,7 @@ def _ensure_session(self): with self._session_lock: if self._session is None or self._session.is_closed: logger.debug("Creating new HTTP session") - self._session = httpx.Client() + self._session = httpx.Client(http2=True) return self._session def close(self): @@ -434,20 +434,13 @@ def _invoke_request(self, p_options, e_options, base_origin): try: res = session.request(**args) - logger.debug( - "PubNub request completed: operation=%s protocol=%s" - % (e_options.operation_type, res.http_version) - ) - # Safely access response text - read content first for streaming responses try: - logger.debug("GOT %s" % res.text) + logger.debug("[%s %s] %s" % (e_options.operation_type, res.http_version, res.text)) except httpx.ResponseNotRead: - # For streaming responses, we need to read first - logger.debug("GOT %s" % res.content.decode('utf-8', errors='ignore')) + logger.debug("[%s %s] %s" % (e_options.operation_type, res.http_version, res.content.decode('utf-8', errors='ignore'))) except Exception as e: - # Fallback logging in case of any response reading issues - logger.debug("GOT response (content access failed: %s)" % str(e)) + logger.debug("[%s %s] (content access failed: %s)" % (e_options.operation_type, res.http_version, str(e))) except httpx.ConnectError as e: if use_watchdog and self._watchdog.triggered: diff --git a/pubnub/request_handlers/requests.py b/pubnub/request_handlers/requests.py index 19df14b9..781d65ec 100644 --- a/pubnub/request_handlers/requests.py +++ b/pubnub/request_handlers/requests.py @@ -272,16 +272,11 @@ def _invoke_request(self, p_options, e_options, base_origin): try: res = self.session.request(**args) - logger.debug("GOT %s" % res.text) - http_ver = ( f"HTTP/{res.raw.version // 10}.{res.raw.version % 10}" if res.raw and res.raw.version else "unknown" ) - logger.debug( - "PubNub request completed: operation=%s protocol=%s" - % (e_options.operation_type, http_ver) - ) + logger.debug("[%s %s] %s" % (e_options.operation_type, http_ver, res.text)) except requests.exceptions.ConnectionError as e: raise PubNubException( diff --git a/setup.py b/setup.py index bae45263..880b9cef 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ install_requires=[ 'pycryptodomex>=3.3', 'httpx>=0.28,<1.0', - 'h2>=4.1', + 'h2>=4.3', 'requests>=2.32.2', 'aiohttp>3.10.11', 'cbor2>=5.6'