diff --git a/httpx/_client.py b/httpx/_client.py index d95877e8be..2da0904afc 100644 --- a/httpx/_client.py +++ b/httpx/_client.py @@ -170,6 +170,7 @@ def __init__( cookies: CookieTypes | None = None, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, follow_redirects: bool = False, + merge_response_cookies: bool = True, max_redirects: int = DEFAULT_MAX_REDIRECTS, event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, base_url: URLTypes = "", @@ -186,6 +187,7 @@ def __init__( self._cookies = Cookies(cookies) self._timeout = Timeout(timeout) self.follow_redirects = follow_redirects + self.merge_response_cookies = merge_response_cookies self.max_redirects = max_redirects self._event_hooks = { "request": list(event_hooks.get("request", [])), @@ -610,6 +612,8 @@ class Client(BaseClient): URLs. * **timeout** - *(optional)* The timeout configuration to use when sending requests. + * **merge_response_cookies** - *(optional) A boolean indicating if cookies from the + response should be merged into the cookie jar. Defaults to `True`. * **limits** - *(optional)* The limits configuration to use. * **max_redirects** - *(optional)* The maximum number of redirect responses that should be followed. @@ -642,6 +646,7 @@ def __init__( mounts: None | (typing.Mapping[str, BaseTransport | None]) = None, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, follow_redirects: bool = False, + merge_response_cookies: bool = True, limits: Limits = DEFAULT_LIMITS, max_redirects: int = DEFAULT_MAX_REDIRECTS, event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, @@ -658,6 +663,7 @@ def __init__( cookies=cookies, timeout=timeout, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, max_redirects=max_redirects, event_hooks=event_hooks, base_url=base_url, @@ -795,6 +801,7 @@ def request( cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: RequestExtensions | None = None, ) -> Response: @@ -835,7 +842,12 @@ def request( timeout=timeout, extensions=extensions, ) - return self.send(request, auth=auth, follow_redirects=follow_redirects) + return self.send( + request, + auth=auth, + follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, + ) @contextmanager def stream( @@ -852,6 +864,7 @@ def stream( cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: RequestExtensions | None = None, ) -> typing.Iterator[Response]: @@ -882,6 +895,7 @@ def stream( request=request, auth=auth, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, stream=True, ) try: @@ -896,6 +910,7 @@ def send( stream: bool = False, auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, ) -> Response: """ Send a request. @@ -919,6 +934,11 @@ def send( if isinstance(follow_redirects, UseClientDefault) else follow_redirects ) + merge_response_cookies = ( + self.merge_response_cookies + if isinstance(merge_response_cookies, UseClientDefault) + else merge_response_cookies + ) self._set_timeout(request) @@ -928,6 +948,7 @@ def send( request, auth=auth, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, history=[], ) try: @@ -945,6 +966,7 @@ def _send_handling_auth( request: Request, auth: Auth, follow_redirects: bool, + merge_response_cookies: bool, history: list[Response], ) -> Response: auth_flow = auth.sync_auth_flow(request) @@ -955,6 +977,7 @@ def _send_handling_auth( response = self._send_handling_redirects( request, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, history=history, ) try: @@ -978,6 +1001,7 @@ def _send_handling_redirects( self, request: Request, follow_redirects: bool, + merge_response_cookies: bool, history: list[Response], ) -> Response: while True: @@ -989,7 +1013,7 @@ def _send_handling_redirects( for hook in self._event_hooks["request"]: hook(request) - response = self._send_single_request(request) + response = self._send_single_request(request, merge_response_cookies) try: for hook in self._event_hooks["response"]: hook(response) @@ -1011,7 +1035,9 @@ def _send_handling_redirects( response.close() raise exc - def _send_single_request(self, request: Request) -> Response: + def _send_single_request( + self, request: Request, merge_response_cookies: bool + ) -> Response: """ Sends a single request, without handling any redirections. """ @@ -1033,7 +1059,8 @@ def _send_single_request(self, request: Request) -> Response: response.stream = BoundSyncStream( response.stream, response=response, timer=timer ) - self.cookies.extract_cookies(response) + if merge_response_cookies: + self.cookies.extract_cookies(response) response.default_encoding = self._default_encoding logger.info( @@ -1056,6 +1083,7 @@ def get( cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: RequestExtensions | None = None, ) -> Response: @@ -1072,6 +1100,7 @@ def get( cookies=cookies, auth=auth, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, timeout=timeout, extensions=extensions, ) @@ -1085,6 +1114,7 @@ def options( cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: RequestExtensions | None = None, ) -> Response: @@ -1101,6 +1131,7 @@ def options( cookies=cookies, auth=auth, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, timeout=timeout, extensions=extensions, ) @@ -1114,6 +1145,7 @@ def head( cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: RequestExtensions | None = None, ) -> Response: @@ -1130,6 +1162,7 @@ def head( cookies=cookies, auth=auth, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, timeout=timeout, extensions=extensions, ) @@ -1147,6 +1180,7 @@ def post( cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: RequestExtensions | None = None, ) -> Response: @@ -1167,6 +1201,7 @@ def post( cookies=cookies, auth=auth, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, timeout=timeout, extensions=extensions, ) @@ -1184,6 +1219,7 @@ def put( cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: RequestExtensions | None = None, ) -> Response: @@ -1204,6 +1240,7 @@ def put( cookies=cookies, auth=auth, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, timeout=timeout, extensions=extensions, ) @@ -1221,6 +1258,7 @@ def patch( cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: RequestExtensions | None = None, ) -> Response: @@ -1241,6 +1279,7 @@ def patch( cookies=cookies, auth=auth, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, timeout=timeout, extensions=extensions, ) @@ -1254,6 +1293,7 @@ def delete( cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: RequestExtensions | None = None, ) -> Response: @@ -1270,6 +1310,7 @@ def delete( cookies=cookies, auth=auth, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, timeout=timeout, extensions=extensions, ) @@ -1357,6 +1398,8 @@ class AsyncClient(BaseClient): URLs. * **timeout** - *(optional)* The timeout configuration to use when sending requests. + * **merge_response_cookies** - *(optional) A boolean indicating if cookies from the + response should be merged into the cookie jar. Defaults to `True`. * **limits** - *(optional)* The limits configuration to use. * **max_redirects** - *(optional)* The maximum number of redirect responses that should be followed. @@ -1389,6 +1432,7 @@ def __init__( mounts: None | (typing.Mapping[str, AsyncBaseTransport | None]) = None, timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG, follow_redirects: bool = False, + merge_response_cookies: bool = True, limits: Limits = DEFAULT_LIMITS, max_redirects: int = DEFAULT_MAX_REDIRECTS, event_hooks: None | (typing.Mapping[str, list[EventHook]]) = None, @@ -1405,6 +1449,7 @@ def __init__( cookies=cookies, timeout=timeout, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, max_redirects=max_redirects, event_hooks=event_hooks, base_url=base_url, @@ -1542,6 +1587,7 @@ async def request( cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: RequestExtensions | None = None, ) -> Response: @@ -1583,7 +1629,12 @@ async def request( timeout=timeout, extensions=extensions, ) - return await self.send(request, auth=auth, follow_redirects=follow_redirects) + return await self.send( + request, + auth=auth, + follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, + ) @asynccontextmanager async def stream( @@ -1600,6 +1651,7 @@ async def stream( cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: RequestExtensions | None = None, ) -> typing.AsyncIterator[Response]: @@ -1630,6 +1682,7 @@ async def stream( request=request, auth=auth, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, stream=True, ) try: @@ -1644,6 +1697,7 @@ async def send( stream: bool = False, auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, ) -> Response: """ Send a request. @@ -1667,6 +1721,11 @@ async def send( if isinstance(follow_redirects, UseClientDefault) else follow_redirects ) + merge_response_cookies = ( + self.merge_response_cookies + if isinstance(merge_response_cookies, UseClientDefault) + else merge_response_cookies + ) self._set_timeout(request) @@ -1676,6 +1735,7 @@ async def send( request, auth=auth, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, history=[], ) try: @@ -1693,6 +1753,7 @@ async def _send_handling_auth( request: Request, auth: Auth, follow_redirects: bool, + merge_response_cookies: bool, history: list[Response], ) -> Response: auth_flow = auth.async_auth_flow(request) @@ -1703,6 +1764,7 @@ async def _send_handling_auth( response = await self._send_handling_redirects( request, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, history=history, ) try: @@ -1726,6 +1788,7 @@ async def _send_handling_redirects( self, request: Request, follow_redirects: bool, + merge_response_cookies: bool, history: list[Response], ) -> Response: while True: @@ -1737,7 +1800,7 @@ async def _send_handling_redirects( for hook in self._event_hooks["request"]: await hook(request) - response = await self._send_single_request(request) + response = await self._send_single_request(request, merge_response_cookies) try: for hook in self._event_hooks["response"]: await hook(response) @@ -1760,7 +1823,9 @@ async def _send_handling_redirects( await response.aclose() raise exc - async def _send_single_request(self, request: Request) -> Response: + async def _send_single_request( + self, request: Request, merge_response_cookies: bool + ) -> Response: """ Sends a single request, without handling any redirections. """ @@ -1781,7 +1846,8 @@ async def _send_single_request(self, request: Request) -> Response: response.stream = BoundAsyncStream( response.stream, response=response, timer=timer ) - self.cookies.extract_cookies(response) + if merge_response_cookies: + self.cookies.extract_cookies(response) response.default_encoding = self._default_encoding logger.info( @@ -1804,6 +1870,7 @@ async def get( cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault | None = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: RequestExtensions | None = None, ) -> Response: @@ -1820,6 +1887,7 @@ async def get( cookies=cookies, auth=auth, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, timeout=timeout, extensions=extensions, ) @@ -1833,6 +1901,7 @@ async def options( cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: RequestExtensions | None = None, ) -> Response: @@ -1849,6 +1918,7 @@ async def options( cookies=cookies, auth=auth, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, timeout=timeout, extensions=extensions, ) @@ -1862,6 +1932,7 @@ async def head( cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: RequestExtensions | None = None, ) -> Response: @@ -1878,6 +1949,7 @@ async def head( cookies=cookies, auth=auth, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, timeout=timeout, extensions=extensions, ) @@ -1895,6 +1967,7 @@ async def post( cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: RequestExtensions | None = None, ) -> Response: @@ -1915,6 +1988,7 @@ async def post( cookies=cookies, auth=auth, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, timeout=timeout, extensions=extensions, ) @@ -1932,6 +2006,7 @@ async def put( cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: RequestExtensions | None = None, ) -> Response: @@ -1952,6 +2027,7 @@ async def put( cookies=cookies, auth=auth, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, timeout=timeout, extensions=extensions, ) @@ -1969,6 +2045,7 @@ async def patch( cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: RequestExtensions | None = None, ) -> Response: @@ -1989,6 +2066,7 @@ async def patch( cookies=cookies, auth=auth, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, timeout=timeout, extensions=extensions, ) @@ -2002,6 +2080,7 @@ async def delete( cookies: CookieTypes | None = None, auth: AuthTypes | UseClientDefault = USE_CLIENT_DEFAULT, follow_redirects: bool | UseClientDefault = USE_CLIENT_DEFAULT, + merge_response_cookies: bool | UseClientDefault = USE_CLIENT_DEFAULT, timeout: TimeoutTypes | UseClientDefault = USE_CLIENT_DEFAULT, extensions: RequestExtensions | None = None, ) -> Response: @@ -2018,6 +2097,7 @@ async def delete( cookies=cookies, auth=auth, follow_redirects=follow_redirects, + merge_response_cookies=merge_response_cookies, timeout=timeout, extensions=extensions, ) diff --git a/tests/client/test_cookies.py b/tests/client/test_cookies.py index f0c8352593..28e5d5f9e9 100644 --- a/tests/client/test_cookies.py +++ b/tests/client/test_cookies.py @@ -166,3 +166,45 @@ def test_cookie_persistence() -> None: response = client.get("http://example.org/echo_cookies") assert response.status_code == 200 assert response.json() == {"cookies": "example-name=example-value"} + + +def test_cookie_persistence_off() -> None: + """ + Ensure that Client instances do not persist cookies between requests when + persistence is off. + """ + client = httpx.Client( + transport=httpx.MockTransport(get_and_set_cookies), merge_response_cookies=False + ) + + response = client.get("http://example.org/echo_cookies") + assert response.status_code == 200 + assert response.json() == {"cookies": None} + + response = client.get("http://example.org/set_cookie") + assert response.status_code == 200 + assert response.cookies["example-name"] == "example-value" + assert "example-name" not in client.cookies.keys() + + response = client.get("http://example.org/echo_cookies") + assert response.status_code == 200 + assert response.json() == {"cookies": None} + + +def test_cookie_persistence_override() -> None: + client = httpx.Client( + transport=httpx.MockTransport(get_and_set_cookies), merge_response_cookies=False + ) + + response = client.get("http://example.org/echo_cookies") + assert response.status_code == 200 + assert response.json() == {"cookies": None} + + response = client.get("http://example.org/set_cookie", merge_response_cookies=True) + assert response.status_code == 200 + assert response.cookies["example-name"] == "example-value" + assert client.cookies["example-name"] == "example-value" + + response = client.get("http://example.org/echo_cookies") + assert response.status_code == 200 + assert response.json() == {"cookies": "example-name=example-value"}