Skip to content

Commit baef5ae

Browse files
mwdd146980claude
andcommitted
Forward instance config (auth, TLS, timeout, proxy, headers) to HTTPXWrapper
- Add _build_httpx_client() that reads instance/init_config with the same field names, remapper, and priority rules as RequestsWrapper - Update HTTPXWrapper.__init__ to accept (instance, init_config, remapper, logger) instead of a pre-built httpx.Client - Update AgentCheck.http property to pass instance config through - Wire basic/digest auth, TLS verify/cert, timeouts, headers, redirects, and proxy mounts; skip_proxy disables trust_env - Update test_http_httpx.py and test_http_backend_equivalence.py for the new constructor; add TestBuildHttpxClient config-parity tests - Add test_config_httpx to nginx unit tests Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent df634c5 commit baef5ae

6 files changed

Lines changed: 385 additions & 21 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Forward instance config (auth, TLS, timeout, proxy, headers) to HTTPXWrapper.

datadog_checks_base/datadog_checks/base/checks/base.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,11 +387,14 @@ def http(self) -> HTTPClientProtocol:
387387
if not hasattr(self, '_http'):
388388
# See Performance Optimizations in this package's README.md.
389389
if is_affirmative((self.instance or {}).get('use_httpx', False)):
390-
import httpx
391-
392390
from datadog_checks.base.utils.http_httpx import HTTPXWrapper
393391

394-
self._http = HTTPXWrapper(httpx.Client())
392+
self._http = HTTPXWrapper(
393+
self.instance or {},
394+
self.init_config,
395+
self.HTTP_CONFIG_REMAPPER,
396+
self.log,
397+
)
395398
else:
396399
from datadog_checks.base.utils.http import RequestsWrapper
397400

datadog_checks_base/datadog_checks/base/utils/http_httpx.py

Lines changed: 187 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,14 @@
33
# Licensed under a 3-clause BSD style license (see LICENSE)
44
from __future__ import annotations
55

6+
import logging
67
from typing import Any, Iterator
78

89
import httpx
910

11+
from datadog_checks.base.config import is_affirmative
12+
from datadog_checks.base.utils.headers import get_default_headers, update_headers
13+
1014
from .http_exceptions import (
1115
HTTPConnectionError,
1216
HTTPError,
@@ -15,6 +19,181 @@
1519
HTTPTimeoutError,
1620
)
1721

22+
LOGGER = logging.getLogger(__name__)
23+
24+
DEFAULT_TIMEOUT = 10
25+
26+
# Fields recognized from instance/init_config for httpx client construction
27+
_STANDARD_FIELDS = {
28+
'allow_redirects': True,
29+
'auth_type': 'basic',
30+
'connect_timeout': None,
31+
'extra_headers': None,
32+
'headers': None,
33+
'kerberos_auth': None,
34+
'kerberos_delegate': False,
35+
'kerberos_force_initiate': False,
36+
'kerberos_hostname': None,
37+
'kerberos_keytab': None,
38+
'kerberos_principal': None,
39+
'ntlm_domain': None,
40+
'password': None,
41+
'proxy': None,
42+
'read_timeout': None,
43+
'skip_proxy': False,
44+
'timeout': DEFAULT_TIMEOUT,
45+
'tls_ca_cert': None,
46+
'tls_cert': None,
47+
'tls_private_key': None,
48+
'tls_verify': True,
49+
'username': None,
50+
}
51+
52+
# Legacy field aliases applied before reading standard fields
53+
_DEFAULT_REMAPPED_FIELDS = {
54+
'kerberos': {'name': 'kerberos_auth'},
55+
'no_proxy': {'name': 'skip_proxy'},
56+
}
57+
58+
59+
def _build_httpx_client(
60+
instance: dict,
61+
init_config: dict,
62+
remapper: dict | None = None,
63+
logger: logging.Logger | None = None,
64+
) -> httpx.Client:
65+
log = logger or LOGGER
66+
67+
# Merge default fields; init_config provides global overrides
68+
default_fields = dict(_STANDARD_FIELDS)
69+
default_fields['skip_proxy'] = init_config.get('skip_proxy', default_fields['skip_proxy'])
70+
default_fields['timeout'] = init_config.get('timeout', default_fields['timeout'])
71+
72+
# Populate config from instance, using defaults for missing fields
73+
config = {field: instance.get(field, value) for field, value in default_fields.items()}
74+
75+
# Apply remapper: normalize legacy/integration-specific field names
76+
if remapper is None:
77+
remapper = {}
78+
remapper.update(_DEFAULT_REMAPPED_FIELDS)
79+
80+
for remapped_field, data in remapper.items():
81+
field = data.get('name')
82+
if field not in _STANDARD_FIELDS:
83+
continue
84+
# Standard field already explicitly set — skip remapped alias
85+
if field in instance:
86+
continue
87+
default = default_fields[field]
88+
if data.get('invert'):
89+
default = not default
90+
value = instance.get(remapped_field, data.get('default', default))
91+
if data.get('invert'):
92+
value = not is_affirmative(value)
93+
config[field] = value
94+
95+
# --- Timeouts ---
96+
connect_timeout = read_timeout = float(config['timeout'])
97+
if config['connect_timeout'] is not None:
98+
connect_timeout = float(config['connect_timeout'])
99+
if config['read_timeout'] is not None:
100+
read_timeout = float(config['read_timeout'])
101+
# read_timeout is the default; connect overrides the connect-phase only
102+
timeout = httpx.Timeout(read_timeout, connect=connect_timeout)
103+
104+
# --- Headers ---
105+
headers = get_default_headers()
106+
if config['headers']:
107+
headers.clear()
108+
update_headers(headers, config['headers'])
109+
if config['extra_headers']:
110+
update_headers(headers, config['extra_headers'])
111+
112+
# --- Auth ---
113+
auth_type = (config['auth_type'] or 'basic').lower()
114+
if auth_type == 'basic':
115+
if config['kerberos_auth']:
116+
log.warning(
117+
'The ability to use Kerberos auth without explicitly setting auth_type to '
118+
'`kerberos` is deprecated and will be removed in Agent 8'
119+
)
120+
auth_type = 'kerberos'
121+
elif config['ntlm_domain']:
122+
log.warning(
123+
'The ability to use NTLM auth without explicitly setting auth_type to '
124+
'`ntlm` is deprecated and will be removed in Agent 8'
125+
)
126+
auth_type = 'ntlm'
127+
128+
auth: httpx.Auth | None = None
129+
if auth_type == 'basic':
130+
if config['username'] is not None:
131+
auth = httpx.BasicAuth(config['username'], config['password'] or '')
132+
elif auth_type == 'digest':
133+
if config['username'] is not None:
134+
auth = httpx.DigestAuth(config['username'], config['password'] or '')
135+
elif auth_type == 'kerberos':
136+
from datadog_checks.base.utils.httpx_auth import KerberosAuth
137+
138+
auth = KerberosAuth(
139+
mutual_authentication=config.get('kerberos_auth') or 'required',
140+
delegate=is_affirmative(config['kerberos_delegate']),
141+
force_preemptive=is_affirmative(config['kerberos_force_initiate']),
142+
hostname_override=config['kerberos_hostname'],
143+
principal=config['kerberos_principal'],
144+
keytab=config['kerberos_keytab'],
145+
)
146+
elif auth_type == 'ntlm':
147+
from datadog_checks.base.utils.httpx_auth import NTLMAuth
148+
149+
auth = NTLMAuth(config['ntlm_domain'], config['password'])
150+
151+
# --- TLS / verify ---
152+
verify: bool | str = True
153+
if isinstance(config['tls_ca_cert'], str):
154+
verify = config['tls_ca_cert']
155+
elif not is_affirmative(config['tls_verify']):
156+
verify = False
157+
158+
cert: tuple[str, str] | str | None = None
159+
if isinstance(config['tls_cert'], str):
160+
if isinstance(config['tls_private_key'], str):
161+
cert = (config['tls_cert'], config['tls_private_key'])
162+
else:
163+
cert = config['tls_cert']
164+
165+
# --- Proxies ---
166+
# trust_env=True lets httpx fall back to HTTP_PROXY/HTTPS_PROXY env vars (same as requests)
167+
trust_env = True
168+
mounts: dict[str, httpx.BaseTransport | None] | None = None
169+
170+
if is_affirmative(config['skip_proxy']):
171+
trust_env = False
172+
else:
173+
raw_proxy = config['proxy'] or init_config.get('proxy')
174+
if raw_proxy:
175+
mounts = {}
176+
for scheme, url in raw_proxy.items():
177+
# 'no_proxy' entries are not proxy URLs — skip them
178+
if scheme == 'no_proxy' or not url:
179+
continue
180+
# Convert requests format {'http': url} to httpx mount format {'http://': transport}
181+
key = scheme if scheme.endswith('://') else f'{scheme}://'
182+
mounts[key] = httpx.HTTPTransport(proxy=url)
183+
if not mounts:
184+
mounts = None
185+
186+
return httpx.Client(
187+
auth=auth,
188+
verify=verify,
189+
cert=cert,
190+
timeout=timeout,
191+
headers=headers,
192+
follow_redirects=is_affirmative(config['allow_redirects']),
193+
mounts=mounts,
194+
trust_env=trust_env,
195+
)
196+
18197

19198
def _translate_httpx_error(e: httpx.HTTPError) -> HTTPError:
20199
if isinstance(e, httpx.HTTPStatusError):
@@ -65,8 +244,14 @@ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
65244

66245

67246
class HTTPXWrapper:
68-
def __init__(self, client: httpx.Client) -> None:
69-
self._client = client
247+
def __init__(
248+
self,
249+
instance: dict,
250+
init_config: dict,
251+
remapper: dict | None = None,
252+
logger: logging.Logger | None = None,
253+
) -> None:
254+
self._client = _build_httpx_client(instance, init_config, remapper, logger)
70255

71256
def __del__(self) -> None: # no cov
72257
try:

datadog_checks_base/tests/base/utils/http/test_http_backend_equivalence.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ def http_client(request):
4040
with patch.object(requests.Session, "get", return_value=_requests_response(_BODY)):
4141
yield RequestsWrapper({}, {})
4242
else:
43-
yield HTTPXWrapper(httpx.Client(transport=_httpx_transport(_BODY)))
43+
with patch(
44+
'datadog_checks.base.utils.http_httpx._build_httpx_client',
45+
return_value=httpx.Client(transport=_httpx_transport(_BODY)),
46+
):
47+
yield HTTPXWrapper({}, {})
4448

4549

4650
def test_status_code(http_client):

0 commit comments

Comments
 (0)