Repository navigation
Make session.cookies the only cookie store and follow redirects in Python - #160
Merged
Merged
Conversation
FlorianREGAZ
force-pushed
the
fix/cookies-and-redirects
branch
from
October 10, 2026 11:49
abe27e0 to
bdf9bb3
Compare
Set-Cookie headers were parsed into a fresh jar and merged into the session jar, so a cookie the server expired was dropped from the fresh jar but stayed in the session jar. Extract into the session jar directly instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The library kept a cookie jar per session of its own, so cookies removed from session.cookies, by clear() or del, were still sent. Requests now run with withoutCookieJar, and the cookies for the request URL are picked from the Python jar by domain, path, secure flag and expiry and sent as a Cookie header. With withoutCookieJar the library adds every requestCookies entry to the request without matching it to the URL, which is why they aren't sent that way anymore. A Cookie header passed with the request is still sent, with the jar's cookies after it. Cookie values are sent as stored; the library used to strip double quotes from them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With followRedirects the library followed redirects itself, and Python only saw the Set-Cookie headers of the last response, so cookies set on the way were lost. followRedirects is now always false, and with allow_redirects execute_request follows 301, 302, 303, 307 and 308 the way requests does: - every hop's cookies go into the session jar and the next hop is sent the jar's cookies for its URL - a relative Location is resolved against the current URL and the fragment carries over - 303 turns anything but HEAD into a GET, 301 and 302 turn a POST into one; the body and its Content-Type, Content-Length and Transfer-Encoding headers are dropped when the method changes, 307 and 308 resend them - Authorization and a Cookie header passed in aren't sent to another host, port or scheme - after 30 redirects TooManyRedirects, a TLSClientException, is raised The final response gets response.history with the earlier responses, and response.cookies holds the cookies of every hop, so AsyncSession, which merges response.cookies into the shared jar, keeps them too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Python 3.10 orders them differently from later versions. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FlorianREGAZ
force-pushed
the
fix/cookies-and-redirects
branch
from
October 10, 2026 11:53
bdf9bb3 to
406de78
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
#25:
session.cookies.clear()doesn't clear cookiesThe Go session kept a cookie jar of its own, keyed by session id, so a cookie removed from
session.cookies(byclear()ordel) was still sent. Every request now sendswithoutCookieJar: true, which makessession.cookiesthe only place cookies are kept.Without a jar, upstream adds every
requestCookiesentry to the request without checking it against the URL (cffi_dist/main.goL235-243:SetCookiesonly runs when a jar exists, otherwise each cookie goes throughreq.AddCookie). So cookies are no longer sent asrequestCookies.http.cookiejar(add_cookie_headervia the existingMockRequest) picks the ones for the request URL by domain, path, secure flag and expiry, and they are sent as aCookieheader.Separate commit:
extract_cookies_to_jarnow extracts straight into the session jar, so when a server expires a cookie it is removed. Before, it was dropped from the per-response jar only. That removes the xfail ontest_server_can_delete_a_session_cookie.Fixes #25
#120: cookies set during redirects are lost
followRedirectsis now alwaysfalse. Withallow_redirects=True,execute_requestfollows redirects itself, the way requests does:Locationare followed, at most 30 times, thenTooManyRedirects(aTLSClientException) is raisedLocationis resolved withurljoin, and the fragment carries over unless theLocationhas its ownContent-Type/Content-Length/Transfer-Encodingheaders are dropped. 307/308 keep the method and bodyAuthorization, and aCookieheader passed in, are dropped when the redirect changes host, port or scheme (an http to https upgrade on default ports keeps them, like requests'should_strip_auth)Set-Cookiegoes into the session jar, and the next hop gets the jar's cookies for its URLresponse.history(the earlier responses, oldest first) andresponse.urlset to the final URLAsyncSessionmergesresponse.cookiesinto the shared jar on the event loop thread. So the finalresponse.cookiesnow holds the cookies of every hop, andAsyncSessionkeeps them without any change to how it touches shared state.Fixes #120
Behaviour changes
session.cookiesare no longer sent, and a cookie the server expires is removed from the jar.Cookieheader built in Python, not asrequestCookies. ACookieheader passed with the request is still sent, with the jar's cookies after it, but it is not sent on to another origin during redirects."from them.response.cookieson a redirected response holds the cookies of every hop. requests only puts the final response's cookies there; each response inhistorystill has its own.response.historyis new, and so istls_client.exceptions.TooManyRedirects.net/http.allow_redirectsstill defaults toFalse. requests defaults toTruefor everything except HEAD; it might make sense to switch the default in a future major version, but this PR leaves it alone.Testing
Set-Cookieon each hop. They check the URL, method, body, headers and cookies of every hop, thatfollowRedirectsis false andwithoutCookieJartrue, relative/fragment resolution, the 301/302/303/307/308 method and body rules, credential stripping across origins, the 30-redirect limit, and domain/path/secure/expiry matching of sent cookies. There are alsoAsyncSessiontests: redirect-hop cookies land in the shared jar, and cleared cookies aren't sent./cookies/set→/cookies→cookies.clear()→/cookiesis empty,delof one cookie,/cookies/delete,/redirect/3,/relative-redirect/3,/absolute-redirect/3,/redirect-to, POST through/redirect-to?status_code=301|302|303|307|308,/redirect/31raising,Authorizationand cookies not following a redirect from127.0.0.1tolocalhost, and/cookies/setwithallow_redirectsfor bothSessionandAsyncSession.http.serverendpoint sets a cookie on every hop of a chain (httpbin can't do that), including a hop that expires one, for bothSessionandAsyncSession.uv run ruff check,uv run ruff format --check,uv run mypyanduv run pytest(422 passed, 6 xfailed) pass.🤖 Generated with Claude Code