Repository: praw-dev/praw

Issue:
UnicodeEncodeError is raised if reddit returns localized error message
## Issue Description

Context: [[PRAW] UnicodeEncodeError when submitting non-unicode text : redditdev](https://www.reddit.com/r/redditdev/comments/6xf600/praw_unicodeencodeerror_when_submitting/)

Reddit may return localized error messages depends on the user's preference settings. Since
localized error messages may contain non-ascii characters (and underlying requests library
converts the errror message to unicode type), running this code in Python2 may raise UnicodeEncodeError:

https://github.com/praw-dev/praw/blob/efbe90f8c01a8afcda1fa09a59d1d89ed0da0f6b/praw/exceptions.py#L25

Here is an example of the localized message:

```
  File "/usr/local/lib/python2.7/site-packages/praw/exceptions.py", line 25, in __init__
    error_str = '{}: \'{}\''.format(error_type, message)
UnicodeEncodeError: 'ascii' codec can't encode characters in position 0-10: ordinal not in range(128)
Uncaught exception. Entering post mortem debugging
Running 'cont' or 'step' will restart the program
> /usr/local/lib/python2.7/site-packages/praw/exceptions.py(25)__init__()
-> error_str = '{}: \'{}\''.format(error_type, message)
(Pdb) p error_type
u'RATELIMIT'
(Pdb) print message
実行回数が多すぎます。9分経ってから再試行してください。
```

I think this issue is only affect to Python2 users because Python3's str type is unicode string. 

## System Information

    PRAW Version: 5.0.0
    Python Version: Python 2.7.13
    Operating System: OS X El Capitan 10.11.6


Discussion:
Well that's certainly not something I would have thought to test for. @nmtake do you think you could try fixing it? Likely requires decoding the message.
Hopefully, I'll send a patch in a day or two.

Resolving patch:
diff --git a/praw/exceptions.py b/praw/exceptions.py
--- a/praw/exceptions.py
+++ b/praw/exceptions.py
@@ -21,10 +21,17 @@ def __init__(self, error_type, message, field):
         :param message: The associated message for the error.
         :param field: The input field associated with the error if available.
 
+        .. note: Calling `str()` on the instance returns `unicode_escape`d
+            ASCII string because the message may be localized and may contain
+            UNICODE characters. If you want a non-escaped message, access
+            the `message` atribute on the instance.
+
         """
-        error_str = '{}: \'{}\''.format(error_type, message)
+        error_str = u'{}: \'{}\''.format(error_type, message)
         if field:
-            error_str += ' on field \'{}\''.format(field)
+            error_str += u' on field \'{}\''.format(field)
+        error_str = error_str.encode('unicode_escape').decode('ascii')
+
         super(APIException, self).__init__(error_str)
         self.error_type = error_type
         self.message = message


Test patch:
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
@@ -1,3 +1,4 @@
+# coding: utf-8
 from praw.exceptions import APIException, ClientException, PRAWException
 
 
@@ -20,6 +21,14 @@ def test_str(self):
         assert str(exception) == ('BAD_SOMETHING: \'invalid something\' on '
                                   'field \'some_field\'')
 
+    def test_str_for_localized_error_string(self):
+        exception = APIException(
+                'RATELIMIT', u'実行回数が多すぎます', u'フィールド')
+        assert str(exception) == (
+                'RATELIMIT: \'\\u5b9f\\u884c\\u56de\\u6570\\u304c\\u591a'
+                '\\u3059\\u304e\\u307e\\u3059\' on field '
+                '\'\\u30d5\\u30a3\\u30fc\\u30eb\\u30c9\'')
+
 
 class TestClientException(object):
     def test_inheritance(self):