Repository: mdn/kuma

Issue:
search_phase_execution_exception on huge queries
https://sentry.prod.mozaws.net/operations/mdn-prod/issues/6572241/

```
RequestError: RequestError(400, u'search_phase_execution_exception', u'failed to create query: {\n  "boosting" : {\n    "positive" : {\n      "bool" : {\n        "must" : [\n          {\n            "function_score" : {\n              "query" : {\n                "bool" : {\n                  "should" : [\n                    {\n                      "match" : {\n                        "title" : {\n                          "query" : "Were insured or registered they are no further in affect nor will we insure cover or entertain any further communication of any kind with you , your family or business... Here on out.   Hope you had a brain at one time and listened to the old grumpy freddie Mac and saved your money and prepared for \\"That future financial crisis that he constantly warned EVERYONE ABOUT YEAR AFTER YEAR LIKE A BROKEN RECORD.   WELL THE FUTURE Is a dead word and no longer will be in ANY dictionary.   Because this is the present as it was years ago **aka today was this future day** we are creating the future each and every moment of every day.   As I told tony the other night.    So how u treat others and yourself today will in Fact what your future also looks like.   So the more you think you have the choice or option to ignore what is important or needed from you today(SPECIFICALLY FROM ME MELISSA jean Capuano***like I have been begging you everyday to hear me and stop letting the lies lead and dictate you and your future....letting either the control of a no one to dictate to you and your heart what is acceptable and right or wrong...... Is JUST WHAT I HAVR BEEN BEGGING YOU TO HEAR MEWARN U***THAT IT WAS BULLSHIT AND THEY WERE ROBBING YOU YOURHAPPINESS AND STRAIGHT UP BROKE MY HEART INTO PIECES EVERYDAY.   AND IF THEY HAD NO GUN TO YOU AND YOUR FAMILIES HEAD. YOU CHOSE THE DOLLLAR BILL OVER Yourself, your kids yout parents etc... And you def chose you over ME and on top of that.... Even WORSE U ALLOWED GRANDMA ALLOWENCES TO RULE YOU AND DENY ME AND MY HEART THE CGANVE TO NOTcontinuesly be cut open as you let them literally pour salt in it and you watched!   If you were able to sleep and eat and go to work.  That is EXACTLY what you did and i literally begged you for your sake first and literally it got to where i needed help and not one person stepped the fuck up in the manner that i did would and will always for the one i love and my kids.  But now there has to be Consequences for your lack of care and love.   I can not will not *FORGET* that you all literally followed the words of another over you allowing uour own intuition and love to lead you.    Sad no pathertic!  BecauSE YOU LISTENED TO SOMEONE WHO I AM EXQCTLY LIKE BUT GUESS WHAT. I ALSO HAVE ALL THEY WERE LACKING THE ABILITY TO BE honest witH Me and not go against what doesnt feel 100\u2105right EVEN tho that mean sticking out like a crazy dirt bag pill popping depressed Mother who couldn\'t get a grip on herself without the love her and her Grandma stared present in my life.   And no matter how right or wrong it looks or how pathetic it was cause it is for a mother to tell her kids she wanted to die.... That wasn\'t me and never ever was again or would be.  But august 1st I lost a piece of me that was unacceptable to cope or function without and I didn\'t and still do y fully understand it in human words to explain. It was spiritual dealth that I would habe to say now isn\'t comparable to any level of loss or pain, drug come down or breakup or death of a loved one and she hadn\'t even **Passed as of that day* nor did I even know that she was that bad and going to that week *my birthday night in between me and my sisters birthday(same night I got ahold of tony and my mother acted like an actual mother with a whole spirit that was alive and had personality that I was able to connect and bible with. I waited for the candy cameras once again that night as I did for more than now.  Because I could not accept the visual reality  not alligned to the emotion and spiritual reality in which I guess I\'ve always lived and without it as it had always been.   I can tell...
(25 additional frame(s) were not displayed)
...
  File "rest_framework/views.py", line 492, in dispatch
    response = handler(request, *args, **kwargs)
  File "rest_framework/generics.py", line 201, in get
    return self.list(request, *args, **kwargs)
  File "rest_framework/mixins.py", line 42, in list
    page = self.paginate_queryset(queryset)
  File "rest_framework/generics.py", line 173, in paginate_queryset
    return self.paginator.paginate_queryset(queryset, self.request, view=self)
  File "rest_framework/pagination.py", line 204, in paginate_queryset
    self.page = paginator.page(page_number)

RequestError: RequestError(400, u'search_phase_execution_exception', u'failed to create query: {\n  "boosting" : {\n    "positive" : {\n      "bool" : {\n        "must" : [\n          {\n            "function_score" : {\n              "query" : {\n                "bool" : {\n                  "should" : [\n                    {\n                      "match" : {\n                        "title" : {\n                          "query" : "Were insured or registered they are no further in affect nor will we insure cover or entertain any further communication of any kind with you , your family or business... Here on out.   Hope you had a brain at one time and listened to the old grumpy freddie Mac and saved your money and prepared for \\"That future financial crisis that he constantly warned EVERYONE ABOUT YEAR AFTER YEAR LIKE A BROKEN RECORD.   WELL THE FUTURE Is a dead word and no longer will be in ANY dictionary.   Because this is the present as it was years ago **aka today was this future day** we are creating the future each and every moment of every day.   As I told tony the other night.    So how u treat others and yourself today will in Fact what your future also looks like.   So the more you think you have the choice or option to ignore what is important or needed from you today(SPECIFICALLY FROM ME MELISSA jean Capuano***like I have been begging you everyday to hear me and stop letting the lies lead and dictate you and your future....letting either the control of a no one to dictate to you and your heart what is acceptable and right or wrong...... Is JUST WHAT I HAVR BEEN BEGGING YOU TO HEAR MEWARN U***THAT IT WAS BULLSHIT AND THEY WERE ROBBING YOU YOURHAPPINESS AND STRAIGHT UP BROKE MY HEART INTO PIECES EVERYDAY.   AND IF THEY HAD NO GUN TO YOU AND YOUR FAMILIES HEAD. YOU CHOSE THE DOLLLAR BILL OVER Yourself, your kids yout parents etc... And you def chose you over ME and on top of that.... Even WORSE U ALLOWED GRANDMA ALLOWENCES TO RULE YOU AND DENY ME AND MY HEART THE CGANVE TO NOTcontinuesly be cut open as you let them literally pour salt in it and you watched!   If you were able to sleep and eat and go to work.  That is EXACTLY what you did and i literally begged you for your sake first and literally it got to where i needed help and not one person stepped the fuck up in the manner that i did would and will always for the one i love and my kids.  But now there has to be Consequences for your lack of care and love.   I can not will not *FORGET* that you all literally followed the words of another over you allowing uour own intuition and love to lead you.    Sad no pathertic!  BecauSE YOU LISTENED TO SOMEONE WHO I AM EXQCTLY LIKE BUT GUESS WHAT. I ALSO HAVE ALL THEY WERE LACKING THE ABILITY TO BE honest witH Me and not go against what doesnt feel 100\u2105right EVEN tho that mean sticking out like a crazy dirt bag pill popping depressed Mother who couldn\'t get a grip on herself without the love her and her Grandma stared present in my life.   And no matter how right or wrong it looks or how pathetic it was cause it is for a mother to tell her kids she wanted to die.... That wasn\'t me and never ever was again or would be.  But august 1st I lost a piece of me that was unacceptable to cope or function without and I didn\'t and still do y fully understand it in human words to explain. It was spiritual dealth that I would habe to say now isn\'t comparable to any level of loss or pain, drug come down or breakup or death of a loved one and she hadn\'t even **Passed as of that day* nor did I even know that she was that bad and going to that week *my birthday night in between me and my sisters birthday(same night I got ahold of tony and my mother acted like an actual mother with a whole spirit that was alive and had personality that I was able to connect and bible with. I waited for the candy cameras once again that night as I did for more than now.  Because I could not accept the visual reality  not alligned to the emotion and spiritual reality in which I guess I\'ve always lived and without it as it had always been.   I can tell everyone one last time. I can not will not and don\'t want to even allow my children their own rights to choose life... If it was to stay like this.  I would infact if I Was TrUmP have hit that nuclear button months ago and took everyone out. Cause this is the true definition of HELL on earth with out a glimpse of any faith have I been left sitting here with day and night after night alone. Waiting by nw door for my grandma to come walkimg through the door. Because if she doesn\'t I swear and promise I\'m gone bevause I trued everything in my human power to overcome this heaetwrenching ache and devestation and I can\'t.   And the mental anguish I\'ve been sitting in.  Having three beautiful angels that I adore and live for and literally are my all.... Have an empty shell of a moment in capable of healing herself and no fucking sycologist or therapy will ever fill this void.   Like I said I had a good my whole life and that was why turned to the bible amd GOD and meetinga and studying human behavior my whole life always longing to connect and understand other human beings hearts and offer them the support and deserved ear and care , we all deserve when we arent up to being ourselves or go through something.  But I was left here yo literally die..... (I will leave out the details. That only my sick family members will be able to answer your or come up with some type of explanation too... Like leaving me with no running water.... Food money gas.... Right after lost the closest person. To my heart and soul.    Until my son came home and I struggle to even be available for him and I have left my babygirl over by my poppy Because I knew that she was a strong and whole emotion spirit that shw would be ok and they needed her and I always yearned for hwr to have other family other than me and gram and ang and aj over the last 6yeara like and and AJ did growing up. Sleeping at gram and PoppYa house 2\\"3\\"4 days at a time like I did my whole like.   BEING",\n                          "operator" : "OR",\n                          "prefix_length" : 0,\n                          "max_expansions" : 50,\n                          "fuzzy_transpositions" : true,\n                          "lenient" : false,\n                          "zero_terms_query" : "NONE",\n                          "auto_generate_synonyms_phrase_query" : true,\n                          "boost" : 7.2\n                        }\n                      }\n                    },\n                    {\n                      "match" : {\n                        "summary" : {\n                          "query" : "Were insured or registered they are no further in affect nor will we insure cover or entertain any further communication of any kind with you , your family or business... Here on out.   Hope you had a brain at one time and listened to the old grumpy freddie Mac and saved your money and prepared for \\"That future financial crisis that he constantly warned EVERYONE ABOUT YEAR AFTER YEAR LIKE A BROKEN RECORD.   WELL THE FUTURE Is a dead word and no longer will be in ANY dictionary.   Because this is the present as it was years ago **aka today was this future day** we are creating the future each and every moment of every day.   As I told tony the other night.    So how u treat others and yourself today will in Fact what your future also looks like.   So the more you think you have the choice or option to ignore what is important or needed from you today(SPECIFICALLY FROM ME MELISSA jean Capuano***like I have been begging you everyday to hear me and stop letting the lies lead and dictate you and your future....letting either the control of a no one to dictate to you and your heart what is acceptable and right or wrong...... Is JUST WHAT I HAVR BEEN BEGGING YOU TO HEAR MEWARN U***THAT IT WAS BULLSHIT AND THEY WERE ROBBING YOU YOURHAPPINESS AND STRAIGHT UP BROKE MY HEART INTO PIECES EVERYDAY.   AND IF THEY HAD NO GUN TO YOU AND YOUR FAMILIES HEAD. YOU CHOSE THE DOLLLAR BILL OVER Yourself, your kids yout parents etc... And you def chose you over ME an...
```


Discussion:
We *used* swallow all errors from Elasticsearch and say something like "Search unavilable. Please try again later" which was a bit of a "lie". 
In cases that like this we should not let it throw an internal server error because the fault lies with the client. 
At the very least, if Elasticsearch throws a `RequestError(400, ...)` we should do so on the Django view too. 
Another thing we could do is make a sanity check the query first so that it's simply not too long and return a `400 Bad Request` before we even sending it to Elasticsearch. That might be a bit harder. 

If you don't have access to Sentry, the offending URL is something like this:
```
http://mdn.localhost:8000/en-US/search?q=Were+insured+or+registered+they+are+no+further+in+affect+nor+will+we+insure+cover+or+entertain+any+further+communication+of+any+kind+with+you+%2C+your+family+or+business...+Here+on+out.+++Hope+you+had+a+brain+at+one+time+and+listened+to+the+old+grumpy+freddie+Mac+and+saved+your+money+and+prepared+for+%22That+future+financial+crisis+that+he+constantly+warned+EVERYONE+ABOUT+YEAR+AFTER+YEAR+LIKE+A+BROKEN+RECORD.+++WELL+THE+FUTURE+Is+a+dead+word+and+no+longer+will+be+in+ANY+dictionary.+++Because+this+is+the+present+as+it+was+years+ago+**aka+today+was+this+future+day**+we+are+creating+the+future+each+and+every+moment+of+every+day.+++As+I+told+tony+the+other+night.++++So+how+u+treat+others+and+yourself+today+will+in+Fact+what+your+future+also+looks+like.+++So+the+more+you+think+you+have+the+choice+or+option+to+ignore+what+is+important+or+needed+from+you+today%28SPECIFICALLY+FROM+ME+MELISSA+jean+Capuano***like+I+have+been+begging+you+everyday+to+hear+me+and+stop+letting+the+lies+lead+and+dictate+you+and+your+future....letting+either+the+control+of+a+no+one+to+dictate+to+you+and+your+heart+what+is+acceptable+and+right+or+wrong......+Is+JUST+WHAT+I+HAVR+BEEN+BEGGING+YOU+TO+HEAR+MEWARN+U***THAT+IT+WAS+BULLSHIT+AND+THEY+WERE+ROBBING+YOU+YOURHAPPINESS+AND+STRAIGHT+UP+BROKE+MY+HEART+INTO+PIECES+EVERYDAY.+++AND+IF+THEY+HAD+NO+GUN+TO+YOU+AND+YOUR+FAMILIES+HEAD.+YOU+CHOSE+THE+DOLLLAR+BILL+OVER+Yourself%2C+your+kids+yout+parents+etc...+And+you+def+chose+you+over+ME+and+on+top+of+that....+Even+WORSE+U+ALLOWED+GRANDMA+ALLOWENCES+TO+RULE+YOU+AND+DENY+ME+AND+MY+HEART+THE+CGANVE+TO+NOTcontinuesly+be+cut+open+as+you+let+them+literally+pour+salt+in+it+and+you+watched%21+++If+you+were+able+to+sleep+and+eat+and+go+to+work.++That+is+EXACTLY+what+you+did+and+i+literally+begged+you+for+your+sake+first+and+literally+it+got+to+where+i+needed+help+and+not+one+person+stepped+the+fuck+up+in+the+manner+that+i+did+would+and+will+always+for+the+one+i+love+and+my+kids.++But+now+there+has+to+be+Consequences+for+your+lack+of+care+and+love.+++I+can+not+will+not+*FORGET*+that+you+all+literally+followed+the+words+of+another+over+you+allowing+uour+own+intuition+and+love+to+lead+you.++++Sad+no+pathertic%21++BecauSE+YOU+LISTENED+TO+SOMEONE+WHO+I+AM+EXQCTLY+LIKE+BUT+GUESS+WHAT.+I+ALSO+HAVE+ALL+THEY+WERE+LACKING+THE+ABILITY+TO+BE+honest+witH+Me+and+not+go+against+what+doesnt+feel+100%E2%84%85right+EVEN+tho+that+mean+sticking+out+like+a+crazy+dirt+bag+pill+popping+depressed+Mother+who+couldn%27t+get+a+grip+on+herself+without+the+love+her+and+her+Grandma+stared+present+in+my+life.+++And+no+matter+how+right+or+wrong+it+looks+or+how+pathetic+it+was+cause+it+is+for+a+mother+to+tell+her+kids+she+wanted+to+die....+That+wasn%27t+me+and+never+ever+was+again+or+would+be.++But+august+1st+I+lost+a+piece+of+me+that+was+unacceptable+to+cope+or+function+without+and+I+didn%27t+and+still+do+y+fully+understand+it+in+human+words+to+explain.+It+was+spiritual+dealth+that+I+would+habe+to+say+now+isn%27t+comparable+to+any+level+of+loss+or+pain%2C+drug+come+down+or+breakup+or+death+of+a+loved+one+and+she+hadn%27t+even+**Passed+as+of+that+day*+nor+did+I+even+know+that+she+was+that+bad+and+going+to+that+week+*my+birthday+night+in+between+me+and+my+sisters+birthday%28same+night+I+got+ahold+of+tony+and+my+mother+acted+like+an+actual+mother+with+a+whole+spirit+that+was+alive+and+had+personality+that+I+was+able+to+connect+and+bible+with.+I+waited+for+the+candy+cameras+once+again+that+night+as+I+did+for+more+than+now.++Because+I+could+not+accept+the+visual+reality++not+alligned+to+the+emotion+and+spiritual+reality+in+which+I+guess+I%27ve+always+lived+and+without+it+as+it+had+always+been.+++I+can+tell+everyone+one+last+time.+I+can+not+will+not+and+don%27t+want+to+even+allow+my+children+their+own+rights+to+choose+life...+If+it+was+to+stay+like+this.++I+would+infact+if+I+Was+TrUmP+have+hit+that+nuclear+button+months+ago+and+took+everyone+out.+Cause+this+is+the+true+definition+of+HELL+on+earth+with+out+a+glimpse+of+any+faith+have+I+been+left+sitting+here+with+day+and+night+after+night+alone.+Waiti...
```

Resolving patch:
diff --git a/kuma/search/serializers.py b/kuma/search/serializers.py
--- a/kuma/search/serializers.py
+++ b/kuma/search/serializers.py
@@ -1,3 +1,4 @@
+from django.conf import settings
 from django.utils.translation import ugettext
 from rest_framework import serializers
 
@@ -6,13 +7,21 @@
 
 
 class SearchQuerySerializer(serializers.Serializer):
-    q = serializers.CharField(required=False)
+    q = serializers.CharField(
+        required=False, max_length=settings.ES_Q_MAXLENGTH)
     highlight = serializers.BooleanField(required=False, default=True)
     # Advanced search query paramenters.
     css_classnames = serializers.CharField(required=False)
     html_attributes = serializers.CharField(required=False)
     kumascript_macros = serializers.CharField(required=False)
 
+    def validate_q(self, value):
+        # Check that \n not in query
+        if r'\n' in value:
+            raise serializers.ValidationError(
+                'Search term must not contain new line')
+        return value
+
 
 class FilterURLSerializer(serializers.Serializer):
     active = serializers.ReadOnlyField()
diff --git a/kuma/search/views.py b/kuma/search/views.py
--- a/kuma/search/views.py
+++ b/kuma/search/views.py
@@ -27,13 +27,21 @@ def search(request, *args, **kwargs):
         return wiki_search(request, *args, **kwargs)
 
     results = search_api(request, *args, **kwargs).data
+
+    # Determine if there were validation errors
+    error = results.get('error') or results.get('q')
+    # If q is returned in the data, there was a validation error for that field,
+    # so return 400 status.
+    status = 200 if results.get('q') is None else 400
+
     context = {
         'results': {
-            'results': None if results.get('error') else results
+            'results': None if error else results,
+            'error': error
         }
     }
 
-    return render(request, 'search/react.html', context)
+    return render(request, 'search/react.html', context, status=status)
 
 
 wiki_search = SearchView.as_view()
diff --git a/kuma/settings/common.py b/kuma/settings/common.py
--- a/kuma/settings/common.py
+++ b/kuma/settings/common.py
@@ -1632,6 +1632,9 @@ def parse_iframe_url(url):
 ES_INDEXING_TIMEOUT = 30
 ES_LIVE_INDEX = config('ES_LIVE_INDEX', default=False, cast=bool)
 ES_URLS = config('ES_URLS', default='127.0.0.1:9200', cast=Csv())
+# Specify a max length for the q param to avoid unnecessary burden on
+# elasticsearch for queries that are probably either mistakes or junk.
+ES_Q_MAXLENGTH = config('ES_Q_MAXLENGTH', default=200, cast=int)
 
 
 # Logging is merged with the default logging


Test patch:
diff --git a/kuma/api/v1/tests/test_views.py b/kuma/api/v1/tests/test_views.py
--- a/kuma/api/v1/tests/test_views.py
+++ b/kuma/api/v1/tests/test_views.py
@@ -1,6 +1,7 @@
 
 
 import pytest
+from django.conf import settings
 from waffle.models import Flag, Sample, Switch
 
 from kuma.api.v1.views import (document_api_data, get_content_based_redirect,
@@ -307,6 +308,19 @@ def test_search_validation_problems(user_client):
     assert response.status_code == 400
     assert response.json()['error'] == 'Not a valid locale code'
 
+    # 'q' present but contains new line
+    response = user_client.get(url, {'q': r'test\nsomething'})
+    assert response.status_code == 400
+    assert response.json()['q'] == ["Search term must not contain new line"]
+
+    # 'q' present but exceeds max allowed characters
+    response = user_client.get(url, {'q': 'x' * (settings.ES_Q_MAXLENGTH + 1)})
+    assert response.status_code == 400
+    assert (
+        response.json()['q'] ==
+        [f"Ensure this field has no more than {settings.ES_Q_MAXLENGTH} characters."]
+    )
+
 
 class SearchViewTests(ElasticTestCase):
     fixtures = ElasticTestCase.fixtures + ['wiki/documents.json',
diff --git a/kuma/search/tests/test_serializers.py b/kuma/search/tests/test_serializers.py
--- a/kuma/search/tests/test_serializers.py
+++ b/kuma/search/tests/test_serializers.py
@@ -1,5 +1,6 @@
 from unittest import mock
 
+from django.conf import settings
 from django.utils import translation
 from rest_framework import serializers
 from rest_framework.test import APIRequestFactory
@@ -10,7 +11,7 @@
 from ..fields import SearchQueryField, SiteURLField
 from ..models import Filter, FilterGroup
 from ..serializers import (DocumentSerializer, FilterSerializer,
-                           FilterWithGroupSerializer)
+                           FilterWithGroupSerializer, SearchQuerySerializer)
 
 
 class SerializerTests(ElasticTestCase):
@@ -59,6 +60,25 @@ def test_document_serializer(self):
         assert isinstance(dict_data, dict)
         assert dict_data['id'] == result[0].id
 
+    def test_search_query_serializer(self):
+        search_serializer = SearchQuerySerializer(
+            data={'q': 'test'}
+        )
+        assert search_serializer.is_valid()
+        assert search_serializer.errors == {}
+
+        search_serializer = SearchQuerySerializer(
+            data={'q': r'test\nsomething'}
+        )
+        assert not search_serializer.is_valid()
+        assert list(search_serializer.errors.keys()) == ['q']
+
+        search_serializer = SearchQuerySerializer(
+            data={'q': 't' * (settings.ES_Q_MAXLENGTH + 1)}
+        )
+        assert not search_serializer.is_valid()
+        assert list(search_serializer.errors.keys()) == ['q']
+
     def test_excerpt(self):
         search = WikiDocumentType.search()
         search = search.query('match', summary='CSS')