Repository: mdn/kuma

Issue:
Site search fails to talk to Elasticsearch sometimes
https://sentry.prod.mozaws.net/operations/mdn-prod/issues/6462576/?query=is:unresolved
```
TypeError: u'Search is temporarily unavailable. Please try again in a few minutes.' is not JSON serializable
  File "django/core/handlers/exception.py", line 41, in inner
    response = get_response(request)
  File "django/core/handlers/base.py", line 187, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "django/core/handlers/base.py", line 185, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "newrelic/hooks/framework_django.py", line 544, in wrapper
    return wrapped(*args, **kwargs)
  File "django/views/decorators/cache.py", line 57, in _wrapped_view_func
    response = view_func(request, *args, **kwargs)
  File "django/views/decorators/http.py", line 40, in inner
    return func(request, *args, **kwargs)
  File "ratelimit/decorators.py", line 30, in _wrapped
    return fn(*args, **kw)
  File "kuma/search/views.py", line 34, in search
    return render(request, 'search/react.html', context)
  File "django/shortcuts.py", line 30, in render
    content = loader.render_to_string(template_name, context, request, using=using)
  File "django/template/loader.py", line 68, in render_to_string
    return template.render(context, request)
  File "django_jinja/backend.py", line 106, in render
    return mark_safe(self.template.render(context))
  File "newrelic/api/function_trace.py", line 121, in dynamic_wrapper
    return wrapped(*args, **kwargs)
  File "jinja2/environment.py", line 1008, in render
    return self.environment.handle_exception(exc_info, True)
  File "jinja2/environment.py", line 780, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/app/kuma/search/jinja2/search/react.html", line 3, in top-level template code
    {% set query = request.GET.get('q') %}
  File "/app/jinja2/react_base.html", line 103, in top-level template code
    {% block document_head %}{% endblock %}
  File "/app/kuma/search/jinja2/search/react.html", line 20, in block "document_head"
    results)|safe }}
  File "kuma/wiki/templatetags/ssr.py", line 50, in render_react
    return server_side_render(component_name, data)
  File "kuma/wiki/templatetags/ssr.py", line 99, in server_side_render
    data=json.dumps(data).encode('utf8'),
  File "json/__init__.py", line 244, in dumps
    return _default_encoder.encode(obj)
  File "json/encoder.py", line 207, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "json/encoder.py", line 270, in iterencode
    return _iterencode(o, 0)
  File "json/encoder.py", line 184, in default
    raise TypeError(repr(o) + " is not JSON serializable")
```

This *can* happens when you get a 500 error from the Elasticsearch service. Any 5xx error. 
It's ugly that `Search is temporarily unavailable. Please try again in a few minutes.` is used to catch all errors. 



Discussion:
What should ideally happen is that if Elasticsearch is going to fail, let us know properly instead of trying to mask the error for something else. 

Secondly, we might add a retry mechanism to some of the Elasticsearch errors if they're deemed solvable if you simply retry later. 

Resolving patch:
diff --git a/kuma/search/utils.py b/kuma/search/utils.py
--- a/kuma/search/utils.py
+++ b/kuma/search/utils.py
@@ -1,11 +1,7 @@
 import logging
 
-import elasticsearch
 import six
 from django.utils.translation import ugettext_lazy as _
-from rest_framework import status
-from rest_framework.response import Response
-from rest_framework.views import exception_handler
 from urlobject import URLObject
 
 
@@ -69,24 +65,3 @@ def clean_params(self, params):
             if param and default:
                 clean_params[param] = default
         return clean_params
-
-
-def search_exception_handler(exc, context):
-    # Call REST framework's default exception handler first,
-    # to get the standard error response.
-    response = exception_handler(exc, context)
-
-    if response is None:
-        if isinstance(exc, elasticsearch.ElasticsearchException):
-            # FIXME: This really should return a 503 error instead but Zeus
-            # doesn't let that through and displays a generic error page in that
-            # case which we don't want here
-            log.error('Elasticsearch exception: %s' % exc)
-            return Response({'detail': SEARCH_DOWN_DETAIL},
-                            status=status.HTTP_200_OK)
-        elif isinstance(exc, UnicodeDecodeError):
-            log.error('UnicodeDecodeError exception: %s' % exc)
-            return Response({'detail': SEARCH_ERROR_DETAIL},
-                            status=status.HTTP_404_NOT_FOUND)
-
-    return response
diff --git a/kuma/settings/common.py b/kuma/settings/common.py
--- a/kuma/settings/common.py
+++ b/kuma/settings/common.py
@@ -1747,11 +1747,6 @@ def get_user_url(user):
     "curl",
 ]
 
-# TODO: Once using DRF more we need to make that exception handler more generic
-REST_FRAMEWORK = {
-    'EXCEPTION_HANDLER': 'kuma.search.utils.search_exception_handler'
-}
-
 SENTRY_DSN = config('SENTRY_DSN', default=None)
 
 if SENTRY_DSN:


Test patch:
diff --git a/kuma/search/tests/test_views.py b/kuma/search/tests/test_views.py
--- a/kuma/search/tests/test_views.py
+++ b/kuma/search/tests/test_views.py
@@ -1,5 +1,4 @@
 # -*- coding: utf-8 -*-
-import elasticsearch
 import pytest
 from django.conf import settings
 
@@ -146,53 +145,6 @@ def test_allowed_methods(self):
         assert response.status_code == 405
         assert_no_cache_header(response)
 
-    def test_handled_exceptions(self):
-
-        # These are instantiated with an error string.
-        for exc in [elasticsearch.ElasticsearchException,
-                    elasticsearch.SerializationError,
-                    elasticsearch.TransportError,
-                    elasticsearch.NotFoundError,
-                    elasticsearch.RequestError]:
-
-            class ExceptionSearchView(SearchView):
-                filter_backends = ()
-
-                def list(self, *args, **kwargs):
-                    raise exc(503, 'ERROR!!')
-
-            view = ExceptionSearchView.as_view()
-            request = self.get_request('/en-US/search')
-            response = view(request).render()
-            assert response.status_code == 200
-            assert 'Search is temporarily unavailable' in response.content
-
-    def test_unicode_exception(self):
-        class UnicodeDecodeErrorSearchView(SearchView):
-            filter_backends = ()
-
-            def list(self, *args, **kwargs):
-                # willfully causing a UnicodeDecodeError
-                return 'coöperative'.encode('ascii')
-
-        view = UnicodeDecodeErrorSearchView.as_view()
-        request = self.get_request('/en-US/search')
-        response = view(request).render()
-        assert response.status_code == 404
-        assert 'Something went wrong with the search query' in response.content
-
-    def test_unhandled_exceptions(self):
-        class RealExceptionSearchView(SearchView):
-            filter_backends = ()
-
-            def list(self, *args, **kwargs):
-                raise ValueError
-
-        view = RealExceptionSearchView.as_view()
-        request = self.get_request('/en-US/search')
-        with pytest.raises(ValueError):
-            view(request)
-
     def test_paginate_by_param(self):
         request = self.get_request('/en-US/search')