JMSwag: @mcsimps2 We may have hit https://github.com/nedbat/coveragepy/issues/883
JMSwag: @mcsimps2 Fixed on master. Please rebase.
codecov[bot]: # [Codecov](https://codecov.io/gh/Digital-Sapphire/PyUpdater/pull/226?src=pr&el=h1) Report
> Merging [#226](https://codecov.io/gh/Digital-Sapphire/PyUpdater/pull/226?src=pr&el=desc) into [master](https://codecov.io/gh/Digital-Sapphire/PyUpdater/commit/1abe00d602327eb106a0a220b03366eca6ea6553?src=pr&el=desc) will **increase** coverage by `0.05%`.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/Digital-Sapphire/PyUpdater/pull/226/graphs/tree.svg?width=650&token=vDPuqGLkLO&height=150&src=pr)](https://codecov.io/gh/Digital-Sapphire/PyUpdater/pull/226?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #226      +/-   ##
==========================================
+ Coverage   86.72%   86.77%   +0.05%     
==========================================
  Files          23       23              
  Lines        2335     2344       +9     
==========================================
+ Hits         2025     2034       +9     
  Misses        310      310
```


| [Impacted Files](https://codecov.io/gh/Digital-Sapphire/PyUpdater/pull/226?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pyupdater/client/patcher.py](https://codecov.io/gh/Digital-Sapphire/PyUpdater/pull/226/diff?src=pr&el=tree#diff-cHl1cGRhdGVyL2NsaWVudC9wYXRjaGVyLnB5) | `82.58% <100%> (+0.08%)` | :arrow_up: |
| [pyupdater/client/downloader.py](https://codecov.io/gh/Digital-Sapphire/PyUpdater/pull/226/diff?src=pr&el=tree#diff-cHl1cGRhdGVyL2NsaWVudC9kb3dubG9hZGVyLnB5) | `87.96% <100%> (+0.52%)` | :arrow_up: |
| [pyupdater/client/updates.py](https://codecov.io/gh/Digital-Sapphire/PyUpdater/pull/226/diff?src=pr&el=tree#diff-cHl1cGRhdGVyL2NsaWVudC91cGRhdGVzLnB5) | `77.68% <100%> (ø)` | :arrow_up: |
| [pyupdater/client/\_\_init\_\_.py](https://codecov.io/gh/Digital-Sapphire/PyUpdater/pull/226/diff?src=pr&el=tree#diff-cHl1cGRhdGVyL2NsaWVudC9fX2luaXRfXy5weQ==) | `88.41% <100%> (-0.05%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/Digital-Sapphire/PyUpdater/pull/226?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/Digital-Sapphire/PyUpdater/pull/226?src=pr&el=footer). Last update [1abe00d...9e1ed1b](https://codecov.io/gh/Digital-Sapphire/PyUpdater/pull/226?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/docs/api.md b/docs/api.md
index 9c62f39..15a8466 100644
--- a/docs/api.md
+++ b/docs/api.md
@@ -95,7 +95,7 @@ progress_hooks (list): List of callbacks
 
 data_dir (str): Path to custom update folder
 
-headers (dict): A urllib3.utils.make_headers compatible dictionary
+headers (dict): A dictionary of generic and/or urllib3.utils.make_headers compatible headers
 
 test (bool): Used to initialize a test client
 
diff --git a/pyupdater/client/__init__.py b/pyupdater/client/__init__.py
index f0785e3..4a2ecea 100644
--- a/pyupdater/client/__init__.py
+++ b/pyupdater/client/__init__.py
@@ -85,7 +85,7 @@ class Client(object):
 
     data_dir (str): Path to custom update folder
 
-    headers (dict): A urllib3.utils.make_headers compatible dictionary
+    headers (dict): A dictionary of generic and/or urllib3.utils.make_headers compatible headers
 
     test (bool): Used to initialize a test client
 
@@ -130,8 +130,6 @@ class Client(object):
                 raise SyntaxError("progress_hooks must be provided as a list.")
             self.progress_hooks += progress_hooks
 
-        obj.URLLIB3_HEADERS = headers
-
         # A super dict used to save config info & be dot accessed
         config = _Config()
         config.from_object(obj)
@@ -198,8 +196,8 @@ class Client(object):
         # The name of the key file to download
         self.key_file = settings.KEY_FILE_FILENAME
 
-        # urllib3 headers
-        self.urllib3_headers = obj.URLLIB3_HEADERS
+        # headers
+        self.headers = headers
 
         # Creating data & update directories
         self._setup()
@@ -244,7 +242,7 @@ class Client(object):
             "http_timeout": self.http_timeout,
             "max_download_retries": self.max_download_retries,
             "progress_hooks": self.progress_hooks,
-            "urllib3_headers": self.urllib3_headers,
+            "headers": self.headers,
             "verify": self.verify,
         }
 
@@ -317,7 +315,7 @@ class Client(object):
             "verify": self.verify,
             "max_download_retries": self.max_download_retries,
             "progress_hooks": list(set(self.progress_hooks)),
-            "urllib3_headers": self.urllib3_headers,
+            "headers": self.headers,
             "downloader": self.downloader,
         }
 
@@ -436,7 +434,8 @@ class Client(object):
                         vf,
                         self.update_urls,
                         verify=self.verify,
-                        urllb3_headers=self.urllib3_headers,
+                        headers=self.headers,
+                        http_timeout=self.http_timeout
                     )
                 data = fd.download_verify_return()
                 try:
@@ -468,7 +467,8 @@ class Client(object):
                     self.key_file,
                     self.update_urls,
                     verify=self.verify,
-                    urllb3_headers=self.urllib3_headers,
+                    headers=self.headers,
+                    http_timeout=self.http_timeout
                 )
             data = fd.download_verify_return()
             try:
diff --git a/pyupdater/client/downloader.py b/pyupdater/client/downloader.py
index da2d1c0..2bd3cc0 100755
--- a/pyupdater/client/downloader.py
+++ b/pyupdater/client/downloader.py
@@ -24,6 +24,7 @@
 # ------------------------------------------------------------------------------
 from __future__ import unicode_literals
 import hashlib
+import inspect
 import logging
 import os
 import time
@@ -131,7 +132,9 @@ class FileDownloader(object):
         self.content_length = None
 
         # Extra headers
-        self.headers = kwargs.get("urllb3_headers")
+        self.headers = kwargs.get("headers")
+
+        self.http_timeout = kwargs.get("http_timeout")
 
         if self.verify is True:
             self.http_pool = self._get_http_pool()
@@ -141,15 +144,25 @@ class FileDownloader(object):
     def _get_http_pool(self, secure=True):
         if secure:
             _http = urllib3.PoolManager(
-                cert_reqs=str("CERT_REQUIRED"), ca_certs=certifi.where()
+                cert_reqs=str("CERT_REQUIRED"), ca_certs=certifi.where(), timeout=self.http_timeout
             )
         else:
-            _http = urllib3.PoolManager()
+            _http = urllib3.PoolManager(timeout=self.http_timeout)
 
         if self.headers:
-            _headers = urllib3.util.make_headers(**self.headers)
+            if six.PY3:
+                # Python3
+                urllib_keys = inspect.getfullargspec(urllib3.util.make_headers).args
+            else:
+                # Python2 fallback
+                urllib_keys = inspect.getargspec(urllib3.util.make_headers).args
+            urllib_headers = {header: value for header, value in six.iteritems(self.headers) if header in urllib_keys}
+            other_headers = {header: value for header, value in six.iteritems(self.headers) if header not in urllib_keys}
+            _headers = urllib3.util.make_headers(**urllib_headers)
+            _headers.update(other_headers)
             _http.headers.update(_headers)
         log.debug(_http.headers)
+        log.debug("HTTP Timeout is " + str(self.http_timeout))
         return _http
 
     def download_verify_write(self):
diff --git a/pyupdater/client/patcher.py b/pyupdater/client/patcher.py
index 4747393..3536f27 100755
--- a/pyupdater/client/patcher.py
+++ b/pyupdater/client/patcher.py
@@ -67,7 +67,9 @@ class Patcher(object):
 
         max_download_retries (int): Number of times to retry a download
 
-        urllib3_headers (dict): Headers to be used with http request
+        headers (dict): Headers to be used with http request.  Accepts urllib3 and generic headers.
+
+        http_timeout (int): HTTP timeout or None
     """
 
     def __init__(self, **kwargs):
@@ -81,8 +83,9 @@ class Patcher(object):
         self.update_urls = kwargs.get("update_urls", [])
         self.verify = kwargs.get("verify", True)
         self.max_download_retries = kwargs.get("max_download_retries")
-        self.urllib3_headers = kwargs.get("urllib3_headers")
+        self.headers = kwargs.get("headers")
         self.downloader = kwargs.get("downloader")
+        self.http_timeout = kwargs.get("http_timeout")
 
         # Progress hooks to be called
         self.progress_hooks = kwargs.get("progress_hooks", [])
@@ -302,7 +305,8 @@ class Patcher(object):
                         hexdigest=p["patch_hash"],
                         verify=self.verify,
                         max_download_retries=self.max_download_retries,
-                        urllb3_headers=self.urllib3_headers,
+                        headers=self.headers,
+                        http_timeout=self.http_timeout
                     )
 
                 # Attempt to download resource
diff --git a/pyupdater/client/updates.py b/pyupdater/client/updates.py
index 79109c2..e48ad75 100644
--- a/pyupdater/client/updates.py
+++ b/pyupdater/client/updates.py
@@ -418,8 +418,8 @@ class LibUpdate(object):
         # Weather or not the verify the https connection
         self.verify = data.get("verify", True)
 
-        # Extra headers to pass to urllib3
-        self.urllib3_headers = data.get("urllib3_headers")
+        # Extra headers
+        self.headers = data.get("headers")
 
         # The amount of times to retry a url before giving up
         self.max_download_retries = data.get("max_download_retries")
@@ -684,7 +684,8 @@ class LibUpdate(object):
                     verify=self.verify,
                     progress_hooks=self.progress_hooks,
                     max_download_retries=self.max_download_retries,
-                    urllb3_headers=self.urllib3_headers,
+                    headers=self.headers,
+                    http_timeout=self.http_timeout
                 )
             result = fd.download_verify_write()
             if result:

Honor HTTP_TIMEOUT in FileDownloader
**Problem:** Developers can set the `HTTP_TIMEOUT` in their client configuration.  This variable does not seem to be honored in the codebase.  As a consequence, there are instances where this can cause an application to hang indefinitely waiting for a response, leaving the application in a "paused" state.

**Real world example:** I had set the `HTTP_TIMEOUT` of my application to be 30 seconds.  Unfortunately, my API server (which is polled in the `UPDATE_URLS` variable) encountered issues and started hanging incoming requests.  Here's a screenshot using the CurrPorts application for Windows that shows a TCP connection, which had been hanging for many hours:
<img width="1077" alt="Screen Shot 2019-09-26 at 10 28 04 AM" src="https://user-images.githubusercontent.com/26491362/65701014-9dbd2380-e04e-11e9-99b3-33a76f65a9ea.png">
Essentially, I had an updater thread that would poll for an update, but the whole thread hung indefinitely since the `app_update.refresh()` was not enforcing the `HTTP_TIMEOUT` setting.  As a result, my application could no longer update itself in this hung state.  When I manually closed the TCP connection through CurrPorts, all normal functionality resumed.  My setup was pretty vanilla with just patching disabled.

**Proposed Solution:** Since this is my first time diving into the PyUpdater codebase, please excuse any mistakes I make during my analysis.

Doing a rough search through for the variable `http_timeout` in the codebase doesn't return many results.  I believe we may not be honoring it as we should be.

 I do not see any references again to the `http_timeout` variable in the `FileDownloader` class.  For example, in the method `_create_response()`, the code is
```
data = self.http_pool.urlopen(
                    "GET", file_url, preload_content=False, retries=max_download_retries
                )
```
If I understand correctly, we should be passing in the timeout to this function call so that it looks more like
```
data = self.http_pool.urlopen(
                    "GET", file_url, preload_content=False, retries=max_download_retries, timeout=<timeout>
                )
```

This also means we must start passing the timeout down to `FileDownloader`, which we are currently not doing.  For example, the `LibUpdate` class doesn't seem to be passing `self.http_timeout` to `FileDownloader` or even referencing the variable at all.

In other methods, such as `_get_manifest_from_http()` and `_get_key_data()`, we also don't pass the timeout down to `FileDownloader` - not that it makes use of it in the first place, anyways.
```
fd = FileDownloader(
                        vf,
                        self.update_urls,
                        verify=self.verify,
                        urllb3_headers=self.urllib3_headers,
                    )
```

Please let me know if I have misinterpreted any parts of the codebase, I look forward to hearing your thoughts.
diff --git a/tests/test_downloader.py b/tests/test_downloader.py
index 3611d89..f9fe70c 100644
--- a/tests/test_downloader.py
+++ b/tests/test_downloader.py
@@ -63,10 +63,20 @@ class TestData(object):
 
 
 @pytest.mark.usefixtue("cleandir")
-class TestBasicAuth(object):
+class TestBasicAuthUrlLib(object):
     def test_basic_auth(self):
         headers = {"basic_auth": "user:pass"}
-        fd = FileDownloader("test", ["test"], urllb3_headers=headers)
+        fd = FileDownloader("test", ["test"], headers=headers)
+        http = fd._get_http_pool(secure=True)
+        sc = http.request("GET", "https://httpbin.org/basic-auth/user/pass").status
+        assert sc == 200
+
+
+@pytest.mark.usefixtue("cleandir")
+class TestAuthorizationHeader(object):
+    def test_auth_header(self):
+        headers = {"Authorization": "Basic dXNlcjpwYXNz"}
+        fd = FileDownloader("test", ["test"], headers=headers)
         http = fd._get_http_pool(secure=True)
         sc = http.request("GET", "https://httpbin.org/basic-auth/user/pass").status
         assert sc == 200

pip install -e .[all]
parse_log_pytest
requirements.txt
pytest pytest-cov pytest-xdist
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
altgraph==0.17.4
appdirs==1.4.4
bcrypt==4.3.0
boto3==1.37.23
botocore==1.37.23
bsdiff4==1.1.5
certifi==2025.1.31
cffi==1.17.1
chardet==5.2.0
coverage==7.8.0
cryptography==44.0.2
Deprecated==1.2.18
dsdev-utils==1.3.0
ed25519==1.5
exceptiongroup==1.2.2
execnet==2.1.1
importlib_metadata==8.6.1
iniconfig==2.1.0
jmespath==1.0.1
packaging==24.2
paramiko==3.5.1
pbr==6.1.1
pluggy==1.5.0
pycparser==2.22
pyinstaller==6.12.0
pyinstaller-hooks-contrib==2025.2
PyNaCl==1.5.0
pytest==8.3.5
pytest-cov==6.0.0
pytest-xdist==3.6.1
python-dateutil==2.9.0.post0
-e git+https://github.com/Digital-Sapphire/PyUpdater.git@1abe00d602327eb106a0a220b03366eca6ea6553#egg=PyUpdater
PyUpdater-S3-Plugin==4.1.2
PyUpdater-SCP-Plugin==4.0.3
s3transfer==0.11.4
scp==0.15.0
six==1.17.0
stevedore==1.32.0
tomli==2.2.1
urllib3==1.26.20
wrapt==1.17.2
zipp==3.21.0

name: PyUpdater
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - altgraph==0.17.4
      - appdirs==1.4.4
      - bcrypt==4.3.0
      - boto3==1.37.23
      - botocore==1.37.23
      - bsdiff4==1.1.5
      - certifi==2025.1.31
      - cffi==1.17.1
      - chardet==5.2.0
      - coverage==7.8.0
      - cryptography==44.0.2
      - deprecated==1.2.18
      - dsdev-utils==1.3.0
      - ed25519==1.5
      - exceptiongroup==1.2.2
      - execnet==2.1.1
      - importlib-metadata==8.6.1
      - iniconfig==2.1.0
      - jmespath==1.0.1
      - packaging==24.2
      - paramiko==3.5.1
      - pbr==6.1.1
      - pluggy==1.5.0
      - pycparser==2.22
      - pyinstaller==6.12.0
      - pyinstaller-hooks-contrib==2025.2
      - pynacl==1.5.0
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - pytest-xdist==3.6.1
      - python-dateutil==2.9.0.post0
      - pyupdater-s3-plugin==4.1.2
      - pyupdater-scp-plugin==4.0.3
      - s3transfer==0.11.4
      - scp==0.15.0
      - six==1.17.0
      - stevedore==1.32.0
      - tomli==2.2.1
      - urllib3==1.26.20
      - wrapt==1.17.2
      - zipp==3.21.0
prefix: /opt/conda/envs/PyUpdater

tests/test_downloader.py::TestBasicAuthUrlLib::test_basic_auth tests/test_downloader.py::TestAuthorizationHeader::test_auth_header
tests/test_downloader.py::TestUrl::test_url_as_string tests/test_downloader.py::TestGetHash::test_get_hash tests/test_downloader.py::TestContentLength::test_bad_content_length tests/test_downloader.py::TestUrl::test_bad_url tests/test_downloader.py::TestData::test_return[0] tests/test_downloader.py::TestContentLength::test_good_content_length tests/test_downloader.py::TestData::test_cb[4194304] tests/test_downloader.py::TestData::test_return_fail[4194304] tests/test_downloader.py::TestData::test_cb[0] tests/test_downloader.py::TestData::test_return[4194304] tests/test_downloader.py::TestData::test_return_fail[0]
docs/api.md pyupdater/client/downloader.py pyupdater/client/__init__.py pyupdater/client/updates.py pyupdater/client/patcher.py
docs/api.md pyupdater/client/downloader.py pyupdater/client/__init__.py pyupdater/client/updates.py pyupdater/client/patcher.py