Repository: praw-dev/praw

Issue:
Praw send_removal_message throws 400 when title > 50 characters
The praw method send_removal_message throws an unhelpful 400 error when attempting to send a removal message with a title > 50 characters. Reddit limits these titles to 50 characters but that limit is not documented anywhere and the error message is not descriptive. Adding documentation would be helpful.

Other alternatives are changing the error thrown when this happens to be more descriptive or implicitly truncating removal reason message titles to 50 characters.

[Link to documentation](https://praw.readthedocs.io/en/latest/code_overview/other/submissionmoderation.html#praw.models.reddit.submission.SubmissionModeration.send_removal_message)

Discussion:

Patch:
diff --git a/.flake8 b/.flake8
new file mode 100644
index 00000000..e61de53c
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,2 @@
+[flake8]
+ignore = E203 E501 W503 W504
diff --git a/CHANGES.rst b/CHANGES.rst
index 1b0c18a6..910336ff 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -59,6 +59,11 @@ Unreleased
   ``RedditAPIException.items``, which returns a list.
 * :class:`.APIException` is an alias to :class:`.RedditAPIException`.
 
+**Changed**
+
+* ``prawcore.BadRequest`` should no longer be raised. Instead a more useful
+  :class:`.RedditAPIException` instance will be raised.
+
 **Deprecated**
 
 * :class:`.APIException` is deprecated and slotted for removal in PRAW 8.0.
diff --git a/praw/models/reddit/mixins/__init__.py b/praw/models/reddit/mixins/__init__.py
index d33f4604..99450720 100644
--- a/praw/models/reddit/mixins/__init__.py
+++ b/praw/models/reddit/mixins/__init__.py
@@ -188,7 +188,8 @@ class ThingModerationMixin:
         .. warning:: The object has to be removed before giving it a removal
             reason. Remove the object with :meth:`.remove`. Trying to add a
             removal reason without removing the object will result in
-            ``prawcore.exceptions.BadRequest`` being thrown.
+            :class:`.RedditAPIException` being thrown with an ``INVALID_ID``
+            error_type.
 
         Reddit adds human-readable information about the object to the message.
 
diff --git a/praw/models/reddit/redditor.py b/praw/models/reddit/redditor.py
index ce8ab354..3b100138 100644
--- a/praw/models/reddit/redditor.py
+++ b/praw/models/reddit/redditor.py
@@ -302,7 +302,7 @@ class Redditor(
         :returns: A ``list`` of :class:`~praw.models.Trophy` objects.
             Return ``[]`` if the redditor has no trophy.
 
-        Raise ``prawcore.exceptions.BadRequest`` if the redditor doesn't exist.
+        Raise :class:`.RedditAPIException` if the redditor doesn't exist.
 
         Usage:
 
diff --git a/praw/models/user.py b/praw/models/user.py
index b2887dcc..2675e46a 100644
--- a/praw/models/user.py
+++ b/praw/models/user.py
@@ -82,7 +82,7 @@ class User(PRAWBase):
             an instance of :class:`.Redditor` or a string can be given.
         :returns: A list of Redditors, or a Redditor if you are friends with
             the given Redditor. The Redditor also has friend attributes.
-        :raises: An instance of ``prawcore.exceptions.BadRequest`` if you are
+        :raises: An instance of :class:`.RedditAPIException` if you are
             not friends with the specified Redditor.
         """
         endpoint = (
diff --git a/praw/reddit.py b/praw/reddit.py
index 6d1a14a9..0722dfc2 100644
--- a/praw/reddit.py
+++ b/praw/reddit.py
@@ -16,11 +16,16 @@ from prawcore import (
     UntrustedAuthenticator,
     session,
 )
+from prawcore.exceptions import BadRequest
 
 from . import models
 from .config import Config
 from .const import API_PATH, USER_AGENT_FORMAT, __version__
-from .exceptions import ClientException, MissingRequiredAttributeException
+from .exceptions import (
+    ClientException,
+    MissingRequiredAttributeException,
+    RedditAPIException,
+)
 from .objector import Objector
 
 try:
@@ -648,9 +653,26 @@ class Reddit:
             (default: None).
 
         """
-        return self._core.request(
-            method, path, data=data, files=files, params=params
-        )
+        try:
+            return self._core.request(
+                method, path, data=data, files=files, params=params
+            )
+        except BadRequest as exception:
+            try:
+                data = exception.response.json()
+            except ValueError:
+                # TODO: Remove this exception after 2020-12-31 if no one has filed a bug against it.
+                raise Exception(
+                    "Unexpected BadRequest without json body. Please file a bug: https://github.com/praw-dev/praw/issues"
+                ) from exception
+            if "fields" in data:
+                assert len(data["fields"]) == 1
+                field = data["fields"][0]
+            else:
+                field = None
+            raise RedditAPIException(
+                [data["reason"], data["explanation"], field]
+            ) from exception
 
     def submission(  # pylint: disable=invalid-name,redefined-builtin
         self, id: Optional[str] = None, url: Optional[str] = None


Test patch:
diff --git a/tests/integration/cassettes/TestCommentModeration.test_send_removal_message__error.json b/tests/integration/cassettes/TestCommentModeration.test_send_removal_message__error.json
new file mode 100644
index 00000000..ee706299
--- /dev/null
+++ b/tests/integration/cassettes/TestCommentModeration.test_send_removal_message__error.json
@@ -0,0 +1,347 @@
+{
+  "http_interactions": [
+    {
+      "recorded_at": "2020-03-16T06:04:33",
+      "request": {
+        "body": {
+          "encoding": "utf-8",
+          "string": "grant_type=password&password=<PASSWORD>&username=<USERNAME>"
+        },
+        "headers": {
+          "Accept": [
+            "*/*"
+          ],
+          "Accept-Encoding": [
+            "identity"
+          ],
+          "Authorization": [
+            "Basic <BASIC_AUTH>"
+          ],
+          "Connection": [
+            "keep-alive"
+          ],
+          "Content-Length": [
+            "59"
+          ],
+          "Content-Type": [
+            "application/x-www-form-urlencoded"
+          ],
+          "User-Agent": [
+            "<USER_AGENT> PRAW/7.0.0.dev0 prawcore/1.0.1"
+          ]
+        },
+        "method": "POST",
+        "uri": "https://www.reddit.com/api/v1/access_token"
+      },
+      "response": {
+        "body": {
+          "encoding": "UTF-8",
+          "string": "{\"access_token\": \"<ACCESS_TOKEN>\", \"token_type\": \"bearer\", \"expires_in\": 3600, \"scope\": \"*\"}"
+        },
+        "headers": {
+          "Accept-Ranges": [
+            "bytes"
+          ],
+          "Connection": [
+            "keep-alive"
+          ],
+          "Content-Length": [
+            "113"
+          ],
+          "Content-Type": [
+            "application/json; charset=UTF-8"
+          ],
+          "Date": [
+            "Mon, 16 Mar 2020 06:04:36 GMT"
+          ],
+          "Server": [
+            "snooserv"
+          ],
+          "Set-Cookie": [
+            "edgebucket=vMiBSRA7UnxPlljzqc; Domain=reddit.com; Max-Age=63071999; Path=/;  secure"
+          ],
+          "Strict-Transport-Security": [
+            "max-age=15552000; includeSubDomains; preload"
+          ],
+          "Via": [
+            "1.1 varnish"
+          ],
+          "X-Cache": [
+            "MISS"
+          ],
+          "X-Cache-Hits": [
+            "0"
+          ],
+          "X-Moose": [
+            "majestic"
+          ],
+          "X-Served-By": [
+            "cache-lax8626-LAX"
+          ],
+          "X-Timer": [
+            "S1584338676.421791,VS0,VE361"
+          ],
+          "cache-control": [
+            "max-age=0, must-revalidate"
+          ],
+          "x-content-type-options": [
+            "nosniff"
+          ],
+          "x-frame-options": [
+            "SAMEORIGIN"
+          ],
+          "x-xss-protection": [
+            "1; mode=block"
+          ]
+        },
+        "status": {
+          "code": 200,
+          "message": "OK"
+        },
+        "url": "https://www.reddit.com/api/v1/access_token"
+      }
+    },
+    {
+      "recorded_at": "2020-03-16T06:04:33",
+      "request": {
+        "body": {
+          "encoding": "utf-8",
+          "string": "api_type=json&id=t1_fkmrn4a&spam=False"
+        },
+        "headers": {
+          "Accept": [
+            "*/*"
+          ],
+          "Accept-Encoding": [
+            "identity"
+          ],
+          "Authorization": [
+            "bearer <ACCESS_TOKEN>"
+          ],
+          "Connection": [
+            "keep-alive"
+          ],
+          "Content-Length": [
+            "38"
+          ],
+          "Content-Type": [
+            "application/x-www-form-urlencoded"
+          ],
+          "Cookie": [
+            "edgebucket=vMiBSRA7UnxPlljzqc"
+          ],
+          "User-Agent": [
+            "<USER_AGENT> PRAW/7.0.0.dev0 prawcore/1.0.1"
+          ]
+        },
+        "method": "POST",
+        "uri": "https://oauth.reddit.com/api/remove/?raw_json=1"
+      },
+      "response": {
+        "body": {
+          "encoding": "UTF-8",
+          "string": "{}"
+        },
+        "headers": {
+          "Accept-Ranges": [
+            "bytes"
+          ],
+          "Connection": [
+            "keep-alive"
+          ],
+          "Content-Length": [
+            "2"
+          ],
+          "Content-Type": [
+            "application/json; charset=UTF-8"
+          ],
+          "Date": [
+            "Mon, 16 Mar 2020 06:04:37 GMT"
+          ],
+          "Server": [
+            "snooserv"
+          ],
+          "Strict-Transport-Security": [
+            "max-age=15552000; includeSubDomains; preload"
+          ],
+          "Via": [
+            "1.1 varnish"
+          ],
+          "X-Cache": [
+            "MISS"
+          ],
+          "X-Cache-Hits": [
+            "0"
+          ],
+          "X-Moose": [
+            "majestic"
+          ],
+          "X-Served-By": [
+            "cache-lax8629-LAX"
+          ],
+          "X-Timer": [
+            "S1584338677.942068,VS0,VE345"
+          ],
+          "cache-control": [
+            "private, s-maxage=0, max-age=0, must-revalidate, no-store, max-age=0, must-revalidate"
+          ],
+          "expires": [
+            "-1"
+          ],
+          "set-cookie": [
+            "loid=00000000000004cixf.2.1284611485984.Z0FBQUFBQmVieGIxUHM1aDFrNUlSb0cxVHRuU2FGckhlNTIta3JRSkU5UTM3ZWMyY0k4SVlDVkdacmpHMWtHdFhqZ3lNREVxaTRGbEFjd2lQNEZJUmZoeGhTQ0tTcnBDY1l3QVVrOGdWUmJBUG5MNklWdzV0UU9ZNkduWGZFZFNYZVFJb0tiWjRlT2w; Domain=reddit.com; Max-Age=63071999; Path=/; expires=Wed, 16-Mar-2022 06:04:37 GMT; secure",
+            "redesign_optout=true; Domain=reddit.com; Max-Age=94607999; Path=/; expires=Thu, 16-Mar-2023 06:04:37 GMT; secure",
+            "session_tracker=DclkBLJCvWnYI0e8FA.0.1584338677003.Z0FBQUFBQmVieGIxZ2FfRFZJeXdHTU5jRWp0aGlNWXNTeHlYTXZwSGVuRHVtYWt5eVU3WTdYWHIyeFdmeTE4S2dZYXFzSk5idHFEVml3T0lXckQ3TmF4NDNjeG8yNVo5QzhrQ2dvc2tlZ0t1Qkl4NFJOckYyU3JvLUpVZ1hRS3BTb0VRMFdxNUJmVE0; Domain=reddit.com; Max-Age=7199; Path=/; expires=Mon, 16-Mar-2020 08:04:37 GMT; secure"
+          ],
+          "x-content-type-options": [
+            "nosniff"
+          ],
+          "x-frame-options": [
+            "SAMEORIGIN"
+          ],
+          "x-ratelimit-remaining": [
+            "598.0"
+          ],
+          "x-ratelimit-reset": [
+            "323"
+          ],
+          "x-ratelimit-used": [
+            "2"
+          ],
+          "x-ua-compatible": [
+            "IE=edge"
+          ],
+          "x-xss-protection": [
+            "1; mode=block"
+          ]
+        },
+        "status": {
+          "code": 200,
+          "message": "OK"
+        },
+        "url": "https://oauth.reddit.com/api/remove/?raw_json=1"
+      }
+    },
+    {
+      "recorded_at": "2020-03-16T06:04:33",
+      "request": {
+        "body": {
+          "encoding": "utf-8",
+          "string": "api_type=json&json=%7B%22item_id%22%3A+%5B%22t1_fkmrn4a%22%5D%2C+%22message%22%3A+%22message%22%2C+%22title%22%3A+%22aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa%22%2C+%22type%22%3A+%22public%22%7D"
+        },
+        "headers": {
+          "Accept": [
+            "*/*"
+          ],
+          "Accept-Encoding": [
+            "identity"
+          ],
+          "Authorization": [
+            "bearer <ACCESS_TOKEN>"
+          ],
+          "Connection": [
+            "keep-alive"
+          ],
+          "Content-Length": [
+            "204"
+          ],
+          "Content-Type": [
+            "application/x-www-form-urlencoded"
+          ],
+          "Cookie": [
+            "edgebucket=vMiBSRA7UnxPlljzqc; loid=00000000000004cixf.2.1284611485984.Z0FBQUFBQmVieGIxUHM1aDFrNUlSb0cxVHRuU2FGckhlNTIta3JRSkU5UTM3ZWMyY0k4SVlDVkdacmpHMWtHdFhqZ3lNREVxaTRGbEFjd2lQNEZJUmZoeGhTQ0tTcnBDY1l3QVVrOGdWUmJBUG5MNklWdzV0UU9ZNkduWGZFZFNYZVFJb0tiWjRlT2w; redesign_optout=true; session_tracker=DclkBLJCvWnYI0e8FA.0.1584338677003.Z0FBQUFBQmVieGIxZ2FfRFZJeXdHTU5jRWp0aGlNWXNTeHlYTXZwSGVuRHVtYWt5eVU3WTdYWHIyeFdmeTE4S2dZYXFzSk5idHFEVml3T0lXckQ3TmF4NDNjeG8yNVo5QzhrQ2dvc2tlZ0t1Qkl4NFJOckYyU3JvLUpVZ1hRS3BTb0VRMFdxNUJmVE0"
+          ],
+          "User-Agent": [
+            "<USER_AGENT> PRAW/7.0.0.dev0 prawcore/1.0.1"
+          ]
+        },
+        "method": "POST",
+        "uri": "https://oauth.reddit.com/api/v1/modactions/removal_comment_message?raw_json=1"
+      },
+      "response": {
+        "body": {
+          "encoding": "UTF-8",
+          "string": "{\"fields\": [\"title\"], \"explanation\": \"this is too long (max: 50)\", \"message\": \"Bad Request\", \"reason\": \"TOO_LONG\"}"
+        },
+        "headers": {
+          "Accept-Ranges": [
+            "bytes"
+          ],
+          "Connection": [
+            "keep-alive"
+          ],
+          "Content-Length": [
+            "114"
+          ],
+          "Content-Type": [
+            "application/json; charset=UTF-8"
+          ],
+          "Date": [
+            "Mon, 16 Mar 2020 06:04:37 GMT"
+          ],
+          "Server": [
+            "snooserv"
+          ],
+          "Strict-Transport-Security": [
+            "max-age=15552000; includeSubDomains; preload"
+          ],
+          "Via": [
+            "1.1 varnish"
+          ],
+          "X-Cache": [
+            "MISS"
+          ],
+          "X-Cache-Hits": [
+            "0"
+          ],
+          "X-Moose": [
+            "majestic"
+          ],
+          "X-Served-By": [
+            "cache-lax8629-LAX"
+          ],
+          "X-Timer": [
+            "S1584338677.309116,VS0,VE132"
+          ],
+          "cache-control": [
+            "private, s-maxage=0, max-age=0, must-revalidate, no-store, max-age=0, must-revalidate"
+          ],
+          "expires": [
+            "-1"
+          ],
+          "set-cookie": [
+            "session_tracker=DclkBLJCvWnYI0e8FA.0.1584338677365.Z0FBQUFBQmVieGIxTmF1dlJQWGZCUldaX1pZajBGdjFkV2IxTXNsM0dDUWxidm9CT2tjT3F6NWt4WDBlR0djNF9qczFGS2VVUEJVSTk4YkZoSUx3TmVVTGxlM1J3anR6X19jbGJOdU9fcW85Y0hVX2ZKcnZvMmtGRmk2bUNiS2lFV2VaZVNXcWVHQ0w; Domain=reddit.com; Max-Age=7199; Path=/; expires=Mon, 16-Mar-2020 08:04:37 GMT; secure",
+            "session_tracker=DclkBLJCvWnYI0e8FA.0.1584338677407.Z0FBQUFBQmVieGIxQlh3eE41dlNYTzhVU183REZiSkdXdkYwUWpybF9qQmdlcE5Hb3R0S2NmSDBIYzF0V2N4ei1SZTR4R3ItMFA2UmczSXhSOFhMMDdEQ0JBVkV3OTdsUEhrZEFnSjFkQ2NNZTNUa3h2bEpCVTloc3NMOHJhYzNZeUdvWUJsUUFhQmE; Domain=reddit.com; Max-Age=7199; Path=/; expires=Mon, 16-Mar-2020 08:04:37 GMT; secure"
+          ],
+          "x-content-type-options": [
+            "nosniff"
+          ],
+          "x-frame-options": [
+            "SAMEORIGIN"
+          ],
+          "x-ratelimit-remaining": [
+            "597.0"
+          ],
+          "x-ratelimit-reset": [
+            "323"
+          ],
+          "x-ratelimit-used": [
+            "3"
+          ],
+          "x-ua-compatible": [
+            "IE=edge"
+          ],
+          "x-xss-protection": [
+            "1; mode=block"
+          ]
+        },
+        "status": {
+          "code": 400,
+          "message": "Bad Request"
+        },
+        "url": "https://oauth.reddit.com/api/v1/modactions/removal_comment_message?raw_json=1"
+      }
+    }
+  ],
+  "recorded_with": "betamax/0.8.1"
+}
\ No newline at end of file
diff --git a/tests/integration/models/reddit/test_comment.py b/tests/integration/models/reddit/test_comment.py
index ff64d64a..913dd6f9 100644
--- a/tests/integration/models/reddit/test_comment.py
+++ b/tests/integration/models/reddit/test_comment.py
@@ -1,8 +1,7 @@
 import mock
 import pytest
-from prawcore import BadRequest
 
-from praw.exceptions import ClientException, PRAWException
+from praw.exceptions import ClientException, PRAWException, RedditAPIException
 from praw.models import Comment, Submission
 
 from ... import IntegrationTest
@@ -76,10 +75,10 @@ class TestComment(IntegrationTest):
     def test_gild__no_creddits(self):
         self.reddit.read_only = False
         with self.recorder.use_cassette("TestComment.test_gild__no_creddits"):
-            with pytest.raises(BadRequest) as excinfo:
+            with pytest.raises(RedditAPIException) as excinfo:
                 Comment(self.reddit, "d1616q2").gild()
-            reason = excinfo.value.response.json()["reason"]
-            assert "INSUFFICIENT_CREDDITS" == reason
+            exception = excinfo.value
+            assert "INSUFFICIENT_CREDDITS" == exception.error_type
 
     def test_invalid(self):
         with self.recorder.use_cassette("TestComment.test_invalid"):
@@ -361,6 +360,20 @@ class TestCommentModeration(IntegrationTest):
             assert res[1] is None
             assert res[2] is None
 
+    @mock.patch("time.sleep", return_value=None)
+    def test_send_removal_message__error(self, _):
+        self.reddit.read_only = False
+        with self.recorder.use_cassette(
+            "TestCommentModeration.test_send_removal_message__error"
+        ):
+            comment = self.reddit.comment("fkmrn4a")
+            comment.mod.remove()
+            with pytest.raises(RedditAPIException) as excinfo:
+                comment.mod.send_removal_message("message", "a" * 51)
+            exception = excinfo.value
+            assert "title" == exception.field
+            assert "TOO_LONG" == exception.error_type
+
     def test_undistinguish(self):
         self.reddit.read_only = False
         with self.recorder.use_cassette(
diff --git a/tests/integration/models/reddit/test_redditor.py b/tests/integration/models/reddit/test_redditor.py
index e823314c..46c9a994 100644
--- a/tests/integration/models/reddit/test_redditor.py
+++ b/tests/integration/models/reddit/test_redditor.py
@@ -1,8 +1,9 @@
 """Test praw.models.redditor."""
 import mock
 import pytest
-from prawcore import BadRequest, Forbidden
+from prawcore import Forbidden
 
+from praw.exceptions import RedditAPIException
 from praw.models import Comment, Submission
 
 from ... import IntegrationTest
@@ -29,9 +30,9 @@ class TestRedditor(IntegrationTest):
         with self.recorder.use_cassette(
             "TestRedditor.test_friend__with_note__no_gold"
         ):
-            with pytest.raises(BadRequest) as excinfo:
+            with pytest.raises(RedditAPIException) as excinfo:
                 self.reddit.redditor(self.FRIEND.lower()).friend(note="praw")
-            assert "GOLD_REQUIRED" == excinfo.value.response.json()["reason"]
+            assert "GOLD_REQUIRED" == excinfo.value.error_type
 
     @mock.patch("time.sleep", return_value=None)
     def test_friend_info(self, _):
@@ -53,10 +54,9 @@ class TestRedditor(IntegrationTest):
     def test_gild__no_creddits(self):
         self.reddit.read_only = False
         with self.recorder.use_cassette("TestRedditor.test_gild__no_creddits"):
-            with pytest.raises(BadRequest) as excinfo:
+            with pytest.raises(RedditAPIException) as excinfo:
                 self.reddit.redditor("subreddit_stats").gild()
-            reason = excinfo.value.response.json()["reason"]
-            assert "INSUFFICIENT_CREDDITS" == reason
+            assert "INSUFFICIENT_CREDDITS" == excinfo.value.error_type
 
     @mock.patch("time.sleep", return_value=None)
     def test_message(self, _):
@@ -126,10 +126,9 @@ class TestRedditor(IntegrationTest):
         with self.recorder.use_cassette(
             "TestRedditor.test_trophies__user_not_exist"
         ):
-            with pytest.raises(BadRequest) as excinfo:
+            with pytest.raises(RedditAPIException) as excinfo:
                 redditor.trophies()
-            response = excinfo.value.response
-            assert "USER_DOESNT_EXIST" == response.json()["reason"]
+            assert "USER_DOESNT_EXIST" == excinfo.value.error_type
 
     @mock.patch("time.sleep", return_value=None)
     def test_unblock(self, _):
diff --git a/tests/integration/models/reddit/test_submission.py b/tests/integration/models/reddit/test_submission.py
index 11065668..d1a63b65 100644
--- a/tests/integration/models/reddit/test_submission.py
+++ b/tests/integration/models/reddit/test_submission.py
@@ -1,8 +1,7 @@
 import mock
 import pytest
-from prawcore import BadRequest
 
-from praw.exceptions import APIException
+from praw.exceptions import RedditAPIException
 from praw.models import Comment, Submission
 
 from ... import IntegrationTest
@@ -62,9 +61,7 @@ class TestSubmission(IntegrationTest):
         self.reddit.validate_on_submit = True
         with self.recorder.use_cassette("TestSubmission.test_edit_invalid"):
             submission = Submission(self.reddit, "eippcc")
-            with pytest.raises(
-                (APIException, BadRequest)
-            ):  # waiting for prawcore fix
+            with pytest.raises(RedditAPIException):
                 submission.edit("rewtwert")
 
     def test_enable_inbox_replies(self):
@@ -80,10 +77,10 @@ class TestSubmission(IntegrationTest):
         with self.recorder.use_cassette(
             "TestSubmission.test_gild__no_creddits"
         ):
-            with pytest.raises(BadRequest) as excinfo:
+            with pytest.raises(RedditAPIException) as excinfo:
                 Submission(self.reddit, "4b1tfm").gild()
-            reason = excinfo.value.response.json()["reason"]
-            assert "INSUFFICIENT_CREDDITS" == reason
+            exception = excinfo.value
+            assert "INSUFFICIENT_CREDDITS" == exception.error_type
 
     def test_gilded(self):
         with self.recorder.use_cassette("TestSubmission.test_gilded"):
diff --git a/tests/integration/models/test_user.py b/tests/integration/models/test_user.py
index 7887a5aa..427e3541 100644
--- a/tests/integration/models/test_user.py
+++ b/tests/integration/models/test_user.py
@@ -1,8 +1,8 @@
 """Test praw.models.user."""
 import mock
 import pytest
-from prawcore.exceptions import BadRequest
 
+from praw.exceptions import RedditAPIException
 from praw.models import Multireddit, Redditor, Subreddit
 
 from .. import IntegrationTest
@@ -45,7 +45,7 @@ class TestUser(IntegrationTest):
     def test_friend_not_exist(self, _):
         self.reddit.read_only = False
         with self.recorder.use_cassette("TestUser.test_friend_not_exist"):
-            with pytest.raises(BadRequest):
+            with pytest.raises(RedditAPIException):
                 self.reddit.user.friends(user="fake__user_user_user")
 
     def test_karma(self):
diff --git a/tests/unit/test_reddit.py b/tests/unit/test_reddit.py
index 177214ea..11fb1c9d 100644
--- a/tests/unit/test_reddit.py
+++ b/tests/unit/test_reddit.py
@@ -4,6 +4,7 @@ import types
 import mock
 import pytest
 from prawcore import Requestor
+from prawcore.exceptions import BadRequest
 
 from praw import Reddit, __version__
 from praw.config import Config
@@ -162,6 +163,23 @@ class TestReddit(UnitTest):
             Reddit("bad_site_name")
         assert "praw.readthedocs.io" in excinfo.value.message
 
+    @mock.patch("prawcore.sessions.Session")
+    def test_request__badrequest_with_no_json_body(self, mock_session):
+        response = mock.Mock(status_code=400)
+        response.json.side_effect = ValueError
+        mock_session.return_value.request = mock.Mock(
+            side_effect=BadRequest(response=response)
+        )
+
+        reddit = Reddit(
+            client_id="dummy", client_secret="dummy", user_agent="dummy"
+        )
+        with pytest.raises(Exception) as excinfo:
+            reddit.request("POST", "/")
+        assert str(excinfo.value).startswith(
+            "Unexpected BadRequest without json body."
+        )
+
     def test_submission(self):
         assert self.reddit.submission("2gmzqe").id == "2gmzqe"