Repository: praw-dev/praw

Issue:
KeyError from corrupted image uploads
On behalf of u/Smoking-Snake (https://www.reddit.com/r/redditdev/comments/g7s1yx/praw_keyerror_redirect_when_trying_to_upload/)

**Describe the bug**
<!-- A clear and concise description of what the bug is. -->Invalid media uploads return an empty payload but a type of failed. PRAW expects a payload with a redirect, and since there is no payload, a KeyError is thrown.

**Expected behavior**
<!-- A clear and concise description of what you expected to happen. -->
An exception that the media is valid should be raised.

**Code/Logs**
<!-- include your code, without the Reddit() initialization, so as to not leak private credentials. -->

**System Info**
 - OS: ?
 - Python: ?
 - PRAW Version: 7.0.0



Discussion:

Resolving patch:
diff --git a/praw/exceptions.py b/praw/exceptions.py
--- a/praw/exceptions.py
+++ b/praw/exceptions.py
@@ -244,11 +244,47 @@ def __init__(self, maximum_size: int, actual: int):
 class WebSocketException(ClientException):
     """Indicate exceptions caused by use of WebSockets."""
 
-    def __init__(self, message: str, exception: Exception):
+    @property
+    def original_exception(self) -> Exception:
+        """Access the original_exception attribute (now deprecated)."""
+        warn(
+            "Accessing the attribute original_exception is deprecated. Please"
+            " rewrite your code in such a way that this attribute does not"
+            " need to be used. It will be removed in PRAW 8.0.",
+            category=DeprecationWarning,
+            stacklevel=2,
+        )
+        return self._original_exception
+
+    @original_exception.setter
+    def original_exception(self, value: Exception):
+        self._original_exception = value
+
+    @original_exception.deleter
+    def original_exception(self):
+        del self._original_exception
+
+    def __init__(self, message: str, exception: Optional[Exception]):
         """Initialize a WebSocketException.
 
         :param message: The exception message.
         :param exception: The exception thrown by the websocket library.
+
+            .. note:: This parameter is deprecated. It will be removed in PRAW
+                8.0.
         """
         super().__init__(message)
-        self.original_exception = exception
+        self._original_exception = exception
+
+
+class MediaPostFailed(WebSocketException):
+    """Indicate exceptions where media uploads failed.."""
+
+    def __init__(self):
+        """Instantiate MediaPostFailed."""
+        super().__init__(
+            "The attempted media upload action has failed. Possible causes"
+            " include the corruption of media files. Check that the media "
+            "file can be opened on your local machine.",
+            None,
+        )
diff --git a/praw/models/reddit/subreddit.py b/praw/models/reddit/subreddit.py
--- a/praw/models/reddit/subreddit.py
+++ b/praw/models/reddit/subreddit.py
@@ -18,6 +18,7 @@
 from ...exceptions import (
     ClientException,
     InvalidFlairTemplateID,
+    MediaPostFailed,
     RedditAPIException,
     TooLargeMediaException,
     WebSocketException,
@@ -612,6 +613,8 @@ def _submit_media(self, data, timeout, without_websockets):
                 "the original exception.",
                 ws_exception,
             )
+        if ws_update.get("type") == "failed":
+            raise MediaPostFailed
         url = ws_update["payload"]["redirect"]
         return self._reddit.submission(url=url)
 


Test patch:
diff --git a/tests/unit/models/reddit/test_subreddit.py b/tests/unit/models/reddit/test_subreddit.py
--- a/tests/unit/models/reddit/test_subreddit.py
+++ b/tests/unit/models/reddit/test_subreddit.py
@@ -1,7 +1,9 @@
+import json
 import pickle
-
 import pytest
+from unittest import mock
 
+from praw.exceptions import MediaPostFailed
 from praw.models import Subreddit, WikiPage
 from praw.models.reddit.subreddit import SubredditFlairTemplates
 
@@ -61,6 +63,21 @@ def test_hash(self):
         assert hash(subreddit2) != hash(subreddit3)
         assert hash(subreddit1) != hash(subreddit3)
 
+    @mock.patch(
+        "praw.Reddit.post",
+        return_value={"json": {"data": {"websocket_url": ""}}},
+    )
+    @mock.patch("praw.models.Subreddit._upload_media", return_value="")
+    @mock.patch("websocket.create_connection")
+    def test_invalid_media(
+        self, connection_mock, _mock_upload_media, _mock_post
+    ):
+        connection_mock().recv.return_value = json.dumps(
+            {"payload": {}, "type": "failed"}
+        )
+        with pytest.raises(MediaPostFailed):
+            self.reddit.subreddit("test").submit_image("Test", "dummy path")
+
     def test_pickle(self):
         subreddit = Subreddit(
             self.reddit, _data={"display_name": "name", "id": "dummy"}
diff --git a/tests/unit/test_deprecations.py b/tests/unit/test_deprecations.py
--- a/tests/unit/test_deprecations.py
+++ b/tests/unit/test_deprecations.py
@@ -2,7 +2,7 @@
 
 import pytest
 
-from praw.exceptions import APIException
+from praw.exceptions import APIException, WebSocketException
 
 from . import UnitTest
 
@@ -39,3 +39,14 @@ def test_subreddit_rules_call(self):
             "/other/subredditrules.html#praw.models.reddit.rules."
             "SubredditRules.__call__)."
         )
+
+    def test_web_socket_exception_attribute(self):
+        exc = WebSocketException("Test", Exception("Test"))
+        with pytest.raises(DeprecationWarning) as excinfo:
+            _ = exc.original_exception
+        assert (
+            excinfo.value.args[0]
+            == "Accessing the attribute original_exception is deprecated."
+            " Please rewrite your code in such a way that this attribute does "
+            "not need to be used. It will be removed in PRAW 8.0."
+        )
diff --git a/tests/unit/test_exceptions.py b/tests/unit/test_exceptions.py
--- a/tests/unit/test_exceptions.py
+++ b/tests/unit/test_exceptions.py
@@ -8,6 +8,7 @@
     InvalidFlairTemplateID,
     InvalidImplicitAuth,
     InvalidURL,
+    MediaPostFailed,
     MissingRequiredAttributeException,
     PRAWException,
     RedditAPIException,
@@ -178,10 +179,29 @@ def test_str(self):
             str(WebSocketException("error message", None)) == "error message"
         )
 
+    @pytest.mark.filterwarnings("ignore", category=DeprecationWarning)
     def test_exception_attr(self):
-        assert WebSocketException(None, None).original_exception is None
+        exc = WebSocketException(None, None)
+        assert exc.original_exception is None
         assert isinstance(WebSocketException(None, Exception()), Exception)
         assert (
             str(WebSocketException(None, Exception("test")).original_exception)
             == "test"
         )
+        exc.original_exception = Exception()
+        assert isinstance(exc.original_exception, Exception)
+        del exc.original_exception
+        assert "_original_exception" not in vars(exc)
+
+
+class TestMediaPostFailed:
+    def test_inheritance(self):
+        assert issubclass(MediaPostFailed, WebSocketException)
+
+    def test_message(self):
+        assert (
+            str(MediaPostFailed())
+            == "The attempted media upload action has failed. Possible causes"
+            " include the corruption of media files. Check that the media "
+            "file can be opened on your local machine."
+        )