Repository: Kinto/kinto (Python)

Issue:
Cannot fetch records list with _since=null anymore
Since https://github.com/Kinto/kinto/pull/2525 the server returns 400 when a plural endpoint is called with `_since=null`. This is not intended and breaks the Admin UI in certain situations.


Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 18cb79d0b..278ce0302 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,6 +9,7 @@ This document describes changes between each past release.
 **Bug fixes**
 
 - Do not break storage implementations (ie. kinto-redis) that rely on ``json`` class attribute (removed in v14.0.0)
+- Do not return 400 for ``?_since=null`` (fixes #2595)
 
 
 14.0.0 (2020-08-13)
@@ -18,6 +19,10 @@ This document describes changes between each past release.
 
 - Drop the ``strict_json`` option, and use ``ultrajson`` everywhere
 
+**Bug fixes**
+
+- Return 400 if ``_since`` is not ``int`` or ``str`` (fixes #2495)
+
 **Internal Changes**
 
 - Upgrade kinto-admin to v1.26.2
diff --git a/docs/api/1.x/filtering.rst b/docs/api/1.x/filtering.rst
index b220c0d68..61f921d0c 100644
--- a/docs/api/1.x/filtering.rst
+++ b/docs/api/1.x/filtering.rst
@@ -189,9 +189,11 @@ collection was not changed, a |status-304| response is returned.
 
 .. note::
 
-    ``_since`` and ``_before`` also accept a value between quotes (``"``) as
-    it would be returned in the ``ETag`` response header
-    (see :ref:`response timestamps <server-timestamps>`).
+    * ``_since`` and ``_before`` also accept a value between quotes (``"``) as
+      it would be returned in the ``ETag`` response header
+      (see :ref:`response timestamps <server-timestamps>`).
+    * ``_since`` and ``_before`` also accept a ``null`` value, which is equivalent
+      to the parameter not being specified.
 
 **Request**:
 
diff --git a/kinto/core/resource/__init__.py b/kinto/core/resource/__init__.py
index d8e84904a..f98dd3a16 100644
--- a/kinto/core/resource/__init__.py
+++ b/kinto/core/resource/__init__.py
@@ -1095,7 +1095,7 @@ def _extract_filters(self):
                         send_alert(self.request, message, url)
                     operator = COMPARISON.LT
 
-                if value == "" or not isinstance(value, (int, str)):
+                if value == "" or not isinstance(value, (int, str, type(None))):
                     raise_invalid(self.request, **error_details)
 
                 filters.append(Filter(self.model.modified_field, value, operator))


Test patch:
diff --git a/tests/core/resource/test_filter.py b/tests/core/resource/test_filter.py
index a2f986320..ba8603c90 100644
--- a/tests/core/resource/test_filter.py
+++ b/tests/core/resource/test_filter.py
@@ -83,6 +83,11 @@ def test_filter_raises_error_if_last_modified_value_is_empty(self):
         self.validated["querystring"] = {"lt_last_modified": ""}
         self.assertRaises(httpexceptions.HTTPBadRequest, self.resource.plural_get)
 
+    def test_filter_works_with_since_none(self):
+        self.validated["querystring"] = {"_since": None}
+        result = self.resource.plural_get()
+        self.assertEqual(len(result["data"]), 8)
+
     def test_filter_raises_error_if_since_or_before_value_is_neither_int_nor_string(self):
         self.validated["querystring"] = {"_before": dict()}
         self.assertRaises(httpexceptions.HTTPBadRequest, self.resource.plural_get)