@@ -50,7 +50,7 @@ class ResponseCacheStrategy implements ResponseCacheStrategyInterface
5050 private $ 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)
8181 return ;
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)
102117 return ;
103118 }
104119
105- // Remove validation related headers of the master response,
120+ // Remove validation related headers of the final 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
229226 if (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 }
0 commit comments