We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
find_updates: No error handling on JSON fetch
See this code:

https://github.com/sopel-irc/sopel/blob/b105fe4aaa6c1cd258337e60a4f17c1a0751ecb5/sopel/modules/find_updates.py#L49

There's no error-handling at all. If the JSON doesn't parse, Sopel will spit out an exception. If the JSON URL won't load for some reason (times out, connection gets reset, domain name expires, etc.), Sopel will spit out an exception. These are just examples.

This code really needs to be rewritten with a robust `try`/`catch` structure to catch as many possible error conditions as possible. It probably wouldn't have prevented e.g. #1433, but we still should gracefully handle failures in the update checker. At present the `latest.json` file Sopel checks is hosted on Netlify, with very good uptime, but the site is still (very) rarely unavailable.

Bonus points for implementing some kind of logic to detect several failed update checks in a row and also alert the bot's owner to that issue, in case there's a networking issue on Sopel's host machine, or a problem with the update endpoint.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `sopel/modules/find_updates.py`
Content:
```
1 # coding=utf-8
2 """
3 find_updates.py - Sopel Update Check Module
4 This is separated from version.py, so that it can be easily overridden by
5 distribution packagers, and they can check their repositories rather than the
6 Sopel website.
7 Copyright 2014, Elsie Powell, embolalia.com
8 Licensed under the Eiffel Forum License 2.
9 
10 https://sopel.chat
11 """
12 from __future__ import unicode_literals, absolute_import, print_function, division
13 
14 import requests
15 
16 import sopel
17 import sopel.module
18 import sopel.tools
19 
20 
21 wait_time = 24 * 60 * 60  # check once per day
22 startup_check_run = False
23 version_url = 'https://sopel.chat/latest.json'
24 message = (
25     'A new Sopel version, {}, is available. I am running {}. Please update '
26     'me. Full release notes at {}'
27 )
28 unstable_message = (
29     'A new pre-release version, {}, is available. I am running {}. Please '
30     'update me. {}'
31 )
32 
33 
34 @sopel.module.event(sopel.tools.events.RPL_LUSERCLIENT)
35 def startup_version_check(bot, trigger):
36     global startup_check_run
37     if not startup_check_run:
38         startup_check_run = True
39         check_version(bot)
40 
41 
42 @sopel.module.interval(wait_time)
43 def check_version(bot):
44     version = sopel.version_info
45 
46     # TODO: Python3 specific. Disable urllib warning from config file.
47     # requests.packages.urllib3.disable_warnings()
48     info = requests.get(version_url).json()
49     if version.releaselevel == 'final':
50         latest = info['version']
51         notes = info['release_notes']
52     else:
53         latest = info['unstable']
54         notes = info.get('unstable_notes', '')
55         if notes:
56             notes = 'Full release notes at ' + notes
57     latest_version = sopel._version_info(latest)
58     msg = message.format(latest, sopel.__version__, notes)
59 
60     if version < latest_version:
61         bot.say(msg, bot.config.core.owner)
62
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/sopel/modules/find_updates.py b/sopel/modules/find_updates.py
--- a/sopel/modules/find_updates.py
+++ b/sopel/modules/find_updates.py
@@ -39,13 +39,45 @@
         check_version(bot)
 
 
+def _check_succeeded(bot):
+    bot.memory['update_failures'] = 0
+
+
+def _check_failed(bot):
+    bot.memory['update_failures'] = 1 + bot.memory.get('update_failures', 0)
+
+
 @sopel.module.interval(wait_time)
 def check_version(bot):
     version = sopel.version_info
+    success = False
+
+    try:
+        r = requests.get(version_url, timeout=(5, 5))
+    except requests.exceptions.RequestException:
+        _check_failed(bot)
+    else:
+        success = True
+
+    try:
+        if success:
+            info = r.json()
+    except ValueError:
+        # TODO: use JSONDecodeError when dropping Pythons < 3.5
+        _check_failed(bot)
+
+    if not success and bot.memory.get('update_failures', 0) > 4:
+        bot.say("I haven't been able to check for updates in a while. "
+                "Please verify that {} is working and I can reach it."
+                .format(version_url), bot.config.core.owner)
+        bot.say("If this issue persists, please alert the Sopel dev team in "
+                "#sopel on freenode, or open a GitHub issue: "
+                "https://github.com/sopel-irc/sopel/issues",
+                bot.config.core.owner)
+        return
+
+    _check_succeeded(bot)
 
-    # TODO: Python3 specific. Disable urllib warning from config file.
-    # requests.packages.urllib3.disable_warnings()
-    info = requests.get(version_url).json()
     if version.releaselevel == 'final':
         latest = info['version']
         notes = info['release_notes']

{"golden_diff": "diff --git a/sopel/modules/find_updates.py b/sopel/modules/find_updates.py\n--- a/sopel/modules/find_updates.py\n+++ b/sopel/modules/find_updates.py\n@@ -39,13 +39,45 @@\n         check_version(bot)\n \n \n+def _check_succeeded(bot):\n+    bot.memory['update_failures'] = 0\n+\n+\n+def _check_failed(bot):\n+    bot.memory['update_failures'] = 1 + bot.memory.get('update_failures', 0)\n+\n+\n @sopel.module.interval(wait_time)\n def check_version(bot):\n     version = sopel.version_info\n+    success = False\n+\n+    try:\n+        r = requests.get(version_url, timeout=(5, 5))\n+    except requests.exceptions.RequestException:\n+        _check_failed(bot)\n+    else:\n+        success = True\n+\n+    try:\n+        if success:\n+            info = r.json()\n+    except ValueError:\n+        # TODO: use JSONDecodeError when dropping Pythons < 3.5\n+        _check_failed(bot)\n+\n+    if not success and bot.memory.get('update_failures', 0) > 4:\n+        bot.say(\"I haven't been able to check for updates in a while. \"\n+                \"Please verify that {} is working and I can reach it.\"\n+                .format(version_url), bot.config.core.owner)\n+        bot.say(\"If this issue persists, please alert the Sopel dev team in \"\n+                \"#sopel on freenode, or open a GitHub issue: \"\n+                \"https://github.com/sopel-irc/sopel/issues\",\n+                bot.config.core.owner)\n+        return\n+\n+    _check_succeeded(bot)\n \n-    # TODO: Python3 specific. Disable urllib warning from config file.\n-    # requests.packages.urllib3.disable_warnings()\n-    info = requests.get(version_url).json()\n     if version.releaselevel == 'final':\n         latest = info['version']\n         notes = info['release_notes']\n", "issue": "find_updates: No error handling on JSON fetch\nSee this code:\r\n\r\nhttps://github.com/sopel-irc/sopel/blob/b105fe4aaa6c1cd258337e60a4f17c1a0751ecb5/sopel/modules/find_updates.py#L49\r\n\r\nThere's no error-handling at all. If the JSON doesn't parse, Sopel will spit out an exception. If the JSON URL won't load for some reason (times out, connection gets reset, domain name expires, etc.), Sopel will spit out an exception. These are just examples.\r\n\r\nThis code really needs to be rewritten with a robust `try`/`catch` structure to catch as many possible error conditions as possible. It probably wouldn't have prevented e.g. #1433, but we still should gracefully handle failures in the update checker. At present the `latest.json` file Sopel checks is hosted on Netlify, with very good uptime, but the site is still (very) rarely unavailable.\r\n\r\nBonus points for implementing some kind of logic to detect several failed update checks in a row and also alert the bot's owner to that issue, in case there's a networking issue on Sopel's host machine, or a problem with the update endpoint.\n", "before_files": [{"content": "# coding=utf-8\n\"\"\"\nfind_updates.py - Sopel Update Check Module\nThis is separated from version.py, so that it can be easily overridden by\ndistribution packagers, and they can check their repositories rather than the\nSopel website.\nCopyright 2014, Elsie Powell, embolalia.com\nLicensed under the Eiffel Forum License 2.\n\nhttps://sopel.chat\n\"\"\"\nfrom __future__ import unicode_literals, absolute_import, print_function, division\n\nimport requests\n\nimport sopel\nimport sopel.module\nimport sopel.tools\n\n\nwait_time = 24 * 60 * 60  # check once per day\nstartup_check_run = False\nversion_url = 'https://sopel.chat/latest.json'\nmessage = (\n    'A new Sopel version, {}, is available. I am running {}. Please update '\n    'me. Full release notes at {}'\n)\nunstable_message = (\n    'A new pre-release version, {}, is available. I am running {}. Please '\n    'update me. {}'\n)\n\n\n@sopel.module.event(sopel.tools.events.RPL_LUSERCLIENT)\ndef startup_version_check(bot, trigger):\n    global startup_check_run\n    if not startup_check_run:\n        startup_check_run = True\n        check_version(bot)\n\n\n@sopel.module.interval(wait_time)\ndef check_version(bot):\n    version = sopel.version_info\n\n    # TODO: Python3 specific. Disable urllib warning from config file.\n    # requests.packages.urllib3.disable_warnings()\n    info = requests.get(version_url).json()\n    if version.releaselevel == 'final':\n        latest = info['version']\n        notes = info['release_notes']\n    else:\n        latest = info['unstable']\n        notes = info.get('unstable_notes', '')\n        if notes:\n            notes = 'Full release notes at ' + notes\n    latest_version = sopel._version_info(latest)\n    msg = message.format(latest, sopel.__version__, notes)\n\n    if version < latest_version:\n        bot.say(msg, bot.config.core.owner)\n", "path": "sopel/modules/find_updates.py"}], "after_files": [{"content": "# coding=utf-8\n\"\"\"\nfind_updates.py - Sopel Update Check Module\nThis is separated from version.py, so that it can be easily overridden by\ndistribution packagers, and they can check their repositories rather than the\nSopel website.\nCopyright 2014, Elsie Powell, embolalia.com\nLicensed under the Eiffel Forum License 2.\n\nhttps://sopel.chat\n\"\"\"\nfrom __future__ import unicode_literals, absolute_import, print_function, division\n\nimport requests\n\nimport sopel\nimport sopel.module\nimport sopel.tools\n\n\nwait_time = 24 * 60 * 60  # check once per day\nstartup_check_run = False\nversion_url = 'https://sopel.chat/latest.json'\nmessage = (\n    'A new Sopel version, {}, is available. I am running {}. Please update '\n    'me. Full release notes at {}'\n)\nunstable_message = (\n    'A new pre-release version, {}, is available. I am running {}. Please '\n    'update me. {}'\n)\n\n\n@sopel.module.event(sopel.tools.events.RPL_LUSERCLIENT)\ndef startup_version_check(bot, trigger):\n    global startup_check_run\n    if not startup_check_run:\n        startup_check_run = True\n        check_version(bot)\n\n\ndef _check_succeeded(bot):\n    bot.memory['update_failures'] = 0\n\n\ndef _check_failed(bot):\n    bot.memory['update_failures'] = 1 + bot.memory.get('update_failures', 0)\n\n\n@sopel.module.interval(wait_time)\ndef check_version(bot):\n    version = sopel.version_info\n    success = False\n\n    try:\n        r = requests.get(version_url, timeout=(5, 5))\n    except requests.exceptions.RequestException:\n        _check_failed(bot)\n    else:\n        success = True\n\n    try:\n        if success:\n            info = r.json()\n    except ValueError:\n        # TODO: use JSONDecodeError when dropping Pythons < 3.5\n        _check_failed(bot)\n\n    if not success and bot.memory.get('update_failures', 0) > 4:\n        bot.say(\"I haven't been able to check for updates in a while. \"\n                \"Please verify that {} is working and I can reach it.\"\n                .format(version_url), bot.config.core.owner)\n        bot.say(\"If this issue persists, please alert the Sopel dev team in \"\n                \"#sopel on freenode, or open a GitHub issue: \"\n                \"https://github.com/sopel-irc/sopel/issues\",\n                bot.config.core.owner)\n        return\n\n    _check_succeeded(bot)\n\n    if version.releaselevel == 'final':\n        latest = info['version']\n        notes = info['release_notes']\n    else:\n        latest = info['unstable']\n        notes = info.get('unstable_notes', '')\n        if notes:\n            notes = 'Full release notes at ' + notes\n    latest_version = sopel._version_info(latest)\n    msg = message.format(latest, sopel.__version__, notes)\n\n    if version < latest_version:\n        bot.say(msg, bot.config.core.owner)\n", "path": "sopel/modules/find_updates.py"}]}