Repository: encode/django-rest-framework

Issue:
HyperlinkedRelatedField exception on string with %20 instead of space
Use serializers.HyperlinkedRelatedField where defined lookup_field link to the field string causes an exception 'Invalid hyperlink - Object does not exist.' when the value of that field contains characters that are encoded in the url. eg space -> %20

Missing unquote when try get  the value from the url



Discussion:
Looks legit as https://github.com/tomchristie/django-rest-framework/blob/master/rest_framework/generics.py#L96 doesn't unquote the data from url.
I'm rather surprised - I'd have fully expected that percent encoding is handled for you by Django's URL routing. A failing test case here might be a good first pass.
@xordoquy what you linked seems unrelated, as the error mentioned here is most likely raised from:
https://github.com/encode/django-rest-framework/blob/4dd71d68d2227cced7f1215a664343b20da8c804/rest_framework/relations.py#L345
The `view` with custom lookup field doesn't seem to cause any trouble by itself.

So why does `resolve` it work in view, but not in related field?
`'%20'` in `path` gets unquoted quite early on, already in WSGI server (or django test client). This is seems not exactly well defined part of WSGI, so some servers may not unquote this, but django builtin server does, and so is `uwsgi` and few others. Anyway, this is just explanation why it does seem to work when working with `path` from taken `request` object.

In related field we deal with user provided `data`, which is not parsed by any magical WSGI implementation. So I think we should call `uri_to_iri` (just like django test server) on `urlparse.urlparse(data).path`.
@tomchristie @rooterkyberian @xordoquy it seems to be broken again since the Django's `uri_to_iri()` function has changed in Django2.0+

See what they've changed: https://github.com/django/django/commit/03281d8fe7a32f580a85235659d4fbb143eeb867#diff-d3fe562051dcbab80f60f067fa6a5838R180

I.e. now, `%20` stays in the URL as opposed to how it worked before. Therefore, HyperlinkedRelatedField doesn't work with hyperlinks containing spaces`%20` and returns "Object does not exist". 

And the unit test in DRF passes because here https://github.com/encode/django-rest-framework/pull/5179 it doesn't cover the %20 case. Thus, HyperlinkedRelatedField appear to be not fully compatible with Django 2.0+ 
Thanks @hexvolt. 
@rpkilby if you don't mind, can you include this on 3.11.x milestone? the milestone tag seems wrong

Resolving patch:
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -344,7 +344,7 @@ def to_internal_value(self, data):
             if data.startswith(prefix):
                 data = '/' + data[len(prefix):]
 
-        data = uri_to_iri(data)
+        data = uri_to_iri(parse.unquote(data))
 
         try:
             match = resolve(data)


Test patch:
diff --git a/tests/test_relations.py b/tests/test_relations.py
--- a/tests/test_relations.py
+++ b/tests/test_relations.py
@@ -153,6 +153,7 @@ def setUp(self):
         self.queryset = MockQueryset([
             MockObject(pk=1, name='foobar'),
             MockObject(pk=2, name='bazABCqux'),
+            MockObject(pk=2, name='bazABC qux'),
         ])
         self.field = serializers.HyperlinkedRelatedField(
             view_name='example',
@@ -191,6 +192,10 @@ def test_hyperlinked_related_lookup_url_encoded_exists(self):
         instance = self.field.to_internal_value('http://example.org/example/baz%41%42%43qux/')
         assert instance is self.queryset.items[1]
 
+    def test_hyperlinked_related_lookup_url_space_encoded_exists(self):
+        instance = self.field.to_internal_value('http://example.org/example/bazABC%20qux/')
+        assert instance is self.queryset.items[2]
+
     def test_hyperlinked_related_lookup_does_not_exist(self):
         with pytest.raises(serializers.ValidationError) as excinfo:
             self.field.to_internal_value('http://example.org/example/doesnotexist/')