From 0354164813a05bfe07015b95c8d7424558974939 Mon Sep 17 00:00:00 2001 From: Cedric Conday Date: Tue, 30 Jun 2026 02:54:32 +0000 Subject: [PATCH] Fix urllib3 getheaders/getheader deprecation in RESTResponse (#195) urllib3 deprecated HTTPResponse.getheaders() and getheader() in favour of accessing HTTPResponse.headers directly. RESTResponse wrapped those deprecated accessors, emitting DeprecationWarnings on every response and risking breakage when urllib3 removes them. Read headers via .headers / .headers.get() instead. The public RESTResponse.getheaders()/getheader() methods are unchanged, so callers in api_client and exceptions keep working. Adds regression tests pinning the wrapper to the non-deprecated accessors. --- tests/test_api_client/test_rest.py | 56 ++++++++++++++++++++++++++++++ xero_python/rest.py | 4 +-- 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 tests/test_api_client/test_rest.py diff --git a/tests/test_api_client/test_rest.py b/tests/test_api_client/test_rest.py new file mode 100644 index 00000000..a7c2e5f7 --- /dev/null +++ b/tests/test_api_client/test_rest.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +""" +Tests for xero_python.rest.RESTResponse header access. + +Regression test for #195: RESTResponse must read response headers via +``HTTPResponse.headers`` instead of the deprecated ``getheaders()`` / +``getheader()`` accessors, which urllib3 schedules for removal. +""" +from urllib3.response import HTTPResponse + +from xero_python.rest import RESTResponse + + +class _SpyResponse: + """Stand-in for a urllib3 response whose deprecated header accessors + raise if used, so the test pins RESTResponse to ``.headers``.""" + + def __init__(self, headers): + self.headers = headers + self.status = 200 + self.reason = "OK" + self.data = b"{}" + + def getheaders(self): # pragma: no cover - must never be called + raise AssertionError("RESTResponse must not call deprecated getheaders()") + + def getheader(self, name, default=None): # pragma: no cover + raise AssertionError("RESTResponse must not call deprecated getheader()") + + +def test_getheaders_returns_all_headers_without_deprecated_accessor(): + response = RESTResponse(_SpyResponse({"Content-Type": "application/json", + "X-Custom": "value"})) + headers = response.getheaders() + assert headers["Content-Type"] == "application/json" + assert headers["X-Custom"] == "value" + + +def test_getheader_returns_named_header_and_default_without_deprecated_accessor(): + response = RESTResponse(_SpyResponse({"X-Custom": "value"})) + assert response.getheader("X-Custom") == "value" + assert response.getheader("Missing") is None + assert response.getheader("Missing", "fallback") == "fallback" + + +def test_works_against_real_urllib3_response(): + resp = HTTPResponse( + body=b"{}", + headers={"Content-Type": "application/json", "X-Custom": "value"}, + status=200, + preload_content=False, + ) + response = RESTResponse(resp) + assert response.getheaders()["X-Custom"] == "value" + assert response.getheader("X-Custom") == "value" + assert response.getheader("Missing", "fallback") == "fallback" diff --git a/xero_python/rest.py b/xero_python/rest.py index db0e84a6..cd5b2dbb 100644 --- a/xero_python/rest.py +++ b/xero_python/rest.py @@ -45,11 +45,11 @@ def text(self): def getheaders(self): """Returns a dictionary of the response headers.""" - return self.urllib3_response.getheaders() + return self.urllib3_response.headers def getheader(self, name, default=None): """Returns a given response header.""" - return self.urllib3_response.getheader(name, default) + return self.urllib3_response.headers.get(name, default) class RESTClientObject(object):