@@ -50,7 +50,7 @@ class ResponseCacheStrategy implements ResponseCacheStrategyInterface
5050private $ ageDirectives = [
5151'max-age ' =>null ,
5252's-maxage ' =>null ,
53- 'expires ' =>null ,
53+ 'expires ' =>false ,
5454 ];
5555
5656/**
@@ -81,15 +81,30 @@ public function add(Response $response)
8181return ;
8282 }
8383
84- $ isHeuristicallyCacheable =$ response ->headers ->hasCacheControlDirective ('public ' );
8584$ maxAge =$ response ->headers ->hasCacheControlDirective ('max-age ' ) ? (int )$ response ->headers ->getCacheControlDirective ('max-age ' ) :null ;
86- $ this ->storeRelativeAgeDirective ('max-age ' ,$ maxAge ,$ age ,$ isHeuristicallyCacheable );
8785$ sharedMaxAge =$ response ->headers ->hasCacheControlDirective ('s-maxage ' ) ? (int )$ response ->headers ->getCacheControlDirective ('s-maxage ' ) :$ maxAge ;
88- $ this ->storeRelativeAgeDirective ('s-maxage ' ,$ sharedMaxAge ,$ age ,$ isHeuristicallyCacheable );
89-
9086$ expires =$ response ->getExpires ();
9187$ expires =null !==$ expires ? (int )$ expires ->format ('U ' ) - (int )$ response ->getDate ()->format ('U ' ) :null ;
92- $ this ->storeRelativeAgeDirective ('expires ' ,$ expires >=0 ?$ expires :null ,0 ,$ isHeuristicallyCacheable );
88+
89+ // See https://datatracker.ietf.org/doc/html/rfc7234#section-4.2.2
90+ // If a response is "public" but does not have maximum lifetime, heuristics might be applied.
91+ // Do not store NULL values so the final response can have more limiting value from other responses.
92+ $ isHeuristicallyCacheable =$ response ->headers ->hasCacheControlDirective ('public ' )
93+ &&null ===$ maxAge
94+ &&null ===$ sharedMaxAge
95+ &&null ===$ expires ;
96+
97+ if (!$ isHeuristicallyCacheable ||null !==$ maxAge ||null !==$ expires ) {
98+ $ this ->storeRelativeAgeDirective ('max-age ' ,$ maxAge ,$ expires ,$ age );
99+ }
100+
101+ if (!$ isHeuristicallyCacheable ||null !==$ sharedMaxAge ||null !==$ expires ) {
102+ $ this ->storeRelativeAgeDirective ('s-maxage ' ,$ sharedMaxAge ,$ expires ,$ age );
103+ }
104+
105+ if (null !==$ expires ) {
106+ $ this ->ageDirectives ['expires ' ] =true ;
107+ }
93108 }
94109
95110/**
@@ -102,7 +117,7 @@ public function update(Response $response)
102117return ;
103118 }
104119
105- // Remove validation related headers of themaster response,
120+ // Remove validation related headers of thefinal response,
106121// because some of the response content comes from at least
107122// one embedded response (which likely has a different caching strategy).
108123$ response ->setEtag (null );
@@ -145,9 +160,9 @@ public function update(Response $response)
145160 }
146161 }
147162
148- if (is_numeric ( $ this ->ageDirectives ['expires ' ]) ) {
163+ if ($ this ->ageDirectives ['expires ' ] && null !== $ maxAge ) {
149164$ date =clone $ response ->getDate ();
150- $ date =$ date ->modify ('+ ' .( $ this -> ageDirectives [ ' expires ' ] + $ this -> age ) .' seconds ' );
165+ $ date =$ date ->modify ('+ ' .$ maxAge .' seconds ' );
151166$ response ->setExpires ($ date );
152167 }
153168 }
@@ -200,33 +215,16 @@ private function willMakeFinalResponseUncacheable(Response $response): bool
200215 * we have to subtract the age so that the value is normalized for an age of 0.
201216 *
202217 * If the value is lower than the currently stored value, we update the value, to keep a rolling
203- * minimal value of each instruction.
204- *
205- * If the value is NULL and the isHeuristicallyCacheable parameter is false, the directive will
206- * not be set on the final response. In this case, not all responses had the directive set and no
207- * value can be found that satisfies the requirements of all responses. The directive will be dropped
208- * from the final response.
209- *
210- * If the isHeuristicallyCacheable parameter is true, however, the current response has been marked
211- * as cacheable in a public (shared) cache, but did not provide an explicit lifetime that would serve
212- * as an upper bound. In this case, we can proceed and possibly keep the directive on the final response.
218+ * minimal value of each instruction. If the value is NULL, the directive will not be set on the final response.
213219 */
214- private function storeRelativeAgeDirective (string $ directive , ?int $ value ,int $ age , bool $ isHeuristicallyCacheable )
220+ private function storeRelativeAgeDirective (string $ directive , ?int $ value ,? int $ expires , int $ age ): void
215221 {
216- if (null ===$ value ) {
217- if ($ isHeuristicallyCacheable ) {
218- /*
219- * See https://datatracker.ietf.org/doc/html/rfc7234#section-4.2.2
220- * This particular response does not require maximum lifetime; heuristics might be applied.
221- * Other responses, however, might have more stringent requirements on maximum lifetime.
222- * So, return early here so that the final response can have the more limiting value set.
223- */
224- return ;
225- }
222+ if (null ===$ value &&null ===$ expires ) {
226223$ this ->ageDirectives [$ directive ] =false ;
227224 }
228225
229226if (false !==$ this ->ageDirectives [$ directive ]) {
227+ $ value =min ($ value ??PHP_INT_MAX ,$ expires ??PHP_INT_MAX );
230228$ value -=$ age ;
231229$ this ->ageDirectives [$ directive ] =null !==$ this ->ageDirectives [$ directive ] ?min ($ this ->ageDirectives [$ directive ],$ value ) :$ value ;
232230 }