From d715fa0efd10c436f786197e9d2e422f9bc372f9 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Tue, 4 Jun 2019 12:20:44 +0200
Subject: [PATCH] Fixed error handling

---
 retribute_api/search/consumers.py | 72 ++++++++++++++++++++++++++++++-
 1 file changed, 70 insertions(+), 2 deletions(-)

diff --git a/retribute_api/search/consumers.py b/retribute_api/search/consumers.py
index f5ff473..eb3867a 100644
--- a/retribute_api/search/consumers.py
+++ b/retribute_api/search/consumers.py
@@ -1,6 +1,7 @@
 import asyncio
 import aiohttp.client
 import json
+import ssl
 
 from channels.generic.http import AsyncHttpConsumer
 
@@ -33,11 +34,58 @@ def wrapper_500(callback):
     return callback
 
 
+def ignore_aiohttp_ssl_eror(loop, aiohttpversion="3.5.4"):
+    """Ignore aiohttp #3535 issue with SSL data after close
+
+    There appears to be an issue on Python 3.7 and aiohttp SSL that throws a
+    ssl.SSLError fatal error (ssl.SSLError: [SSL: KRB5_S_INIT] application data
+    after close notify (_ssl.c:2609)) after we are already done with the
+    connection. See GitHub issue aio-libs/aiohttp#3535
+
+    Given a loop, this sets up a exception handler that ignores this specific
+    exception, but passes everything else on to the previous exception handler
+    this one replaces.
+
+    If the current aiohttp version is not exactly equal to aiohttpversion
+    nothing is done, assuming that the next version will have this bug fixed.
+    This can be disabled by setting this parameter to None
+
+    """
+    if aiohttpversion is not None and aiohttp.__version__ != aiohttpversion:
+        return
+
+    orig_handler = loop.get_exception_handler()
+
+    def ignore_ssl_error(loop, context):
+        if context.get("message") == "SSL error in data received":
+            # validate we have the right exception, transport and protocol
+            exception = context.get("exception")
+            protocol = context.get("protocol")
+            if (
+                isinstance(exception, ssl.SSLError)
+                and exception.reason == "KRB5_S_INIT"
+                and isinstance(protocol, asyncio.sslproto.SSLProtocol)
+                and isinstance(
+                    protocol._app_protocol, aiohttp.client_proto.ResponseHandler
+                )
+            ):
+                if loop.get_debug():
+                    asyncio.log.logger.debug("Ignoring aiohttp SSL KRB5_S_INIT error")
+                return
+        if orig_handler is not None:
+            orig_handler(loop, context)
+        else:
+            loop.default_exception_handler(context)
+
+    loop.set_exception_handler(ignore_ssl_error)
+
+
 class SearchSingleConsumer(AsyncHttpConsumer):
     @wrapper_500
     async def handle(self, body):
         lookup_type = self.scope["url_route"]["kwargs"]["lookup_type"]
         lookup = self.scope["url_route"]["kwargs"]["lookup"]
+        ignore_aiohttp_ssl_eror(asyncio.get_running_loop())
         try:
             source = sources.registry._data[lookup_type]
         except KeyError:
@@ -59,16 +107,35 @@ async def do_lookup(lookup, lookup_type, session, source, results):
     try:
         data = await source.get(lookup, session)
         profile = sources.result_to_retribute_profile(lookup_type, lookup, data)
-    except (exceptions.SearchError, aiohttp.ClientError) as e:
+    except (
+        exceptions.SearchError,
+        aiohttp.ClientError,
+        serializers.serializers.ValidationError,
+        asyncio.TimeoutError,
+    ) as e:
+        results[":".join([lookup_type, lookup])] = None
+        return
+    except Exception as e:
+        print("ERROR: unhandled - {}, {}".format(e.__class__, e))
         results[":".join([lookup_type, lookup])] = None
         return
-
     results[":".join([lookup_type, lookup])] = profile
 
 
 class SearchMultipleConsumer(AsyncHttpConsumer):
     @wrapper_500
     async def handle(self, body):
+        if self.scope["method"] == "OPTIONS":
+            return await self.send_response(
+                200,
+                b"",
+                headers=[
+                    (b"Content-Type", b"application/json"),
+                    (b"Access-Control-Allow-Origin", b"*"),
+                    (b"Access-Control-Allow-Headers", b"content-type"),
+                ],
+            )
+        ignore_aiohttp_ssl_eror(asyncio.get_running_loop())
         if self.scope["method"] not in ["POST"]:
             return await self.send_response(405, b"")
 
@@ -103,6 +170,7 @@ class SearchMultipleConsumer(AsyncHttpConsumer):
             try:
                 await asyncio.gather(*tasks)
             except Exception as e:
+                print("ERROR: unhandled - {}, {}".format(e.__class__, e))
                 await json_response(self, 500, {})
                 raise
         await json_response(self, 200, results)
-- 
GitLab