Repository: pypi/warehouse

Issue:
Invalid ordering param causes search_phrase_execution_exception
It's possible to generate a 500-level exception by passing an invalid `o` parameter (for ordering) to the search route, e.g. https://pypi.org/search/?q=&o=foobar.

We should check if the parameter is a valid field before trying to sort on it, [or possibly just ignore it](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-request-sort.html#_ignoring_unmapped_fields).

Here are the relevant lines in `warehouse/views.py`: https://github.com/pypa/warehouse/blob/436d52d1dcebc593846d04559f38a5de980cb0c1/warehouse/views.py#L219-L220



Discussion:

Resolving patch:
diff --git a/warehouse/views.py b/warehouse/views.py
--- a/warehouse/views.py
+++ b/warehouse/views.py
@@ -217,7 +217,22 @@ def search(request):
         query = request.es.query()
 
     if request.params.get("o"):
-        query = query.sort(request.params["o"])
+        sort_key = request.params["o"]
+        if sort_key.startswith("-"):
+            sort = {
+                sort_key[1:]: {
+                    "order": "desc",
+                    "unmapped_type": "long",
+                },
+            }
+        else:
+            sort = {
+                sort_key: {
+                    "unmapped_type": "long",
+                }
+            }
+
+        query = query.sort(sort)
 
     if request.params.getall("c"):
         query = query.filter("terms", classifiers=request.params.getall("c"))


Test patch:
diff --git a/tests/unit/test_views.py b/tests/unit/test_views.py
--- a/tests/unit/test_views.py
+++ b/tests/unit/test_views.py
@@ -220,11 +220,25 @@ def test_with_a_single_char_query(self, monkeypatch, db_request, page):
             ),
         ]
 
-    @pytest.mark.parametrize("page", [None, 1, 5])
-    def test_with_an_ordering(self, monkeypatch, db_request, page):
-        params = MultiDict({"q": "foo bar", "o": "-created"})
+    @pytest.mark.parametrize(
+        ("page", "order", "expected"),
+        [
+            (None, None, []),
+            (
+                1,
+                "-created",
+                [{"created": {"order": "desc", "unmapped_type": "long"}}],
+            ),
+            (5, "created", [{"created": {"unmapped_type": "long"}}]),
+        ],
+    )
+    def test_with_an_ordering(self, monkeypatch, db_request, page, order,
+                              expected):
+        params = MultiDict({"q": "foo bar"})
         if page is not None:
             params["page"] = page
+        if order is not None:
+            params["o"] = order
         db_request.params = params
 
         sort = pretend.stub()
@@ -254,7 +268,11 @@ def test_with_an_ordering(self, monkeypatch, db_request, page):
             "available_filters": [],
         }
         assert page_cls.calls == [
-            pretend.call(sort, url_maker=url_maker, page=page or 1),
+            pretend.call(
+                sort if order is not None else suggest,
+                url_maker=url_maker,
+                page=page or 1,
+            ),
         ]
         assert url_maker_factory.calls == [pretend.call(db_request)]
         assert db_request.es.query.calls == [
@@ -270,9 +288,7 @@ def test_with_an_ordering(self, monkeypatch, db_request, page):
                 term={"field": "name"},
             ),
         ]
-        assert suggest.sort.calls == [
-            pretend.call("-created")
-        ]
+        assert suggest.sort.calls == [pretend.call(i) for i in expected]
 
     @pytest.mark.parametrize("page", [None, 1, 5])
     def test_with_classifiers(self, monkeypatch, db_request, page):