Repository: pallets/werkzeug

Issue:
max_age in send_file should enforce integer
## Description

Currently any value passed into `send_file` for the `max_age` ends up as the value of `max-age` within the `Cache-Control` header. The type hints specify that either an integer or a callable that takes a string and returns an integer. However, since these are just hints (and not strong typing) they are not enforced. 

The specification for that value states that it must be an integer (not for example a float). So I think it is a bug that Werkzeug does not atleast attempt to enforce the value being an int with an attempt at conversion on: https://github.com/pallets/werkzeug/blob/3c5d3c9bd0d9ce64590f0af8997a38f3823b368d/src/werkzeug/utils.py#L784

## Replication:

Call `send_file` with the `max_age` argument value of: `1.0`, and observe that `1.0` is the value that comes out in the header.

## Expectation:

When a convertible value is passed, an integer is what ends up in the header.

## Environment:

- Python version: 3.8.2
- Werkzeug version: 0.16.1

Attempt to enforce max-age as an integer in `send_file`
This change attempts to enforce an integer value for max-age in send_file in order to comply with the specification for `Cache-Control: max-age=<value>`.

- fixes #2230

Checklist:

- [x] Add tests that demonstrate the correct behavior of the change. Tests should fail without the change.
- [x] Add or update relevant docs, in the docs folder and in code.
- [x] Add an entry in `CHANGES.rst` summarizing the change and linking to the issue.
- [x] Add `.. versionchanged::` entries in any relevant code docs.
- [x] Run `pre-commit` hooks and fix any issues.
- [x] Run `pytest` and `tox`, no tests failed.



Discussion:
Python in general does not do runtime type checking. I don't plan to change that for this case.
I understand that Python itself does not do this. 

But in order to stay in-line with the specification for Cache-Control's max-age value, I think it would provide a better user experience to attempt to align it. Right now, this can create unexpected behavior downstream with no warning. 

If not enforcement, would you be open to me putting up a PR to atleast give a warning of some sort that the resulting value it out of spec?
For what it is worth, Django does something similar to what I was suggesting: https://github.com/django/django/blob/68b8eda78864b78d1b8b2529ec119ec440f8458a/django/utils/cache.py#L76
Another note (appeal to spec authority incoming): the spec for [max-age](https://httpwg.org/specs/rfc7234.html#cache-request-directive.max-age) says that the value _must_ be a [delta-seconds](https://httpwg.org/specs/rfc7234.html#delta-seconds), which has the following definition:

> The delta-seconds rule specifies a non-negative integer, representing time in seconds.

so without enforcing that, werkzeug is in danger of being out of spec based on user input.
Werkzeug isn't in danger of being out of spec. A program that decides to pass an invalid value is out of spec, but in general we/Python don't validate/coerce every value that's passed to a function. I don't plan to change this at this time.
Would it be more appropriate for flask, as a framework built on top of werkzeug, to do this validation for the user?
I think it would be better to alter the `_CacheControl._set_cache_value` to utilise the `type` argument, rather than just in the send_file function.

Resolving patch:
diff --git a/src/werkzeug/datastructures.py b/src/werkzeug/datastructures.py
--- a/src/werkzeug/datastructures.py
+++ b/src/werkzeug/datastructures.py
@@ -2014,6 +2014,10 @@ class _CacheControl(UpdateDictMixin, dict):
     to subclass it and add your own items have a look at the sourcecode for
     that class.
 
+    .. versionchanged:: 2.1.0
+        Setting int properties such as ``max_age`` will convert the
+        value to an int.
+
     .. versionchanged:: 0.4
 
        Setting `no_cache` or `private` to boolean `True` will set the implicit
@@ -2072,7 +2076,7 @@ def _set_cache_value(self, key, value, type):
             elif value is True:
                 self[key] = None
             else:
-                self[key] = value
+                self[key] = type(value)
 
     def _del_cache_value(self, key):
         """Used internally by the accessor properties."""
@@ -2102,6 +2106,10 @@ class RequestCacheControl(ImmutableDictMixin, _CacheControl):
     you plan to subclass it and add your own items have a look at the sourcecode
     for that class.
 
+    .. versionchanged:: 2.1.0
+        Setting int properties such as ``max_age`` will convert the
+        value to an int.
+
     .. versionadded:: 0.5
        In previous versions a `CacheControl` class existed that was used
        both for request and response.
@@ -2122,6 +2130,10 @@ class ResponseCacheControl(_CacheControl):
     you plan to subclass it and add your own items have a look at the sourcecode
     for that class.
 
+    .. versionchanged:: 2.1.0
+        Setting int properties such as ``max_age`` will convert the
+        value to an int.
+
     .. versionadded:: 0.5
        In previous versions a `CacheControl` class existed that was used
        both for request and response.


Test patch:
diff --git a/tests/test_http.py b/tests/test_http.py
--- a/tests/test_http.py
+++ b/tests/test_http.py
@@ -115,6 +115,10 @@ def test_cache_control_header(self):
         assert c.private == "*"
         del c.private
         assert c.private is None
+        # max_age is an int, other types are converted
+        c.max_age = 3.1
+        assert c.max_age == 3
+        del c.max_age
         assert c.to_header() == "no-cache"
 
     def test_csp_header(self):