@@ -156,6 +156,109 @@ def _create_client_span(self, method: str, url: str, is_pre_app_start: bool) ->
156156 )
157157 )
158158
159+ def _build_auth_from_param (self , auth : Any , httpx_module : Any ) -> Any :
160+ """Convert auth parameter to an httpx Auth object.
161+
162+ Mirrors httpx's BaseClient._build_auth() logic:
163+ - tuple -> BasicAuth(username, password)
164+ - Auth instance -> pass through
165+ - callable -> FunctionAuth(func)
166+
167+ Args:
168+ auth: Auth parameter (tuple, callable, Auth instance, or None)
169+ httpx_module: The httpx module (for accessing Auth classes)
170+
171+ Returns:
172+ httpx.Auth instance or None
173+ """
174+ if auth is None :
175+ return None
176+
177+ # Tuple -> BasicAuth (username, password)
178+ if isinstance (auth , tuple ):
179+ return httpx_module .BasicAuth (username = auth [0 ], password = auth [1 ])
180+
181+ # Check if it's an Auth subclass
182+ try :
183+ if hasattr (httpx_module , "Auth" ) and isinstance (auth , httpx_module .Auth ):
184+ return auth
185+ except TypeError :
186+ # isinstance check failed, try duck typing
187+ if hasattr (auth , "sync_auth_flow" ) and hasattr (auth , "auth_flow" ):
188+ return auth
189+
190+ # Callable -> FunctionAuth
191+ if callable (auth ):
192+ return httpx_module .FunctionAuth (func = auth )
193+
194+ return None
195+
196+ def _apply_auth_to_request_sync (
197+ self ,
198+ request : Any ,
199+ auth : Any ,
200+ httpx_module : Any ,
201+ client : Any = None ,
202+ ) -> Any :
203+ """Apply auth flow to request to add Authorization header.
204+
205+ Runs the auth flow generator to get the modified request.
206+ For BasicAuth/FunctionAuth, this adds the Authorization header.
207+ For complex auth (DigestAuth), only gets the first request.
208+
209+ Args:
210+ request: httpx.Request object
211+ auth: Auth parameter (tuple, callable, Auth instance, USE_CLIENT_DEFAULT, or None)
212+ httpx_module: The httpx module
213+ client: The httpx Client instance (needed to resolve USE_CLIENT_DEFAULT)
214+
215+ Returns:
216+ Modified request with auth headers applied (for simple auth types)
217+ """
218+ # Handle USE_CLIENT_DEFAULT sentinel - get auth from client's default
219+ # httpx uses this when auth is set on the client, not the request
220+ if client is not None and auth is not None :
221+ # Check if auth is USE_CLIENT_DEFAULT (a sentinel class instance)
222+ auth_type_name = type (auth ).__name__
223+ if auth_type_name == "UseClientDefault" :
224+ # Get the client's default auth
225+ auth = getattr (client , "_auth" , None )
226+
227+ # Convert auth param to Auth object
228+ auth_obj = self ._build_auth_from_param (auth , httpx_module )
229+
230+ # Check for URL-embedded credentials if no explicit auth
231+ if auth_obj is None :
232+ try :
233+ username = request .url .username
234+ password = request .url .password
235+ if username or password :
236+ auth_obj = httpx_module .BasicAuth (
237+ username = username or "" ,
238+ password = password or "" ,
239+ )
240+ except Exception :
241+ pass
242+
243+ if auth_obj is None :
244+ return request
245+
246+ try :
247+ # Get the sync auth flow generator
248+ auth_flow = auth_obj .sync_auth_flow (request )
249+
250+ # Get the first (and for BasicAuth/FunctionAuth, only) request
251+ # This modifies the request in-place for BasicAuth (adds Authorization header)
252+ modified_request = next (auth_flow )
253+
254+ # Close the generator to clean up
255+ auth_flow .close ()
256+
257+ return modified_request
258+ except Exception as e :
259+ logger .warning (f"Error applying auth flow in replay mode: { e } " )
260+ return request
261+
159262 def _patch_sync_client (self , module : Any ) -> None :
160263 """Patch httpx.Client.send for sync HTTP calls.
161264
@@ -189,7 +292,9 @@ def original_call():
189292 # REPLAY mode: Use handle_replay_mode for proper background request handling
190293 if sdk .mode == TuskDriftMode .REPLAY :
191294 return handle_replay_mode (
192- replay_mode_handler = lambda : instrumentation_self ._handle_replay_send_sync (sdk , module , request ),
295+ replay_mode_handler = lambda : instrumentation_self ._handle_replay_send_sync (
296+ sdk , module , request , auth = auth , client = client_self
297+ ),
193298 no_op_request_handler = lambda : instrumentation_self ._get_default_response (module , url_str ),
194299 is_server_request = False ,
195300 )
@@ -218,12 +323,20 @@ def _handle_replay_send_sync(
218323 sdk : TuskDrift ,
219324 httpx_module : Any ,
220325 request : Any , # httpx.Request object
326+ auth : Any = None , # Auth parameter to apply before mock lookup
327+ client : Any = None , # Client instance (needed to resolve USE_CLIENT_DEFAULT)
221328 ) -> Any :
222329 """Handle send in REPLAY mode (sync).
223330
224- Creates a span, fetches mock response from the Request object, and returns it .
331+ Creates a span, applies auth to get modified request, fetches mock response .
225332 Raises RuntimeError if no mock is found.
226333 """
334+ # Apply auth flow to get request with Authorization header
335+ # This is needed because during RECORD, auth is applied by httpx internally,
336+ # but during REPLAY we skip httpx and need to apply it ourselves
337+ if auth is not None :
338+ request = self ._apply_auth_to_request_sync (request , auth , httpx_module , client )
339+
227340 method = request .method
228341 url = str (request .url )
229342
@@ -354,7 +467,9 @@ async def original_call():
354467 # handle_replay_mode returns coroutine which we await
355468 if sdk .mode == TuskDriftMode .REPLAY :
356469 return await handle_replay_mode (
357- replay_mode_handler = lambda : instrumentation_self ._handle_replay_send_async (sdk , module , request ),
470+ replay_mode_handler = lambda : instrumentation_self ._handle_replay_send_async (
471+ sdk , module , request , auth = auth , client = client_self
472+ ),
358473 no_op_request_handler = lambda : instrumentation_self ._get_default_response (module , url_str ),
359474 is_server_request = False ,
360475 )
@@ -384,13 +499,18 @@ async def _handle_replay_send_async(
384499 sdk : TuskDrift ,
385500 httpx_module : Any ,
386501 request : Any , # httpx.Request object
502+ auth : Any = None , # Auth parameter to apply before mock lookup
503+ client : Any = None , # Client instance (needed to resolve USE_CLIENT_DEFAULT)
387504 ) -> Any :
388505 """Handle send in REPLAY mode (async).
389506
390507 Delegates to sync version since mock lookup uses sync operations
391508 to avoid nested event loop issues with Flask's asyncio.run().
509+
510+ Note: Auth is applied synchronously since the auth flow for simple auth types
511+ (BasicAuth, FunctionAuth) does not require async operations.
392512 """
393- return self ._handle_replay_send_sync (sdk , httpx_module , request )
513+ return self ._handle_replay_send_sync (sdk , httpx_module , request , auth = auth , client = client )
394514
395515 async def _handle_record_send_async (
396516 self ,
0 commit comments