Repository navigation
Add stored cookies without rebuilding the request - #2384
Open
pavel-ptashyts wants to merge 4 commits into
Open
pavel-ptashyts wants to merge 4 commits into
pavel-ptashyts wants to merge 4 commits into
Conversation
When the cookie store had a cookie for the request URI,
executeRequest ran request.toBuilder() and build() to add it. That
copied every header into a new header set, copied the cookie, form
param and body part lists twice and encoded the URI again, on every
request that carries a session cookie.
The URI step was also a bug. toBuilder() always uses the fixing URI
encoder, so a request built with URL encoding disabled was sent
encoded, but only when the store had a cookie for it: "/a|b?q={x}"
went out as "/a%7Cb?q=%7Bx%7D".
A DefaultRequest is now copied with only its cookie list replaced
and its URI kept as built, and is returned unchanged when it already
carries a cookie of every stored name. Other Request implementations
still go through toBuilder(), as they may override what it copies.
Claude Code on behalf of Pavel Ptashyts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The field test set all four Boolean flags to true, so swapping two adjacent Boolean parameters in DefaultRequest.withCookies would still pass. It now uses two requests in which every pair of flags differs, and compares the body fields too. New tests check that the first stored cookie of a name wins and that a DefaultRequest subclass still goes through toBuilder(). Claude Code on behalf of Pavel Ptashyts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A DefaultRequest made through its public constructor may carry a null cookie list. The toBuilder() path treated it as empty, so the new path failed with a NullPointerException only when the cookie store had a cookie for the request, while the same request without a stored cookie was sent. Claude Code on behalf of Pavel Ptashyts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
hyperxpro
requested changes
Nov 9, 2026
Review of AsyncHttpClient#2384 found gaps the tests let through. The field test used requests with no file, stream or byte body, so withCookies could drop those fields unnoticed: it now builds requests with every field set to a distinct non-null value and compares the fields reflectively. A request cookie SID and a stored cookie sid both go out, which pins the case-sensitive name match. Sharing the caller's headers relies on the redirect interceptor copying them before stripping credentials; a test now executes one request twice across a cross-origin 302 with a stored cookie and checks its headers are unchanged. The withCookies javadoc no longer says a raw URI is sent as written: auth retries and CONNECT still rebuild the request through toBuilder. Claude Code on behalf of Pavel Ptashyts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.
Summary
When the client's cookie store has a cookie for the request URI,
DefaultAsyncHttpClient.executeRequestadds it withrequest.toBuilder(),addCookieIfUnsetandbuild(). That rebuild copies every header into a new header set, copies the cookie, form param and body part lists twice and encodes the URI again, on every request that carries a session cookie.The URI step is also a bug.
toBuilder()always uses the fixing URI encoder, so a request built with URL encoding disabled is re-encoded, but only when the store has a cookie for it:Changes:
DefaultRequest.withCookies(package-private) returns a request that differs only by its cookie list. The URI is kept as built and the other fields are shared, not copied.executeRequestmerges the stored cookies into a new list with the same rule asaddCookieIfUnset: the request's own cookies first and never replaced, then each stored cookie whose name is not there yet, so the first stored cookie of a name wins. If every stored name is already set, the request is passed on unchanged. A null cookie list (possible through the publicDefaultRequestconstructor) is treated as empty, as the builder did.Requestimplementations, including subclasses ofDefaultRequest, still go throughtoBuilder(), as they may override what it copies.StoredCookieRequestBenchmarkmeasuresexecuteRequestup to the request filters with and without a matching stored cookie.Behaviour notes
addCookieIfUnsetdoes. The only intended difference is that the URI is no longer re-encoded on this path.HttpHeadersobject and lists. Without a matching stored cookie (or with the cookie store disabled)executeRequestalready passes the caller's request, with the same objects, to the filters and the sender, so the copy was incidental to the cookie path. No production code mutatesrequest.getHeaders(): the redirect and auth interceptors copy first. Before this change the rebuild hid a missing copy for every request with a stored cookie, so a test now executes one request twice across a cross-origin 302 with a stored cookie and checks that its headers are unchanged.withCookiesis package-private, the new helpers inDefaultAsyncHttpClientare private. Revapi passes.Not changed here: these paths also go through
toBuilder()and so still re-encode a URI built with URL encoding disabled. They are not on the per-request path and change more than the cookies, so they are left for a separate change:preProcessRequest, for a request with a range offset, andResumableAsyncHandler.adjustRequestRange;Unauthorized401InterceptorandProxyUnauthorized407Interceptor;ConnectSuccessInterceptorafter a proxyCONNECT.So a request built with URL encoding disabled keeps its URI when a stored cookie is added, but not yet on an auth retry or through a tunnelling proxy.
Benchmark
StoredCookieRequestBenchmark, JMH 1.37, JDK 17.0.2, Windows,-prof gc, 5 x 1 s warmup, 5 x 1 s measurement,mainagainst this branch. A request filter ends each call by throwing a preallocated exception, so nothing reaches the network; both arms then build the same failed future.noStoredCookiesends to a host with no stored cookie and is the baseline.Bytes/op with escape analysis off (
-XX:-DoEscapeAnalysis, 1 fork), which gives exact allocation counts:mainstoredCookiestoredCookienoStoredCookienoStoredCookieAdding the stored cookie costs 920 to 1400 bytes on
main, growing with the header count, and 312 bytes here, independent of it (the store lookup's list, the merged cookie list and the new request).With default JVM flags (2 forks) escape analysis removes part of it, so the figures vary by a few dozen bytes between forks:
storedCookie1800 -> 1056 B/op with 4 headers and 2184 -> 1272 B/op with 16. The host was running other builds, so the timings had error bars of 50 to 100 percent and are not reported.Tests
New tests in
DefaultAsyncHttpClientTestcapture the request that reaches the filters, with no server:mainwith the re-encoded URL above);SIDon the request andsidin the store both go out);DefaultRequestis carried over as the same object, checked reflectively on requests that set every field to a non-null value no other field holds, except theBooleanflags; across three such requests each flag takes both values and each pair of flags differs, so a swapped flag or a constant in its place fails;DefaultRequestgoes through itstoBuilder();DefaultRequestwith a null cookie list gets the stored cookies.RedirectCredentialSecurityTest.crossDomainRedirectLeavesCallerRequestHeadersUnchangedexecutes one built request withAuthorizationandCookieheaders and a stored cookie twice across a cross-origin 302, and checks after each run that the request's headers are unchanged, that the stored cookie was sent (so the run went throughwithCookies) and that both runs sent the credentials to the first origin only../mvnw clean verify -Dgpg.skip=truepasses on JDK 17 (2001 tests; no JDK 11 and no gpg on the host); CI covers JDK 11.Second-agent review
Both reviewers ran from fresh CLI sessions, read-only, with the intent and constraints in the prompt.
AGENTS.md, ongit diff upstream/main...HEAD)gpt-6-astraclaude-opus-5-5Six rounds in all: three before the PR was opened (15ece6b, 3c843f5, f7ff016), and three on the commit that answers the maintainer review (rounds 4 to 6; it was amended before the push after rounds 4 and 5). Fixed from earlier rounds:
Booleanflags totrue, so a swap of two of them inwithCookieswould pass; now two requests make every pair differ (checked by swapping two flags, which fails the test), and the body fields are compared too;DefaultRequestwith a null cookie list failed only on the stored-cookie path; it is now treated as empty;withCookies; it now asserts that the cookie reached the first origin;followRedirectandrefuseCrossOriginBodyOnRedirect, so passingBooleanconstants for those two would pass; a third request now gives each flag both values.Not changed:
HttpHeadersand lists (round 1, guided reviewer as a bug, independent reviewer as a nit). A filter or a reusedRequestBuilderthat mutates them now reaches the original request also when a stored cookie matched. The path without a matching stored cookie already behaves that way, as described under behaviour notes, and deep-copying the headers is the cost this change removes.build()did for a hand-builtDefaultRequest(round 1, independent reviewer, nit), such as a null charset. Such a request fails the same way without a stored cookie.Claude Code on behalf of Pavel Ptashyts
🤖 Generated with Claude Code