Repository: python-discord/sir-lancebot
Issue: KeyError: 'query' (wikipedia cog)

Sentry Issue: [SIR-LANCEBOT-5T](https://sentry.io/organizations/python-discord/issues/2548014774/?referrer=github_integration)

```
KeyError: 'query'
  File "discord/ext/commands/core.py", line 85, in wrapped
    ret = await coro(*args, **kwargs)
  File "bot/exts/evergreen/wikipedia.py", line 78, in wikipedia_search_command
    contents = await self.wiki_request(ctx.channel, search)
  File "bot/exts/evergreen/wikipedia.py", line 42, in wiki_request
    number_of_results = raw_data["query"]["searchinfo"]["totalhits"]

Unhandled command error: 'query'
```

This looks like the request didn't return an expected result, even with a 200 response code.

Changed files:
bot/exts/evergreen/error_handler.py, bot/utils/exceptions.py, bot/exts/evergreen/wikipedia.py

diff --git a/bot/exts/evergreen/error_handler.py b/bot/exts/evergreen/error_handler.py
index 5873fb83..a280c725 100644
--- a/bot/exts/evergreen/error_handler.py
+++ b/bot/exts/evergreen/error_handler.py
@@ -11,7 +11,7 @@ from sentry_sdk import push_scope
 from bot.bot import Bot
 from bot.constants import Channels, Colours, ERROR_REPLIES, NEGATIVE_REPLIES, RedirectOutput
 from bot.utils.decorators import InChannelCheckFailure, InMonthCheckFailure
-from bot.utils.exceptions import UserNotPlayingError
+from bot.utils.exceptions import APIError, UserNotPlayingError
 
 log = logging.getLogger(__name__)
 
@@ -120,6 +120,15 @@ class CommandErrorHandler(commands.Cog):
             await ctx.send("Game not found.")
             return
 
+        if isinstance(error, APIError):
+            await ctx.send(
+                embed=self.error_embed(
+                    f"There was an error when communicating with the {error.api}",
+                    NEGATIVE_REPLIES
+                )
+            )
+            return
+
         with push_scope() as scope:
             scope.user = {
                 "id": ctx.author.id,
diff --git a/bot/exts/evergreen/wikipedia.py b/bot/exts/evergreen/wikipedia.py
index 7b96cb7b..27e68397 100644
--- a/bot/exts/evergreen/wikipedia.py
+++ b/bot/exts/evergreen/wikipedia.py
@@ -2,13 +2,14 @@ import logging
 import re
 from datetime import datetime
 from html import unescape
-from typing import List, Optional
+from typing import List
 
 from discord import Color, Embed, TextChannel
 from discord.ext import commands
 
 from bot.bot import Bot
 from bot.utils import LinePaginator
+from bot.utils.exceptions import APIError
 
 log = logging.getLogger(__name__)
 
@@ -42,43 +43,36 @@ class WikipediaSearch(commands.Cog):
     def __init__(self, bot: Bot):
         self.bot = bot
 
-    async def wiki_request(self, channel: TextChannel, search: str) -> Optional[List[str]]:
+    async def wiki_request(self, channel: TextChannel, search: str) -> List[str]:
         """Search wikipedia search string and return formatted first 10 pages found."""
         params = WIKI_PARAMS | {"srlimit": 10, "srsearch": search}
         async with self.bot.http_session.get(url=SEARCH_API, params=params) as resp:
-            if resp.status == 200:
-                raw_data = await resp.json()
-                number_of_results = raw_data["query"]["searchinfo"]["totalhits"]
-
-                if number_of_results:
-                    results = raw_data["query"]["search"]
-                    lines = []
-
-                    for article in results:
-                        line = WIKI_SEARCH_RESULT.format(
-                            name=article["title"],
-                            description=unescape(
-                                re.sub(
-                                    WIKI_SNIPPET_REGEX, "", article["snippet"]
-                                )
-                            ),
-                            url=f"https://en.wikipedia.org/?curid={article['pageid']}"
-                        )
-                        lines.append(line)
-
-                    return lines
-
-                else:
-                    await channel.send(
-                        "Sorry, we could not find a wikipedia article using that search term."
-                    )
-                    return
-            else:
+            if resp.status != 200:
                 log.info(f"Unexpected response `{resp.status}` while searching wikipedia for `{search}`")
-                await channel.send(
-                    "Whoops, the Wikipedia API is having some issues right now. Try again later."
-                )
-                return
+                raise APIError("Wikipedia API", resp.status)
+
+            raw_data = await resp.json()
+
+            if not raw_data.get("query"):
+                if error := raw_data.get("errors"):
+                    log.error(f"There was an error while communicating with the Wikipedia API: {error}")
+                raise APIError("Wikipedia API", resp.status, error)
+
+            lines = []
+            if raw_data["query"]["searchinfo"]["totalhits"]:
+                for article in raw_data["query"]["search"]:
+                    line = WIKI_SEARCH_RESULT.format(
+                        name=article["title"],
+                        description=unescape(
+                            re.sub(
+                                WIKI_SNIPPET_REGEX, "", article["snippet"]
+                            )
+                        ),
+                        url=f"https://en.wikipedia.org/?curid={article['pageid']}"
+                    )
+                    lines.append(line)
+
+            return lines
 
     @commands.cooldown(1, 10, commands.BucketType.user)
     @commands.command(name="wikipedia", aliases=("wiki",))
@@ -96,6 +90,10 @@ class WikipediaSearch(commands.Cog):
             await LinePaginator.paginate(
                 contents, ctx, embed
             )
+        else:
+            await ctx.send(
+                "Sorry, we could not find a wikipedia article using that search term."
+            )
 
 
 def setup(bot: Bot) -> None:
diff --git a/bot/utils/exceptions.py b/bot/utils/exceptions.py
index 9e080759..bf0e5813 100644
--- a/bot/utils/exceptions.py
+++ b/bot/utils/exceptions.py
@@ -1,4 +1,17 @@
+from typing import Optional
+
+
 class UserNotPlayingError(Exception):
     """Raised when users try to use game commands when they are not playing."""
 
     pass
+
+
+class APIError(Exception):
+    """Raised when an external API (eg. Wikipedia) returns an error response."""
+
+    def __init__(self, api: str, status_code: int, error_msg: Optional[str] = None):
+        super().__init__()
+        self.api = api
+        self.status_code = status_code
+        self.error_msg = error_msg