Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -205,36 +205,38 @@ private List<Cookie> get(String domain, String path, boolean secure) {
List<Cookie> results = null;

while (MiscUtils.isNonEmpty(subDomain)) {
final List<Cookie> storedCookies = getStoredCookies(subDomain, path, secure, exactDomainMatch);
subDomain = DomainUtils.getSubDomain(subDomain);
exactDomainMatch = false;
if (storedCookies.isEmpty()) {
continue;
}
// Lazily allocate a single result list and append matches straight into it; an imperative
// scan avoids the per-sub-domain-level Stream pipeline (filter/map stages, two capturing
// lambdas, a spliterator and the Collectors.toList intermediate list) that this ran on every
// cookie-enabled request. Cookie header order is re-sorted by Netty's ClientCookieEncoder,
// so the (preserved) entrySet iteration order is not observable on the wire.
if (results == null) {
results = new ArrayList<>(4);
}
results.addAll(storedCookies);
collectStoredCookies(subDomain, path, secure, exactDomainMatch, results);
subDomain = DomainUtils.getSubDomain(subDomain);
exactDomainMatch = false;
}

return results == null ? Collections.emptyList() : results;
return results == null || results.isEmpty() ? Collections.emptyList() : results;
}

private List<Cookie> getStoredCookies(String domain, String path, boolean secure, boolean isExactMatch) {
private void collectStoredCookies(String domain, String path, boolean secure, boolean isExactMatch, List<Cookie> out) {
final Map<CookieKey, StoredCookie> innerMap = cookieJar.get(domain);
if (innerMap == null) {
return Collections.emptyList();
return;
}

return innerMap.entrySet().stream().filter(pair -> {
CookieKey key = pair.getKey();
StoredCookie storedCookie = pair.getValue();
boolean hasCookieExpired = hasCookieExpired(storedCookie.cookie, storedCookie.createdAt);
return !hasCookieExpired &&
(isExactMatch || !storedCookie.hostOnly) &&
pathsMatch(key.path, path) &&
(secure || !storedCookie.cookie.isSecure());
}).map(v -> v.getValue().cookie).collect(Collectors.toList());
for (Map.Entry<CookieKey, StoredCookie> entry : innerMap.entrySet()) {
CookieKey key = entry.getKey();
StoredCookie storedCookie = entry.getValue();
if (!hasCookieExpired(storedCookie.cookie, storedCookie.createdAt)
&& (isExactMatch || !storedCookie.hostOnly)
&& pathsMatch(key.path, path)
&& (secure || !storedCookie.cookie.isSecure())) {
out.add(storedCookie.cookie);
}
}
}

private void removeExpired() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
/*
* Copyright (c) 2026 AsyncHttpClient Project. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.asynchttpclient.cookie;

import io.netty.handler.codec.http.cookie.ClientCookieDecoder;
import io.netty.handler.codec.http.cookie.Cookie;
import org.asynchttpclient.uri.Uri;
import org.junit.jupiter.api.Test;

import java.util.List;
import java.util.Set;
import java.util.TreeSet;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Behavior-equivalence tests for the imperative {@link ThreadSafeCookieStore#get(Uri)} scan that
* replaced the per-sub-domain Stream pipeline. The returned cookie set must be unchanged; iteration
* order is not contractual (Netty's {@code ClientCookieEncoder} re-sorts on the wire), so results are
* compared as a SET of {@code name=value} pairs.
*/
public class ThreadSafeCookieStoreGetTest {

private static Set<String> namesValues(List<Cookie> cookies) {
Set<String> out = new TreeSet<>();
for (Cookie c : cookies) {
out.add(c.name() + '=' + c.value());
}
return out;
}

private static Set<String> setOf(String... values) {
Set<String> out = new TreeSet<>();
for (String v : values) {
out.add(v);
}
return out;
}

@Test
public void returnsCookieOnExactDomainAndPath() {
ThreadSafeCookieStore store = new ThreadSafeCookieStore();
store.add(Uri.create("http://www.foo.com/bar"),
ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; path=/bar"));

assertEquals(setOf("ALPHA=VALUE1"), namesValues(store.get(Uri.create("http://www.foo.com/bar/baz"))));
}

@Test
public void inheritsParentDomainCookieButExcludesHostOnlyCookie() {
ThreadSafeCookieStore store = new ThreadSafeCookieStore();
// Domain cookie on .foo.com -> visible to sub.foo.com
store.add(Uri.create("http://foo.com/"),
ClientCookieDecoder.LAX.decode("DOMAINCK=DV; Domain=foo.com; Path=/"));
// Host-only cookie on foo.com (no Domain attr) -> NOT visible to a sub-domain request
store.add(Uri.create("http://foo.com/"),
ClientCookieDecoder.LAX.decode("HOSTONLY=HV; Path=/"));

// Request to the sub-domain: sees the domain cookie, not the host-only one.
assertEquals(setOf("DOMAINCK=DV"), namesValues(store.get(Uri.create("http://sub.foo.com/"))));
// Request to the exact host: sees both.
assertEquals(setOf("DOMAINCK=DV", "HOSTONLY=HV"), namesValues(store.get(Uri.create("http://foo.com/"))));
}

@Test
public void excludesSecureCookieOnInsecureRequest() {
ThreadSafeCookieStore store = new ThreadSafeCookieStore();
store.add(Uri.create("https://www.foo.com/"),
ClientCookieDecoder.LAX.decode("SEC=SV; Domain=www.foo.com; Path=/; Secure"));
store.add(Uri.create("http://www.foo.com/"),
ClientCookieDecoder.LAX.decode("PLAIN=PV; Domain=www.foo.com; Path=/"));

// Insecure request: only the non-secure cookie.
assertEquals(setOf("PLAIN=PV"), namesValues(store.get(Uri.create("http://www.foo.com/"))));
// Secure request: both.
assertEquals(setOf("SEC=SV", "PLAIN=PV"), namesValues(store.get(Uri.create("https://www.foo.com/"))));
}

@Test
public void excludesExpiredCookie() {
ThreadSafeCookieStore store = new ThreadSafeCookieStore();
store.add(Uri.create("http://www.foo.com/bar"),
ClientCookieDecoder.LAX.decode("LIVE=VALUE1; Domain=www.foo.com; path=/bar"));
// Max-Age=0 expires immediately; the store drops it on add, so it must never be returned.
store.add(Uri.create("http://www.foo.com/bar"),
ClientCookieDecoder.LAX.decode("DEAD=GONE; Domain=www.foo.com; path=/bar; Max-Age=0"));

assertEquals(setOf("LIVE=VALUE1"), namesValues(store.get(Uri.create("http://www.foo.com/bar"))));
}

@Test
public void returnsMultipleDistinctCookiesAtSameDomainPath() {
ThreadSafeCookieStore store = new ThreadSafeCookieStore();
Uri uri = Uri.create("http://www.foo.com/bar");
store.add(uri, ClientCookieDecoder.LAX.decode("ALPHA=AV; Domain=www.foo.com; path=/bar"));
store.add(uri, ClientCookieDecoder.LAX.decode("BETA=BV; Domain=www.foo.com; path=/bar"));

assertEquals(setOf("ALPHA=AV", "BETA=BV"), namesValues(store.get(uri)));
}

@Test
public void returnsEmptyForUnknownDomain() {
ThreadSafeCookieStore store = new ThreadSafeCookieStore();
store.add(Uri.create("http://www.foo.com/"),
ClientCookieDecoder.LAX.decode("ALPHA=VALUE1; Domain=www.foo.com; Path=/"));

List<Cookie> result = store.get(Uri.create("http://www.bar.com/"));
assertTrue(result.isEmpty(), "no cookies should match an unrelated domain");
}
}
Loading