Repository: searx/searx

Issue:
Unhandled expection when engine times out
The following exception occurs when an engine does not respond in time returning a timeout error.

```
Exception in thread aeb1ee8b-0fe7-4e90-8f29-0c487673c1eb:
Traceback (most recent call last):
  File "/home/n/p/searx/venv3.6/lib/python3.6/site-packages/urllib3/connectionpool.py", line 421, in _make_request
    six.raise_from(e, None)
  File "<string>", line 3, in raise_from
  File "/home/n/p/searx/venv3.6/lib/python3.6/site-packages/urllib3/connectionpool.py", line 416, in _make_request
    httplib_response = conn.getresponse()
  File "/home/n/.pyenv/versions/3.6.3/lib/python3.6/http/client.py", line 1331, in getresponse
    response.begin()
  File "/home/n/.pyenv/versions/3.6.3/lib/python3.6/http/client.py", line 297, in begin
    version, status, reason = self._read_status()
  File "/home/n/.pyenv/versions/3.6.3/lib/python3.6/http/client.py", line 258, in _read_status
    line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1")
  File "/home/n/.pyenv/versions/3.6.3/lib/python3.6/socket.py", line 586, in readinto
    return self._sock.recv_into(b)
  File "/home/n/p/searx/venv3.6/lib/python3.6/site-packages/urllib3/contrib/pyopenssl.py", line 326, in recv_into
    raise timeout("The read operation timed out")
socket.timeout: The read operation timed out

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/n/p/searx/venv3.6/lib/python3.6/site-packages/requests/adapters.py", line 449, in send
    timeout=timeout
  File "/home/n/p/searx/venv3.6/lib/python3.6/site-packages/urllib3/connectionpool.py", line 720, in urlopen
    method, url, error=e, _pool=self, _stacktrace=sys.exc_info()[2]
  File "/home/n/p/searx/venv3.6/lib/python3.6/site-packages/urllib3/util/retry.py", line 400, in increment
    raise six.reraise(type(error), error, _stacktrace)
  File "/home/n/p/searx/venv3.6/lib/python3.6/site-packages/urllib3/packages/six.py", line 735, in reraise
    raise value
  File "/home/n/p/searx/venv3.6/lib/python3.6/site-packages/urllib3/connectionpool.py", line 672, in urlopen
    chunked=chunked,
  File "/home/n/p/searx/venv3.6/lib/python3.6/site-packages/urllib3/connectionpool.py", line 423, in _make_request
    self._raise_timeout(err=e, url=url, timeout_value=read_timeout)
  File "/home/n/p/searx/venv3.6/lib/python3.6/site-packages/urllib3/connectionpool.py", line 331, in _raise_timeout
    self, url, "Read timed out. (read timeout=%s)" % timeout_value
urllib3.exceptions.ReadTimeoutError: HTTPSConnectionPool(host='gitlab.com', port=443): Read timed out. (read timeout=1.0)

During handling of the above exception, another exception occurred:


Traceback (most recent call last):
  File "/home/n/p/searx/searx/search.py", line 160, in search_one_http_request_safe
    search_results = search_one_http_request(engine, query, request_params)
  File "/home/n/p/searx/searx/search.py", line 93, in search_one_http_request
    response = send_http_request(engine, request_params)
  File "/home/n/p/searx/searx/search.py", line 77, in send_http_request
    return req(request_params['url'], **request_args)
  File "/home/n/p/searx/searx/poolrequests.py", line 133, in get
    return request('get', url, **kwargs)
  File "/home/n/p/searx/searx/poolrequests.py", line 109, in request
    response = session.request(method=method, url=url, **kwargs)
  File "/home/n/p/searx/venv3.6/lib/python3.6/site-packages/requests/sessions.py", line 533, in request
    resp = self.send(prep, **send_kwargs)
  File "/home/n/p/searx/venv3.6/lib/python3.6/site-packages/requests/sessions.py", line 646, in send
    r = adapter.send(request, **kwargs)
  File "/home/n/p/searx/venv3.6/lib/python3.6/site-packages/requests/adapters.py", line 529, in send
    raise ReadTimeout(e, request=request)
requests.exceptions.ReadTimeout: HTTPSConnectionPool(host='gitlab.com', port=443): Read timed out. (read timeout=1.0)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/n/.pyenv/versions/3.6.3/lib/python3.6/threading.py", line 916, in _bootstrap_inner
    self.run()
  File "/home/n/.pyenv/versions/3.6.3/lib/python3.6/threading.py", line 864, in run
    self._target(*self._args, **self._kwargs)
  File "/home/n/p/searx/searx/search.py", line 107, in search_one_request_safe
    return search_one_http_request_safe(engine_name, query, request_params, result_container, start_time, timeout_limit)
  File "/home/n/p/searx/searx/search.py", line 189, in search_one_http_request_safe
    result_container.add_unresponsive_engine((engine_name, gettext('timeout')))
  File "/home/n/p/searx/venv3.6/lib/python3.6/site-packages/flask_babel/__init__.py", line 548, in gettext
    t = get_translations()
  File "searx/webapp.py", line 168, in _get_translations
    translation_locale = request.form.get('use-translation')
  File "/home/n/p/searx/venv3.6/lib/python3.6/site-packages/werkzeug/local.py", line 347, in __getattr__
    return getattr(self._get_current_object(), name)
  File "/home/n/p/searx/venv3.6/lib/python3.6/site-packages/werkzeug/local.py", line 306, in _get_current_object
    return self.__local()
  File "/home/n/p/searx/venv3.6/lib/python3.6/site-packages/flask/globals.py", line 37, in _lookup_req_object
    raise RuntimeError(_request_ctx_err_msg)
RuntimeError: Working outside of request context.

This typically means that you attempted to use functionality that needed
an active HTTP request.  Consult the documentation on testing for
information about how to avoid this problem.
```

@dalf has already provided a patch to fix it: https://github.com/asciimoo/searx/pull/1903#issuecomment-607137470


Discussion:

Resolving patch:
diff --git a/searx/results.py b/searx/results.py
--- a/searx/results.py
+++ b/searx/results.py
@@ -345,8 +345,8 @@ def results_number(self):
             return 0
         return resultnum_sum / len(self._number_of_results)
 
-    def add_unresponsive_engine(self, engine_error):
-        self.unresponsive_engines.add(engine_error)
+    def add_unresponsive_engine(self, engine_name, error_type, error_message=None):
+        self.unresponsive_engines.add((engine_name, error_type, error_message))
 
     def add_timing(self, engine_name, engine_time, page_load_time):
         self.timings.append({
diff --git a/searx/search.py b/searx/search.py
--- a/searx/search.py
+++ b/searx/search.py
@@ -127,11 +127,7 @@ def search_one_offline_request_safe(engine_name, query, request_params, result_c
         logger.exception('engine {0} : invalid input : {1}'.format(engine_name, e))
     except Exception as e:
         record_offline_engine_stats_on_error(engine, result_container, start_time)
-
-        result_container.add_unresponsive_engine((
-            engine_name,
-            u'{0}: {1}'.format(gettext('unexpected crash'), e),
-        ))
+        result_container.add_unresponsive_engine(engine_name, 'unexpected crash', str(e))
         logger.exception('engine {0} : exception : {1}'.format(engine_name, e))
 
 
@@ -186,24 +182,21 @@ def search_one_http_request_safe(engine_name, query, request_params, result_cont
             engine.stats['errors'] += 1
 
         if (issubclass(e.__class__, requests.exceptions.Timeout)):
-            result_container.add_unresponsive_engine((engine_name, gettext('timeout')))
+            result_container.add_unresponsive_engine(engine_name, 'timeout')
             # requests timeout (connect or read)
             logger.error("engine {0} : HTTP requests timeout"
                          "(search duration : {1} s, timeout: {2} s) : {3}"
                          .format(engine_name, engine_time, timeout_limit, e.__class__.__name__))
             requests_exception = True
         elif (issubclass(e.__class__, requests.exceptions.RequestException)):
-            result_container.add_unresponsive_engine((engine_name, gettext('request exception')))
+            result_container.add_unresponsive_engine(engine_name, 'request exception')
             # other requests exception
             logger.exception("engine {0} : requests exception"
                              "(search duration : {1} s, timeout: {2} s) : {3}"
                              .format(engine_name, engine_time, timeout_limit, e))
             requests_exception = True
         else:
-            result_container.add_unresponsive_engine((
-                engine_name,
-                u'{0}: {1}'.format(gettext('unexpected crash'), e),
-            ))
+            result_container.add_unresponsive_engine(engine_name, 'unexpected crash', str(e))
             # others errors
             logger.exception('engine {0} : exception : {1}'.format(engine_name, e))
 
@@ -238,7 +231,7 @@ def search_multiple_requests(requests, result_container, start_time, timeout_lim
             remaining_time = max(0.0, timeout_limit - (time() - start_time))
             th.join(remaining_time)
             if th.isAlive():
-                result_container.add_unresponsive_engine((th._engine_name, gettext('timeout')))
+                result_container.add_unresponsive_engine(th._engine_name, 'timeout')
                 logger.warning('engine timeout: {0}'.format(th._engine_name))
 
 
diff --git a/searx/webapp.py b/searx/webapp.py
--- a/searx/webapp.py
+++ b/searx/webapp.py
@@ -56,6 +56,7 @@
 from babel.support import Translations
 import flask_babel
 from flask_babel import Babel, gettext, format_date, format_decimal
+from flask.ctx import has_request_context
 from flask.json import jsonify
 from searx import brand
 from searx import settings, searx_dir, searx_debug
@@ -165,13 +166,11 @@
 
 # monkey patch for flask_babel.get_translations
 def _get_translations():
-    translation_locale = request.form.get('use-translation')
-    if translation_locale:
+    if has_request_context() and request.form.get('use-translation') == 'oc':
         babel_ext = flask_babel.current_app.extensions['babel']
-        translation = Translations.load(next(babel_ext.translation_directories), 'oc')
-    else:
-        translation = _flask_babel_get_translations()
-    return translation
+        return Translations.load(next(babel_ext.translation_directories), 'oc')
+
+    return _flask_babel_get_translations()
 
 
 flask_babel.get_translations = _get_translations
@@ -627,7 +626,7 @@ def index():
                                     'corrections': list(result_container.corrections),
                                     'infoboxes': result_container.infoboxes,
                                     'suggestions': list(result_container.suggestions),
-                                    'unresponsive_engines': list(result_container.unresponsive_engines)},
+                                    'unresponsive_engines': __get_translated_errors(result_container.unresponsive_engines)},  # noqa
                                    default=lambda item: list(item) if isinstance(item, set) else item),
                         mimetype='application/json')
     elif output_format == 'csv':
@@ -695,7 +694,7 @@ def index():
         corrections=correction_urls,
         infoboxes=result_container.infoboxes,
         paging=result_container.paging,
-        unresponsive_engines=result_container.unresponsive_engines,
+        unresponsive_engines=__get_translated_errors(result_container.unresponsive_engines),
         current_language=match_language(search_query.lang,
                                         LANGUAGE_CODES,
                                         fallback=request.preferences.get_value("language")),
@@ -706,6 +705,16 @@ def index():
     )
 
 
+def __get_translated_errors(unresponsive_engines):
+    translated_errors = []
+    for unresponsive_engine in unresponsive_engines:
+        error_msg = gettext(unresponsive_engine[1])
+        if unresponsive_engine[2]:
+            error_msg = "{} {}".format(error_msg, unresponsive_engine[2])
+        translated_errors.append((unresponsive_engine[0], error_msg))
+    return translated_errors
+
+
 @app.route('/about', methods=['GET'])
 def about():
     """Render about page"""


Test patch: