Repository: pallets/werkzeug (python)

Issue:
json: Return 415 if wrong Content-Type
Amending to #2355:
When a request contains a wrong or no Content-Type, not 400 Bad Request should be returned but more precisely [415 Unsupported Media Type](https://httpwg.org/specs/rfc9110.html#status.415). This greatly helps tracking down the reason of malformed requests.

### Replicate:
Using Flask
```
from flask import Flask, request
app = Flask(__name__)
@app.post('/')
def test():
    return request.json
```
```
curl -i localhost:5000 -d '{"json": "test"}'
HTTP/1.1 400 BAD REQUEST
Server: Werkzeug/2.2.2 Python/3.10.6
Date: Thu, 24 Nov 2022 15:44:58 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 193
Connection: close

<!doctype html>
<html lang=en>
<title>400 Bad Request</title>
<h1>Bad Request</h1>
<p>Did not attempt to load JSON data because the request Content-Type was not &#39;application/json&#39;.</p>
```

### Expected behaviour
```
curl -i localhost:5000 -d '{"json": "test"}'
HTTP/1.1 415 UNSUPPORTED MEDIA TYPE
Server: Werkzeug/2.2.2 Python/3.10.6
Date: Thu, 24 Nov 2022 15:47:46 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 215
Connection: close

<!doctype html>
<html lang=en>
<title>415 Unsupported Media Type</title>
<h1>Unsupported Media Type</h1>
<p>Did not attempt to load JSON data because the request Content-Type was not &#39;application/json&#39;.</p>
```

Environment:

- Python version: 3.10.6
- Werkzeug version: 2.2.2


Pull request:
json: Return 415 if wrong Content-Type
Return HTTP status 415 instead of 400 to indicate a problem with Content-Type more precisely when processing json requests.

amends #2355
fixes #2550

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.


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 02c18bc2..5d5b9b99 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -3,12 +3,12 @@ ci:
   autoupdate_schedule: monthly
 repos:
   - repo: https://github.com/asottile/pyupgrade
-    rev: v3.1.0
+    rev: v3.2.0
     hooks:
       - id: pyupgrade
         args: ["--py37-plus"]
   - repo: https://github.com/asottile/reorder_python_imports
-    rev: v3.8.4
+    rev: v3.9.0
     hooks:
       - id: reorder-python-imports
         name: Reorder Python imports (src, tests)
@@ -32,7 +32,7 @@ repos:
           - flake8-bugbear
           - flake8-implicit-str-concat
   - repo: https://github.com/peterdemin/pip-compile-multi
-    rev: v2.4.6
+    rev: v2.5.0
     hooks:
       - id: pip-compile-multi-verify
   - repo: https://github.com/pre-commit/pre-commit-hooks
diff --git a/CHANGES.rst b/CHANGES.rst
index 2885ef45..e8a6c46e 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -17,6 +17,9 @@ Unreleased
     where the length is invalid, instead of raising an ``AssertionError``. :issue:`2531`
 -   Address remaining ``ResourceWarning`` related to the socket used by ``run_simple``.
     Remove ``prepare_socket``, which now happens when creating the server. :issue:`2421`
+-   ``Request.get_json()`` will raise a 415 ``UnsupportedMediaType`` error if the
+    ``Content-Type`` header is not ``application/json``. This makes a
+    very common source of confusion even more visible. :issue:`2550`
 
 
 Version 2.2.2
diff --git a/src/werkzeug/wrappers/request.py b/src/werkzeug/wrappers/request.py
index 283e5922..6fbbc825 100644
--- a/src/werkzeug/wrappers/request.py
+++ b/src/werkzeug/wrappers/request.py
@@ -19,6 +19,7 @@ from ..utils import environ_property
 from ..wsgi import _get_server
 from ..wsgi import get_input_stream
 from werkzeug.exceptions import BadRequest
+from werkzeug.exceptions import UnsupportedMediaType
 
 if t.TYPE_CHECKING:
     import typing_extensions as te
@@ -532,10 +533,10 @@ class Request(_SansIORequest):
         Calls :meth:`get_json` with default arguments.
 
         If the request content type is not ``application/json``, this
-        will raise a 400 Bad Request error.
+        will raise a 415 Unsupported Media Type error.
 
-        .. versionchanged:: 2.1
-            Raise a 400 error if the content type is incorrect.
+        .. versionchanged:: 2.2.3
+            Raise a 415 error if the content type is incorrect.
         """
         return self.get_json()
 
@@ -564,7 +565,7 @@ class Request(_SansIORequest):
         (:mimetype:`application/json`, see :attr:`is_json`), or parsing
         fails, :meth:`on_json_loading_failed` is called and
         its return value is used as the return value. By default this
-        raises a 400 Bad Request error.
+        raises a 415 Unsupported Media Type resp. 400 Bad Request error.
 
         :param force: Ignore the mimetype and always try to parse JSON.
         :param silent: Silence mimetype and parsing errors, and
@@ -572,8 +573,8 @@ class Request(_SansIORequest):
         :param cache: Store the parsed JSON to return for subsequent
             calls.
 
-        .. versionchanged:: 2.1
-            Raise a 400 error if the content type is incorrect.
+        .. versionchanged:: 2.2.3
+            Raise a 415 error if the content type is incorrect.
         """
         if cache and self._cached_json[silent] is not Ellipsis:
             return self._cached_json[silent]
@@ -620,7 +621,7 @@ class Request(_SansIORequest):
         if e is not None:
             raise BadRequest(f"Failed to decode JSON object: {e}")
 
-        raise BadRequest(
+        raise UnsupportedMediaType(
             "Did not attempt to load JSON data because the request"
             " Content-Type was not 'application/json'."
         )


Test patch:
diff --git a/tests/test_wrappers.py b/tests/test_wrappers.py
index b769a388..e9930ad9 100644
--- a/tests/test_wrappers.py
+++ b/tests/test_wrappers.py
@@ -23,6 +23,7 @@ from werkzeug.datastructures import MultiDict
 from werkzeug.exceptions import BadRequest
 from werkzeug.exceptions import RequestedRangeNotSatisfiable
 from werkzeug.exceptions import SecurityError
+from werkzeug.exceptions import UnsupportedMediaType
 from werkzeug.http import COEP
 from werkzeug.http import COOP
 from werkzeug.http import generate_etag
@@ -1350,7 +1351,7 @@ class TestJSON:
         value = [1, 2, 3]
         request = wrappers.Request.from_values(json=value, content_type="text/plain")
 
-        with pytest.raises(BadRequest):
+        with pytest.raises(UnsupportedMediaType):
             request.get_json()
 
         assert request.get_json(silent=True) is None