Repository: encode/django-rest-framework

Issue:
Cursor Pagination doesn't play well with query parameters
The url in next doesn't include the query parameters used in a request, causing 400 bad request responses when using cursor pagination.



Discussion:
Please try to be more specific when raising an issue. Causing a 400 bad request how exactly? What's the smallest possible replicable example?

Fair enough. I'll put some code up tomorrow or the day after. In the meantime, the most basic example, without code, would be this: There is a model Person with a CharField name. There is a a view that is subclassed off of generics.ListAPIView. This view's get_queryset method is overridden to return people whose name contains a search by a user encoded in a parameter q passed in a get request (i.e. `Person.objects.all().filter(Q(name__icontains=self.request.query_params['q']))`).

This view is wired up to a url and instantiated with as_view(). Unfortunately, if Cursor Pagination is active (or others, possibly, I have not tested), then the next link will not include the query parameter q. It will only include a parameter for the cursor. This will result in a "400: Bad Request" because there is no query parameter for the search. I don't know if this is a known issue or not. I've already worked around it on my end.

If none of the above is clear, no worries, I'll drop some code to clarify ASAP. Thank you for your work on a truly fantastic product! I have subclassed, used, and read the code repeatedly this past year, for a variety of products. It truly is a life-saver.

Thanks! That should be enough to get started with.

I still don't quite see where/why that'd result in a 400 (I'd expect a KeyError to be raised by your `get_queryset()` method, instead) but either way the upshot is that CursorPagination should preserve any other query parameters present in the request.


Resolving patch:
diff --git a/rest_framework/utils/urls.py b/rest_framework/utils/urls.py
--- a/rest_framework/utils/urls.py
+++ b/rest_framework/utils/urls.py
@@ -7,7 +7,7 @@ def replace_query_param(url, key, val):
     parameters of the URL, and return the new URL.
     """
     (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
-    query_dict = urlparse.parse_qs(query)
+    query_dict = urlparse.parse_qs(query, keep_blank_values=True)
     query_dict[key] = [val]
     query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True)
     return urlparse.urlunsplit((scheme, netloc, path, query, fragment))
@@ -19,7 +19,7 @@ def remove_query_param(url, key):
     parameters of the URL, and return the new URL.
     """
     (scheme, netloc, path, query, fragment) = urlparse.urlsplit(url)
-    query_dict = urlparse.parse_qs(query)
+    query_dict = urlparse.parse_qs(query, keep_blank_values=True)
     query_dict.pop(key, None)
     query = urlparse.urlencode(sorted(list(query_dict.items())), doseq=True)
     return urlparse.urlunsplit((scheme, netloc, path, query, fragment))


Test patch:
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -108,6 +108,17 @@ def test_additional_query_params_are_preserved(self):
             'count': 50
         }
 
+    def test_empty_query_params_are_preserved(self):
+        request = factory.get('/', {'page': 2, 'filter': ''})
+        response = self.view(request)
+        assert response.status_code == status.HTTP_200_OK
+        assert response.data == {
+            'results': [12, 14, 16, 18, 20],
+            'previous': 'http://testserver/?filter=',
+            'next': 'http://testserver/?filter=&page=3',
+            'count': 50
+        }
+
     def test_404_not_found_for_zero_page(self):
         request = factory.get('/', {'page': '0'})
         response = self.view(request)