@@ -445,20 +445,31 @@ def _auto_url(self, url, features):
445445 return url
446446
447447 async def _get_client (self ) -> httpx .AsyncClient :
448- """Get or create the httpx AsyncClient."""
449- if self ._client is None :
450- transport = None
451- if self ._proxy :
452- transport = httpx .AsyncHTTPTransport (proxy = self ._proxy )
453-
454- self ._client = httpx .AsyncClient (
455- auth = self .auth ,
456- timeout = self .timeout ,
457- verify = self .ssl_verify_cert ,
458- cert = self .ssl_cert ,
459- transport = transport ,
460- )
461- return self ._client
448+ """Get or create the httpx AsyncClient.
449+
450+ When used from sync wrappers (via anyio.run()), we need to create
451+ a fresh client each time because the connection pool gets invalidated
452+ when the event loop closes.
453+ """
454+ # Always create a fresh client to avoid event loop issues
455+ # The overhead is minimal compared to connection establishment
456+ transport = None
457+ if self ._proxy :
458+ transport = httpx .AsyncHTTPTransport (proxy = self ._proxy )
459+
460+ # Disable connection pooling to avoid stale connections
461+ # when used from sync context with multiple anyio.run() calls
462+ limits = httpx .Limits (max_keepalive_connections = 0 )
463+
464+ client = httpx .AsyncClient (
465+ auth = self .auth ,
466+ timeout = self .timeout ,
467+ verify = self .ssl_verify_cert ,
468+ cert = self .ssl_cert ,
469+ transport = transport ,
470+ limits = limits ,
471+ )
472+ return client
462473
463474 async def __aenter__ (self ) -> Self :
464475 await self ._get_client ()
@@ -518,7 +529,6 @@ async def request(
518529 This is the core method that all other HTTP methods use.
519530 """
520531 headers = headers or {}
521- client = await self ._get_client ()
522532
523533 combined_headers = dict (self .headers )
524534 combined_headers .update (headers or {})
@@ -533,49 +543,57 @@ async def request(
533543 )
534544 )
535545
536- try :
537- r = await client .request (
538- method ,
539- str (url_obj ),
540- content = to_wire (body ) if body else None ,
541- headers = combined_headers ,
542- )
543- log .debug ("server responded with %i %s" % (r .status_code , r .reason_phrase ))
544-
545- if (
546- r .status_code == 401
547- and "text/html" in self .headers .get ("Content-Type" , "" )
548- and not self .auth
549- ):
550- msg = (
551- "No authentication object was provided. "
552- "HTML was returned when probing the server for supported authentication types."
546+ # Create a fresh client for each request to avoid event loop issues
547+ # when used from sync context with multiple anyio.run() calls
548+ async with await self ._get_client () as client :
549+ try :
550+ r = await client .request (
551+ method ,
552+ str (url_obj ),
553+ content = to_wire (body ) if body else None ,
554+ headers = combined_headers ,
553555 )
554- if r .headers .get ("WWW-Authenticate" ):
555- auth_types = [
556- t
557- for t in self .extract_auth_types (r .headers ["WWW-Authenticate" ])
558- if t in ["basic" , "digest" , "bearer" ]
559- ]
560- if auth_types :
561- msg += "\n Supported authentication types: %s" % (
562- ", " .join (auth_types )
563- )
564- log .warning (msg )
565- response = DAVResponse (r , self )
566- except httpx .RequestError :
567- if self .auth or not self .password :
568- raise
569- # Workaround for servers that abort connection instead of 401
570- r = await client .request (
571- method = "GET" ,
572- url = str (url_obj ),
573- headers = combined_headers ,
574- )
575- if r .status_code != 401 :
576- raise
556+ log .debug (
557+ "server responded with %i %s" % (r .status_code , r .reason_phrase )
558+ )
559+
560+ if (
561+ r .status_code == 401
562+ and "text/html" in self .headers .get ("Content-Type" , "" )
563+ and not self .auth
564+ ):
565+ msg = (
566+ "No authentication object was provided. "
567+ "HTML was returned when probing the server for supported authentication types."
568+ )
569+ if r .headers .get ("WWW-Authenticate" ):
570+ auth_types = [
571+ t
572+ for t in self .extract_auth_types (
573+ r .headers ["WWW-Authenticate" ]
574+ )
575+ if t in ["basic" , "digest" , "bearer" ]
576+ ]
577+ if auth_types :
578+ msg += "\n Supported authentication types: %s" % (
579+ ", " .join (auth_types )
580+ )
581+ log .warning (msg )
582+ response = DAVResponse (r , self )
583+ except httpx .RequestError :
584+ if self .auth or not self .password :
585+ raise
586+ # Workaround for servers that abort connection instead of 401
587+ r = await client .request (
588+ method = "GET" ,
589+ url = str (url_obj ),
590+ headers = combined_headers ,
591+ )
592+ if r .status_code != 401 :
593+ raise
594+ response = DAVResponse (r , self )
577595
578- # Handle authentication
596+ # Handle authentication (outside the client context - will create new client if needed)
579597 if (
580598 r .status_code == 401
581599 and "WWW-Authenticate" in r .headers
@@ -591,11 +609,6 @@ async def request(
591609 "supported authentication methods: basic, digest, bearer"
592610 )
593611
594- # Recreate client with new auth
595- if self ._client :
596- await self ._client .aclose ()
597- self ._client = None
598-
599612 return await self .request (url , method , body , headers )
600613
601614 elif (
@@ -613,11 +626,6 @@ async def request(
613626 self .username = None
614627 self .password = None
615628
616- # Recreate client with new auth
617- if self ._client :
618- await self ._client .aclose ()
619- self ._client = None
620-
621629 return await self .request (str (url_obj ), method , body , headers )
622630
623631 # Handle authorization errors
0 commit comments