Repository: pallets/werkzeug (python)

Issue:
`str(request.headers)` is empty

Greetings from changedetection.io, thanks for your work!

Our tests started failing, turns out related (somehow) to Werkzeug 3.1.0

We have a test endpoint that is looking at the headers that were sent with a request

https://github.com/dgtlmoon/changedetection.io/blob/7029d10f8bd22ac2379fcfe70bf57b67ebee69fc/changedetectionio/tests/util.py#L231

for example

```
    # Where we POST to as a notification
    @live_server.app.route('/test_notification_endpoint', methods=['POST', 'GET'])
    def test_notification_endpoint():
        p = request.headers
```

but `request.headers` is always empty (by empty i mean contains `\r\n`), downgrading to `3.0.6` fixed it (from `3.1.0`)



Tried python 3.10, 3.11, and 3.12

```
pytest                       7.4.4
pytest-flask                 1.3.0
Werkzeug                     3.0.6 (this version is OK)
```

Strangely, all other variables in `request` look perfectly OK, tcpdump shows the headers are being sent without problem, HTTP_ type vars in the request look OK too

thanks!

Pull request description:
fix `str(request.headers)`
Revert the refactored lines in `Headers.__str__` and `Headers.__repr__` to iterate instead of using the internal list directly. Add a test for `str(EnvironHeaders)`.

fixes #2985 

Patch:
diff --git a/CHANGES.rst b/CHANGES.rst
index d4c040e2..1f293869 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -5,6 +5,9 @@ Version 3.1.1
 
 Unreleased
 
+-   Fix an issue that caused ``str(Request.headers)`` to always appear empty.
+    :issue:`2985`
+
 
 Version 3.1.0
 -------------
diff --git a/src/werkzeug/datastructures/headers.py b/src/werkzeug/datastructures/headers.py
index db53cda7..8b274c75 100644
--- a/src/werkzeug/datastructures/headers.py
+++ b/src/werkzeug/datastructures/headers.py
@@ -568,13 +568,13 @@ class Headers(cabc.MutableMapping[str, str]):
     def __str__(self) -> str:
         """Returns formatted headers suitable for HTTP transmission."""
         strs = []
-        for key, value in self._list:
+        for key, value in self.to_wsgi_list():
             strs.append(f"{key}: {value}")
         strs.append("\r\n")
         return "\r\n".join(strs)
 
     def __repr__(self) -> str:
-        return f"{type(self).__name__}({self._list!r})"
+        return f"{type(self).__name__}({list(self)!r})"
 
 
 def _options_header_vkw(value: str, kw: dict[str, t.Any]) -> str:


Test patch:
diff --git a/tests/test_datastructures.py b/tests/test_datastructures.py
index 51799722..dcbc7969 100644
--- a/tests/test_datastructures.py
+++ b/tests/test_datastructures.py
@@ -885,6 +885,10 @@ class TestEnvironHeaders:
         with pytest.raises(TypeError):
             headers |= {"y": "2"}
 
+    def test_str(self) -> None:
+        headers = ds.EnvironHeaders({"CONTENT_LENGTH": "50", "HTTP_HOST": "test"})
+        assert str(headers) == "Content-Length: 50\r\nHost: test\r\n\r\n"
+
 
 class TestHeaderSet:
     storage_class = ds.HeaderSet