Repository: Kinto/kinto

Issue:
Add details attribute to 404 errors
Something like `{"details": {"id": "abc", "resource_name": collection}}`

It should be feasible to just raise custom error responses now that we have #748 



Discussion:
need more information to work on this bug.

We must improve the way we raise 404 errors
Here https://github.com/Kinto/kinto/blob/2f0ba5e/kinto/views/__init__.py#L24-L32
And https://github.com/Kinto/kinto/blob/2f0ba5e/kinto/core/resource/__init__.py#L664-L679
in order to provide some additional details in the 404 response JSON body.

See also what was done in #748 

Don't hesitate to ask questions if something does not seem feasible or clear!

@lavish205 Are you still working on this issue? If not, can I please work on this?

@mansimarkaur No. You can take this issue. 


Resolving patch:
diff --git a/kinto/core/resource/__init__.py b/kinto/core/resource/__init__.py
--- a/kinto/core/resource/__init__.py
+++ b/kinto/core/resource/__init__.py
@@ -680,8 +680,12 @@ def _get_record_or_404(self, record_id):
         try:
             return self.model.get_record(record_id)
         except storage_exceptions.RecordNotFoundError:
-            response = http_error(HTTPNotFound(),
-                                  errno=ERRORS.INVALID_RESOURCE_ID)
+            details = {
+                "id": record_id,
+                "resource_name": self.request.current_resource_name
+            }
+            response = http_error(HTTPNotFound(), errno=ERRORS.INVALID_RESOURCE_ID,
+                                  details=details)
             raise response
 
     def _add_timestamp_header(self, response, timestamp=None):
diff --git a/kinto/views/__init__.py b/kinto/views/__init__.py
--- a/kinto/views/__init__.py
+++ b/kinto/views/__init__.py
@@ -2,7 +2,8 @@
 import string
 
 from kinto.core.storage import generators, exceptions
-from pyramid import httpexceptions
+from pyramid.httpexceptions import HTTPNotFound
+from kinto.core.errors import http_error, ERRORS
 
 
 class NameGenerator(generators.Generator):
@@ -29,4 +30,9 @@ def object_exists_or_404(request, collection_id, object_id, parent_id=''):
                            object_id=object_id)
     except exceptions.RecordNotFoundError:
         # XXX: We gave up putting details about parent id here (See #53).
-        raise httpexceptions.HTTPNotFound()
+        details = {
+            "id": object_id,
+            "resource_name": collection_id
+        }
+        response = http_error(HTTPNotFound(), errno=ERRORS.MISSING_RESOURCE, details=details)
+        raise response


Test patch:
diff --git a/kinto/core/testing.py b/kinto/core/testing.py
--- a/kinto/core/testing.py
+++ b/kinto/core/testing.py
@@ -79,7 +79,6 @@ def assertFormattedError(self, response, code, errno, error,
         self.assertEqual(response.json['code'], code)
         self.assertEqual(response.json['errno'], errno)
         self.assertEqual(response.json['error'], error)
-
         if message is not None:
             self.assertIn(message, response.json['message'])
         else:  # pragma: no cover
diff --git a/tests/core/test_views_errors.py b/tests/core/test_views_errors.py
--- a/tests/core/test_views_errors.py
+++ b/tests/core/test_views_errors.py
@@ -44,15 +44,15 @@ def test_404_is_valid_formatted_error(self):
         self.assertFormattedError(response, 404, ERRORS.MISSING_RESOURCE, "Not Found",
                                   "The resource you are looking for could not be found.")
 
-    def test_404_can_be_overridded(self):
+    def test_404_can_be_overridden(self):
         custom_404 = http_error(httpexceptions.HTTPNotFound(),
                                 errno=ERRORS.MISSING_RESOURCE,
                                 message="Customized.")
         with mock.patch('tests.core.testapp.views.Mushroom._extract_filters',
                         side_effect=custom_404):
             response = self.app.get(self.sample_url, headers=self.headers, status=404)
-        self.assertFormattedError(
-            response, 404, ERRORS.MISSING_RESOURCE, "Not Found", "Customized.")
+        self.assertFormattedError(response, 404, ERRORS.MISSING_RESOURCE,
+                                  "Not Found", "Customized.")
 
     def test_401_is_valid_formatted_error(self):
         response = self.app.get(self.sample_url, status=401)
diff --git a/tests/test_views_buckets.py b/tests/test_views_buckets.py
--- a/tests/test_views_buckets.py
+++ b/tests/test_views_buckets.py
@@ -220,6 +220,11 @@ def test_buckets_can_be_deleted_in_bulk(self):
         self.app.get('/buckets/2', headers=self.headers, status=404)
         self.app.get('/buckets/3', headers=self.headers, status=404)
 
+    def test_unknown_bucket_raises_404(self):
+        resp = self.app.get('/buckets/2', headers=self.headers, status=404)
+        self.assertEqual(resp.json['details']['id'], '2')
+        self.assertEqual(resp.json['details']['resource_name'], 'bucket')
+
     def test_buckets_can_be_deleted(self):
         self.app.get(self.bucket_url, headers=self.headers,
                      status=404)
@@ -228,6 +233,7 @@ def test_every_collections_are_deleted_too(self):
         self.app.put_json(self.bucket_url, MINIMALIST_BUCKET,
                           headers=self.headers)
         self.app.get(self.collection_url, headers=self.headers, status=404)
+
         # Verify tombstones
         resp = self.app.get('%s/collections?_since=0' % self.bucket_url,
                             headers=self.headers)
diff --git a/tests/test_views_collections.py b/tests/test_views_collections.py
--- a/tests/test_views_collections.py
+++ b/tests/test_views_collections.py
@@ -47,6 +47,12 @@ def test_unknown_bucket_raises_403(self):
         other_bucket = self.collections_url.replace('beers', 'sodas')
         self.app.get(other_bucket, headers=self.headers, status=403)
 
+    def test_unknown_collection_raises_404(self):
+        other_collection = self.collection_url.replace('barley', 'pills')
+        resp = self.app.get(other_collection, headers=self.headers, status=404)
+        self.assertEqual(resp.json['details']['id'], 'pills')
+        self.assertEqual(resp.json['details']['resource_name'], 'collection')
+
     def test_collections_are_isolated_by_bucket(self):
         other_bucket = self.collection_url.replace('beers', 'sodas')
         self.app.put_json('/buckets/sodas',
@@ -117,8 +123,7 @@ def setUp(self):
         self.app.delete(self.collection_url, headers=self.headers)
 
     def test_collections_can_be_deleted(self):
-        self.app.get(self.collection_url, headers=self.headers,
-                     status=404)
+        self.app.get(self.collection_url, headers=self.headers, status=404)
 
     def test_collections_can_be_deleted_in_bulk(self):
         alice_headers = get_user_headers('alice')
diff --git a/tests/test_views_groups.py b/tests/test_views_groups.py
--- a/tests/test_views_groups.py
+++ b/tests/test_views_groups.py
@@ -145,6 +145,12 @@ def test_groups_can_be_deleted(self):
         self.app.get(self.group_url, headers=self.headers,
                      status=404)
 
+    def test_unknown_group_raises_404(self):
+        other_group = self.group_url.replace('moderators', 'blah')
+        resp = self.app.get(other_group, headers=self.headers, status=404)
+        self.assertEqual(resp.json['details']['id'], 'blah')
+        self.assertEqual(resp.json['details']['resource_name'], 'group')
+
     def test_group_is_removed_from_users_principals_on_group_deletion(self):
         self.app.put_json(self.group_url, MINIMALIST_GROUP,
                           headers=self.headers, status=201)
diff --git a/tests/test_views_records.py b/tests/test_views_records.py
--- a/tests/test_views_records.py
+++ b/tests/test_views_records.py
@@ -38,7 +38,15 @@ def test_unknown_bucket_raises_403(self):
 
     def test_unknown_collection_raises_404(self):
         other_collection = self.collection_url.replace('barley', 'pills')
-        self.app.get(other_collection, headers=self.headers, status=404)
+        resp = self.app.get(other_collection, headers=self.headers, status=404)
+        self.assertEqual(resp.json['details']['id'], 'pills')
+        self.assertEqual(resp.json['details']['resource_name'], 'collection')
+
+    def test_unknown_record_raises_404(self):
+        other_record = self.record_url.replace(self.record['id'], self.record['id']+'blah')
+        response = self.app.get(other_record, headers=self.headers, status=404)
+        self.assertEqual(response.json['details']['id'], self.record['id']+'blah')
+        self.assertEqual(response.json['details']['resource_name'], 'record')
 
     def test_unknown_collection_does_not_query_timestamp(self):
         other_collection = self.collection_url.replace('barley', 'pills')