Repository: encode/django-rest-framework

Issue:
Try/catch in HyperlinkedRelatedField too broad.
In https://github.com/encode/django-rest-framework/blob/master/rest_framework/relations.py#L347-L350

Catching and silently discarding the `TypeError` and `ValueError` seems overly broad, is it necessary?

I'm asking, after spending time debugging a derived `HyperlinkedRelatedField` where, it turns out, I was using the wrong method signature, which was throwing `TypeError` exceptions that DRF was thing reporting as 'object not found'.

Turns out there was an error in the documentation which I've patched in https://github.com/encode/django-rest-framework/pull/6024

Maybe correcting the docs is enough to prevent similar mistakes in the future, but catching `TypeError` could mask all sorts of errors, is it too broad?


Discussion:
> catching TypeError could mask all sorts of errors, is it too broad?

I'm not sure it's resolvable. Making a queryset lookup with a string that *doesn't* convert to the expected type will raise one of those two error classes.

This will happen eg. in the case of API input of a hyperlinked URL using "/organizations/abc" for a model that expects "/organizations/123" .

We do need to catch those cases and represent them as "does_not_exist" errors against the user input.

Resolving patch:
diff --git a/rest_framework/relations.py b/rest_framework/relations.py
--- a/rest_framework/relations.py
+++ b/rest_framework/relations.py
@@ -1,6 +1,7 @@
 # coding: utf-8
 from __future__ import unicode_literals
 
+import sys
 from collections import OrderedDict
 
 from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
@@ -31,6 +32,20 @@ def method_overridden(method_name, klass, instance):
     return default_method is not getattr(instance, method_name).__func__
 
 
+class ObjectValueError(ValueError):
+    """
+    Raised when `queryset.get()` failed due to an underlying `ValueError`.
+    Wrapping prevents calling code conflating this with unrelated errors.
+    """
+
+
+class ObjectTypeError(TypeError):
+    """
+    Raised when `queryset.get()` failed due to an underlying `TypeError`.
+    Wrapping prevents calling code conflating this with unrelated errors.
+    """
+
+
 class Hyperlink(six.text_type):
     """
     A string like object that additionally has an associated name.
@@ -296,7 +311,16 @@ def get_object(self, view_name, view_args, view_kwargs):
         """
         lookup_value = view_kwargs[self.lookup_url_kwarg]
         lookup_kwargs = {self.lookup_field: lookup_value}
-        return self.get_queryset().get(**lookup_kwargs)
+        queryset = self.get_queryset()
+
+        try:
+            return queryset.get(**lookup_kwargs)
+        except ValueError:
+            exc = ObjectValueError(str(sys.exc_info()[1]))
+            six.reraise(type(exc), exc, sys.exc_info()[2])
+        except TypeError:
+            exc = ObjectTypeError(str(sys.exc_info()[1]))
+            six.reraise(type(exc), exc, sys.exc_info()[2])
 
     def get_url(self, obj, view_name, request, format):
         """
@@ -346,7 +370,7 @@ def to_internal_value(self, data):
 
         try:
             return self.get_object(match.view_name, match.args, match.kwargs)
-        except (ObjectDoesNotExist, TypeError, ValueError):
+        except (ObjectDoesNotExist, ObjectValueError, ObjectTypeError):
             self.fail('does_not_exist')
 
     def to_representation(self, value):


Test patch:
diff --git a/tests/test_relations.py b/tests/test_relations.py
--- a/tests/test_relations.py
+++ b/tests/test_relations.py
@@ -197,6 +197,36 @@ def test_hyperlinked_related_lookup_does_not_exist(self):
         msg = excinfo.value.detail[0]
         assert msg == 'Invalid hyperlink - Object does not exist.'
 
+    def test_hyperlinked_related_internal_type_error(self):
+        class Field(serializers.HyperlinkedRelatedField):
+            def get_object(self, incorrect, signature):
+                raise NotImplementedError()
+
+        field = Field(view_name='example', queryset=self.queryset)
+        with pytest.raises(TypeError):
+            field.to_internal_value('http://example.org/example/doesnotexist/')
+
+    def hyperlinked_related_queryset_error(self, exc_type):
+        class QuerySet:
+            def get(self, *args, **kwargs):
+                raise exc_type
+
+        field = serializers.HyperlinkedRelatedField(
+            view_name='example',
+            lookup_field='name',
+            queryset=QuerySet(),
+        )
+        with pytest.raises(serializers.ValidationError) as excinfo:
+            field.to_internal_value('http://example.org/example/doesnotexist/')
+        msg = excinfo.value.detail[0]
+        assert msg == 'Invalid hyperlink - Object does not exist.'
+
+    def test_hyperlinked_related_queryset_type_error(self):
+        self.hyperlinked_related_queryset_error(TypeError)
+
+    def test_hyperlinked_related_queryset_value_error(self):
+        self.hyperlinked_related_queryset_error(ValueError)
+
 
 class TestHyperlinkedIdentityField(APISimpleTestCase):
     def setUp(self):