From 210f59b14ca01f61e1fd8334d0c8c0678610e08e Mon Sep 17 00:00:00 2001 From: jguz-pubnub Date: Thu, 14 May 2026 12:25:09 +0200 Subject: [PATCH 1/2] 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/2] 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(