Repository: pallets/werkzeug (python)

Issue:
Add support for RFC5861 Cache-Control headers
[RFC5861](https://datatracker.ietf.org/doc/html/rfc5861) standardised two Cache-Control extensions:
1. `stale-while-revalidate`
2. `stale-if-error`

Please add support for these in the standard werkzerg `ResponseCacheControl`.

One use case of these extensions is to allow sites to have their CDNs "coalesce" requests for content when it goes stale.  Another is to serve a stale representation from the CDN when the site is having an outage.  It's possible to solve this problem without changes to werkzeug, but as it's a standard feature, I think it would be nice to have it in the library.

Pull request description:
Add support for RFC5861 Cache-Control headers
This PR adds the RFC5861 headers (`stale-while-revalidate` and `stale-if-error`) to the werkzeug `ResponseCacheControl` class.  This is done by just adding more properties.

fixes #2948

(Haven't yet added all the right docs and so on, but will do so)
<!--
Ensure each step in CONTRIBUTING.rst is complete, especially the following:

- Add tests that demonstrate the correct behavior of the change. Tests
  should fail without the change.
- Add or update relevant docs, in the docs folder and in code.
- Add an entry in CHANGES.rst summarizing the change and linking to the issue.
- Add `.. versionchanged::` entries in any relevant code docs.
-->


Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index 3fcd0ae4..4c21d209 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -9,6 +9,8 @@ Unreleased
 -   ``CacheControl.no_transform`` is a boolean when present. ``min_fresh`` is
     ``None`` when not present. Added the ``must_understand`` attribute. Fixed
     some typing issues on cache control. :issue:`2881`
+-   Add ``stale_while_revalidate`` and ``stale_if_error`` properties to
+    ``ResponseCacheControl``. :issue:`2948`
 -   Add 421 ``MisdirectedRequest`` HTTP exception. :issue:`2850`
 
 
diff --git a/docs/datastructures.rst b/docs/datastructures.rst
index 01432f41..92c96993 100644
--- a/docs/datastructures.rst
+++ b/docs/datastructures.rst
@@ -69,26 +69,12 @@ HTTP Related
 .. autoclass:: LanguageAccept
 
 .. autoclass:: RequestCacheControl
-   :members:
-
-   .. autoattribute:: no_cache
-
-   .. autoattribute:: no_store
-
-   .. autoattribute:: max_age
-
-   .. autoattribute:: no_transform
+    :members:
+    :inherited-members:
 
 .. autoclass:: ResponseCacheControl
-   :members:
-
-   .. autoattribute:: no_cache
-
-   .. autoattribute:: no_store
-
-   .. autoattribute:: max_age
-
-   .. autoattribute:: no_transform
+    :members:
+    :inherited-members:
 
 .. autoclass:: ETags
    :members:
diff --git a/src/werkzeug/datastructures/cache_control.py b/src/werkzeug/datastructures/cache_control.py
index 6ff4ecee..fa7ed0a7 100644
--- a/src/werkzeug/datastructures/cache_control.py
+++ b/src/werkzeug/datastructures/cache_control.py
@@ -165,7 +165,8 @@ class ResponseCacheControl(_CacheControl):
        ``no_transform`` is a boolean when present.
 
     .. versionchanged:: 3.1
-        Added the ``must_understand`` attribute.
+        Added the ``must_understand``, ``stale_while_revalidate``, and
+        ``stale_if_error`` attributes.
 
     .. versionchanged:: 2.1.1
         ``s_maxage`` converts the value to an int.
@@ -186,6 +187,8 @@ class ResponseCacheControl(_CacheControl):
     s_maxage = cache_control_property("s-maxage", None, int)
     immutable = cache_control_property("immutable", None, bool)
     must_understand = cache_control_property("must-understand", None, bool)
+    stale_while_revalidate = cache_control_property("stale-while-revalidate", None, int)
+    stale_if_error = cache_control_property("stale-if-error", None, int)
 
 
 # circular dependencies
diff --git a/src/werkzeug/datastructures/cache_control.pyi b/src/werkzeug/datastructures/cache_control.pyi
index 4c9f4df3..93c595db 100644
--- a/src/werkzeug/datastructures/cache_control.pyi
+++ b/src/werkzeug/datastructures/cache_control.pyi
@@ -116,3 +116,15 @@ class ResponseCacheControl(_CacheControl):
     def must_understand(self, value: bool | None) -> None: ...
     @must_understand.deleter
     def must_understand(self) -> None: ...
+    @property
+    def stale_while_revalidate(self) -> int | None: ...
+    @stale_while_revalidate.setter
+    def stale_while_revalidate(self, value: int | None) -> None: ...
+    @stale_while_revalidate.deleter
+    def stale_while_revalidate(self) -> None: ...
+    @property
+    def stale_if_error(self) -> int | None: ...
+    @stale_if_error.setter
+    def stale_if_error(self, value: int | None) -> None: ...
+    @stale_if_error.deleter
+    def stale_if_error(self) -> None: ...


Test patch:
diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py
index 830dfefd..a681c022 100644
--- a/tests/test_datastructures.py
+++ b/tests/test_datastructures.py
@@ -973,6 +973,18 @@ class TestCacheControl:
         cc = ds.ResponseCacheControl()
         assert cc.must_understand is False
 
+    def test_stale_while_revalidate(self):
+        cc = ds.ResponseCacheControl([("stale-while-revalidate", "1")])
+        assert cc.stale_while_revalidate == 1
+        cc = ds.ResponseCacheControl()
+        assert cc.stale_while_revalidate is None
+
+    def test_stale_if_error(self):
+        cc = ds.ResponseCacheControl([("stale-if-error", "1")])
+        assert cc.stale_if_error == 1
+        cc = ds.ResponseCacheControl()
+        assert cc.stale_while_revalidate is None
+
 
 class TestContentSecurityPolicy:
     def test_construct(self):