Repository: graphite-project/graphite-web
Issue: [BUG] Remote Finder unable to parse UTF-8 characters

**Describe the bug**
When using the remote finder to look for metrics from multiple backends, any non ascii character results in an exception when trying to unpickle the response.

**To Reproduce**
Steps to reproduce the behavior:
* create a metric with with a non-ascii character in its name or path
> echo "test.random.død 123 `date +%s`" |nc ${carbon-host} 2003
* attempt to expand metrics finder in graphite web

**Expected behavior**
Be able to see all metrics present, including the ones with UTF-8 characters in their namespace

**Screenshots**
If applicable, add screenshots to help explain your problem.

**Environment (please complete the following information):**
 - OS flavor: Ubuntu (Xenial)
 - Graphite-web version: 1.1.7, master
 - Django/Python version: 2.2.16/3.5.2
 - Setup type: from sources

**Additional context**

This was tested on originally on v1.1.7, as well as on the master branch with the environment set for `UTF8_METRICS=true`, related to https://github.com/graphite-project/graphite-web/issues/2641

Trying to expand the metric tree in the UI or trying to render this metric results in the following error

```
2020-11-23,22:11:43.356 :: RemoteFinder[carbon-1:8080] Error decoding response from http://carbon-1:8080/render/?format=pickle&local=1&noCache=1&from=1606168063&until=1606169503&target=test.random.%2A&now=1606169503: 'ascii' codec can't decode byte 0xc3 in position 13: ordinal not in range(128)
Traceback (most recent call last):
  File "/opt/graphite/webapp/graphite/finders/remote.py", line 291, in deserialize
    return self._deserialize_stream(reader, result.getheader('content-type'))
  File "/opt/graphite/webapp/graphite/finders/remote.py", line 319, in _deserialize_stream
    data = unpickle.load(stream)
  File "/opt/graphite/webapp/graphite/util.py", line 219, in load
    return SafeUnpickler(file).load()
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc3 in position 13: ordinal not in range(128)
```

It may be the case that the some means to specify `encoding='bytes'` needs to be passed to:

https://github.com/graphite-project/graphite-web/blob/master/webapp/graphite/util.py#L194


Changed files:
webapp/graphite/util.py, webapp/tests/test_util.py

diff --git a/webapp/graphite/util.py b/webapp/graphite/util.py
index b5dc1e1d..1b620331 100644
--- a/webapp/graphite/util.py
+++ b/webapp/graphite/util.py
@@ -200,6 +200,9 @@ else:
       'graphite.intervals': set(['Interval', 'IntervalSet']),
     }
 
+    def __init__(self, file):
+        super().__init__(file, encoding='utf8')
+
     def find_class(self, module, name):
       if module not in self.PICKLE_SAFE:
         raise pickle.UnpicklingError('Attempting to unpickle unsafe module %s' % module)
diff --git a/webapp/tests/test_util.py b/webapp/tests/test_util.py
index ca52a78b..08504282 100644
--- a/webapp/tests/test_util.py
+++ b/webapp/tests/test_util.py
@@ -1,9 +1,11 @@
+# -*- encoding: utf-8 -*-
 import os
 import socket
 import pytz
 
 from datetime import datetime
 from mock import patch
+from io import BytesIO
 
 from .base import TestCase
 
@@ -119,3 +121,14 @@ class UtilTest(TestCase):
             test_logtime(False)
         self.assertEqual(log.info.call_count, 3)
         self.assertRegexpMatches(log.info.call_args[0][0], r'test :: failed in [-.e0-9]+s')
+
+
+class SafeUnpicklerTest(TestCase):
+
+    def test_load(self):
+        unpickler = util.unpickle()
+        p = b'\\x80\\x04\\x95\\r\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x8c\\ttest.d\\xc3\\xb8d\\x94.'
+        u = unpickler.load(BytesIO(p))
+        x = 'test.død'
+
+        self.assertEqual(u, x)