Repository: pallets/werkzeug

Issue:
Default Content-Type header re-applied when using test client
When using 'make_response' in a view function, default headers can be cleared, such as the default 'Content-Type' header. This works fine when using `flask run`, but when using the test client, the default 'Content-Type' header is re-applied, causing the test response to be different from the actual server response.

## Current Behaviour
Given the following flask app (test_app.py):
```python
from flask import Flask, make_response

app = Flask(__name__)


@app.route("/")
def hello_world():
    response = make_response("<p>Hello, World!</p>")
    response.headers.clear()
    return response


if __name__ == '__main__':
    client = app.test_client()
    response = client.get('/')
    print(response.headers)
```
When you do `flask run` in one terminal window, and curl in another, the output is as expected (No Content-Type header):
```
>> curl -i http://127.0.0.1:5000/
HTTP/1.1 200 OK
Server: Werkzeug/2.1.2 Python/3.10.5
Date: Fri, 08 Jul 2022 12:58:44 GMT
Content-Length: 20
Connection: close
```
When you run the application standalone, so the test client is triggered however, the default flask 'Content-Type' header is applied:
```
>> python test_app.py
Content-Length: 20
Content-Type: text/html; charset=utf-8
```

## Expected behavior
I expect, if default headers are cleared in the view function, that they are not re-applied by the test client, especially since this doesn't match the actual response of the server.

## Environment:
Python 3.10.5
Flask 2.1.2
Werkzeug 2.1.2

## Cause
The `test_client` method initializes the FlaskClient class with `self.response_class` (flask.wrappers.Response), which in term passes this as a 'response_wrapper' to the werkzeug.test.Client `__init__` method. Since `flask.wrappers.Response.default_mimetype` has the value seen above, this is always applied to a test_client response in case there's no 'Content-Type' header.



Discussion:
Seems like setting `TestResponse.default_mimetype = None` should do the trick, happy to review a PR. Note that this was moved to Werkzeug, which is where `TestResponse` is defined.
@davidism I'm happy to submit a PR, but as far as I can see (On the latest 'main' branch for flask), the 'app.test_client' defined [here](https://github.com/pallets/flask/blob/900e11850a201717599fb12fb7cd76c37078206d/src/flask/app.py#L1068) still passes the Flask.Response as second argument to the FlaskClient, which in term passes the 'Flask.Response' as the second argument to werkzeug.test.Client, meaning the Flask.Response is the second base class when the 'WrapperTestResponse' is constructed, in term the default_mimetype from Flask.Response is used for the WrapperTestResponse (see [here](https://github.com/pallets/werkzeug/blob/acb1b04ef2e0ff269dc5313462c341dfaacb1b5b/src/werkzeug/test.py#L870)), or am I missing something?

If the above is the case, the fix should be inside the 'flask' repo, e.g. by changing line 1067 - 1069 in app.test_client() to:
```python
return cls(  # type: ignore
    self, type('AppTestResponse', (self.response_class,), {'default_mimetype': None}), use_cookies=use_cookies, **kwargs
)
```

Please let me know your thoughts on this!

_Note that this issue is about the way it's used by Flask. As you mention, the same issue may occur when using purely WerkZeug, and initializing the test.client with response_wrapper = None. In that case the TestResponse class from Werkzeug is used, which also has a default mimetype, but then set to text/plain, rather than the text/html that I'm seeing..._

Resolving patch:
diff --git a/src/werkzeug/sansio/response.py b/src/werkzeug/sansio/response.py
--- a/src/werkzeug/sansio/response.py
+++ b/src/werkzeug/sansio/response.py
@@ -92,7 +92,7 @@ class Response:
     default_status = 200
 
     #: the default mimetype if none is provided.
-    default_mimetype = "text/plain"
+    default_mimetype: t.Optional[str] = "text/plain"
 
     #: Warn if a cookie header exceeds this size. The default, 4093, should be
     #: safely `supported by most browsers <cookie_>`_. A cookie larger than


Test patch:
diff --git a/src/werkzeug/test.py b/src/werkzeug/test.py
--- a/src/werkzeug/test.py
+++ b/src/werkzeug/test.py
@@ -1280,6 +1280,10 @@ class TestResponse(Response):
     serving a file, call :meth:`close` to close any open files and
     prevent Python showing a ``ResourceWarning``.
 
+    .. versionchanged:: 2.2
+        Set the ``default_mimetype`` to None to prevent a mimetype being
+        assumed if missing.
+
     .. versionchanged:: 2.1
         Removed deprecated behavior for treating the response instance
         as a tuple.
@@ -1288,6 +1292,9 @@ class TestResponse(Response):
         Test client methods always return instances of this class.
     """
 
+    default_mimetype = None
+    # Don't assume a mimetype, instead use whatever the response provides
+
     request: Request
     """A request object with the environ used to make the request that
     resulted in this response.
diff --git a/tests/test_test.py b/tests/test_test.py
--- a/tests/test_test.py
+++ b/tests/test_test.py
@@ -876,3 +876,16 @@ def app(request):
 
     response = client.get("/%3f?")  # escaped ? in path, and empty query string
     assert response.text == "/?\n/%3f?"
+
+
+def no_response_headers_app(environ, start_response):
+    """A WSGI application which returns a resposne with no headers."""
+    response = Response("Response")
+    response.headers.clear()
+    return response(environ, start_response)
+
+
+def test_no_content_type_header_addition():
+    c = Client(no_response_headers_app)
+    response = c.open()
+    assert response.headers == Headers([("Content-Length", "8")])