Don't send cookies after they reached their expiry time (#6756)

* Don't send cookies after they reached their expiry time

The cookie specs used by the HTTP Cookie Manager do not check the expiry
date when matching cookies for an URL, so cookies that were valid when
they were received kept being sent after they expired.

Filter expired cookies in HC4CookieHandler#getCookiesForUrl, so they are
not sent anymore. Session cookies, which have no expiry date, are not
affected.

Closes #6428

* Document not sending expired cookies as incompatible change

Per review feedback on PR #6756: move the changes.xml entry from
Bug fixes to a new Incompatible changes section and document the
behavior change in the HTTP Cookie Manager reference, warning about
the impact on long-running tests that outlive a cookie's expiry.

* Move Incompatible changes link before Changes link in 6.0.0 summary

Per review feedback on PR #6756
diff --git a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/HC4CookieHandler.java b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/HC4CookieHandler.java
index 990b33e..276d913 100644
--- a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/HC4CookieHandler.java
+++ b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/HC4CookieHandler.java
@@ -18,6 +18,7 @@
 package org.apache.jmeter.protocol.http.control;
 
 import java.net.URL;
+import java.time.Instant;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
@@ -228,7 +229,14 @@
         CookieOrigin cookieOrigin = new CookieOrigin(host, port, path, secure);
 
         List<org.apache.http.cookie.Cookie> cookiesValid = new ArrayList<>();
+        // #6428 Cookies must not be sent after they reached their expiry time.
+        // The cookie specs used here do not check the expiry date in match(),
+        // so expired cookies have to be filtered out explicitly.
+        Date now = Date.from(Instant.now());
         for (org.apache.http.cookie.Cookie cookie : cookies) {
+            if (cookie.isExpired(now)) {
+                continue;
+            }
             if (cookieSpec.match(cookie, cookieOrigin)) {
                 cookiesValid.add(cookie);
             }
diff --git a/src/protocol/http/src/test/java/org/apache/jmeter/protocol/http/control/TestHC4CookieManager.java b/src/protocol/http/src/test/java/org/apache/jmeter/protocol/http/control/TestHC4CookieManager.java
index 8b24254..14bd099 100644
--- a/src/protocol/http/src/test/java/org/apache/jmeter/protocol/http/control/TestHC4CookieManager.java
+++ b/src/protocol/http/src/test/java/org/apache/jmeter/protocol/http/control/TestHC4CookieManager.java
@@ -259,6 +259,27 @@
         assertNull(s);
     }
 
+    // Test cookie that was valid when received is not sent anymore
+    // once its expiry time has passed (#6428)
+    @Test
+    public void testCookieExpiredAfterReceptionIsNotSent() throws Exception {
+        URL url = new URL("http://a.b.c/");
+        man.addCookieFromHeader("test=1; expires=Wed, 01-Jan-2099 00:00:00 GMT", url);
+        assertEquals(1, man.getCookieCount());
+        assertEquals("test=1", man.getCookieHeaderForURL(url));
+        // Simulate the passing of time: the cookie is expired by now
+        man.get(0).setExpires(System.currentTimeMillis() / 1000L - 3600L);
+        assertNull(man.getCookieHeaderForURL(url), "expired cookie must not be sent");
+    }
+
+    // Test session cookie (no expiry date) is still sent
+    @Test
+    public void testSessionCookieWithoutExpiryIsStillSent() throws Exception {
+        URL url = new URL("http://a.b.c/");
+        man.addCookieFromHeader("test=1", url);
+        assertEquals("test=1", man.getCookieHeaderForURL(url));
+    }
+
     // Test New cookie is returned
     @Test
     public void testNewCookie() throws Exception {
diff --git a/xdocs/changes.xml b/xdocs/changes.xml
index 13e0d09..56b5657 100644
--- a/xdocs/changes.xml
+++ b/xdocs/changes.xml
@@ -57,10 +57,21 @@
 Summary
 </p>
 <ul>
+<li><a href="#Incompatible changes">Incompatible changes</a></li>
 <li><a href="#Changes">Changes</a></li>
 <li><a href="#Bug fixes">Bug fixes</a></li>
 </ul>
 
+<!-- =================== Incompatible changes =================== -->
+
+<ch_section>Incompatible changes</ch_section>
+<ul>
+  <li><issue>6428</issue>HTTP Cookie Manager no longer sends cookies after they reached their expiry time.
+  Previously, expired cookies kept being sent indefinitely. Long-running tests that outlive a cookie's
+  expiry time may experience authentication or session failures and must handle re-authentication
+  or session refresh.</li>
+</ul>
+
   <ch_section>Changes</ch_section>
   <h3>General</h3>
   <ul>
diff --git a/xdocs/usermanual/component_reference.xml b/xdocs/usermanual/component_reference.xml
index 663b24f..783051d 100644
--- a/xdocs/usermanual/component_reference.xml
+++ b/xdocs/usermanual/component_reference.xml
@@ -3847,6 +3847,12 @@
 If you have bugged behaviour or want Cross-Domain cookies to be used, define the JMeter property "<code>CookieManager.check.cookies=false</code>".
 </p>
 <p>
+Cookies that reached their expiry time are no longer sent with requests.
+Be aware that long-running tests, such as endurance tests that outlive a cookie's expiry time,
+will no longer send the cookie once it has expired. This can lead to authentication or session
+errors, so such test plans must handle re-authentication or session refresh.
+</p>
+<p>
 Received Cookies can be stored as JMeter thread variables.
 To save cookies as variables, define the property "<code>CookieManager.save.cookies=true</code>".
 Also, cookies names are prefixed with "<code>COOKIE_</code>" before they are stored (this avoids accidental corruption of local variables)